# This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. package LJ::User; use strict; no warnings 'uninitialized'; use Carp; ######################################################################## ### 5. Database and Memcache Functions =head2 Database and Memcache Functions =cut sub begin_work { my $u = shift; return 1 unless $u->is_innodb; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $rv = $dbcm->begin_work; if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } return $rv; } sub commit { my $u = shift; return 1 unless $u->is_innodb; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $rv = $dbcm->commit; if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } return $rv; } # $u->do("UPDATE foo SET key=?", undef, $val); sub do { my $u = shift; my $query = shift; my $uid = $u->userid + 0 or croak "Database update called on null user object"; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; $query =~ s!^(\s*\w+\s+)!$1/* uid=$uid */ !; my $rv = $dbcm->do( $query, @_ ); if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } $u->{_mysql_insertid} = $dbcm->{'mysql_insertid'} if $dbcm->{'mysql_insertid'}; return $rv; } sub dversion { my $u = shift; return $u->{dversion}; } sub err { my $u = shift; return $u->{_dberr}; } sub errstr { my $u = shift; return $u->{_dberrstr}; } sub is_innodb { my $u = shift; my $cluid = $u->clusterid; return $LJ::CACHE_CLUSTER_IS_INNO{$cluid} if defined $LJ::CACHE_CLUSTER_IS_INNO{$cluid}; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my ( undef, $ctable ) = $dbcm->selectrow_array("SHOW CREATE TABLE log2"); die "Failed to auto-discover database type for cluster \#$cluid: [$ctable]" unless $ctable =~ /^CREATE TABLE/; my $is_inno = ( $ctable =~ /=InnoDB/i ? 1 : 0 ); return $LJ::CACHE_CLUSTER_IS_INNO{$cluid} = $is_inno; } # log2_do # see comments for talk2_do sub log2_do { my ( $u, $errref, $sql, @args ) = @_; return undef unless $u->writer; my $dbcm = $u->{_dbcm}; my $memkey = [ $u->userid, "log2lt:" . $u->userid ]; my $lockkey = $memkey->[1]; $dbcm->selectrow_array( "SELECT GET_LOCK(?,10)", undef, $lockkey ); my $ret = $u->do( $sql, undef, @args ); $$errref = $u->errstr if ref $errref && $u->err; $dbcm->selectrow_array( "SELECT RELEASE_LOCK(?)", undef, $lockkey ); LJ::MemCache::delete( $memkey, 0 ) if int($ret); return $ret; } # simple function for getting something from memcache; this assumes that the # item being gotten follows the standard format [ $userid, "item:$userid" ] sub memc_get { return LJ::MemCache::get( [ $_[0]->userid, "$_[1]:" . $_[0]->userid ] ); } # sets a predictably named item. usage: # $u->memc_set( key => 'value', [ $timeout ] ); sub memc_set { return LJ::MemCache::set( [ $_[0]->userid, "$_[1]:" . $_[0]->userid ], $_[2], $_[3] || 1800 ); } # deletes a predictably named item. usage: # $u->memc_delete( key ); sub memc_delete { return LJ::MemCache::delete( [ $_[0]->userid, "$_[1]:" . $_[0]->userid ] ); } sub mysql_insertid { my $u = shift; if ( $u->isa("LJ::User") ) { return $u->{_mysql_insertid}; } elsif ( LJ::DB::isdb($u) ) { my $db = $u; return $db->{'mysql_insertid'}; } else { die "Unknown object '$u' being passed to LJ::User::mysql_insertid."; } } sub nodb_err { my $u = shift; return "Database handle unavailable [user: " . $u->user . "; cluster: " . $u->clusterid . ", errstr: $DBI::errstr]"; } # get an $sth from the writer sub prepare { my $u = shift; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $rv = $dbcm->prepare(@_); if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } return $rv; } sub quote { my ( $u, $text ) = @_; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; return $dbcm->quote($text); } # memcache key that holds the number of times a user performed one of the rate-limited actions sub rate_memkey { my ( $u, $rp ) = @_; return [ $u->id, "rate:" . $u->id . ":$rp->{id}" ]; } sub readonly { my $u = shift; return LJ::get_cap( $u, "readonly" ); } sub rollback { my $u = shift; return 1 unless $u->is_innodb; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $rv = $dbcm->rollback; if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } return $rv; } sub selectall_arrayref { my $u = shift; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $rv = $dbcm->selectall_arrayref(@_); if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } return $rv; } sub selectall_hashref { my $u = shift; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $rv = $dbcm->selectall_hashref(@_); if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } return $rv; } sub selectcol_arrayref { my $u = shift; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $rv = $dbcm->selectcol_arrayref(@_); if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } return $rv; } sub selectrow_array { my $u = shift; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $set_err = sub { if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } }; if ( wantarray() ) { my @rv = $dbcm->selectrow_array(@_); $set_err->(); return @rv; } my $rv = $dbcm->selectrow_array(@_); $set_err->(); return $rv; } sub selectrow_hashref { my $u = shift; my $dbcm = $u->{'_dbcm'} ||= LJ::get_cluster_master($u) or croak $u->nodb_err; my $rv = $dbcm->selectrow_hashref(@_); if ( $u->{_dberr} = $dbcm->err ) { $u->{_dberrstr} = $dbcm->errstr; } return $rv; } # do some internal consistency checks on self. die if problems, # else returns 1. sub selfassert { my $u = shift; LJ::assert_is( $u->userid, $u->{_orig_userid} ) if $u->{_orig_userid}; LJ::assert_is( $u->user, $u->{_orig_user} ) if $u->{_orig_user}; return 1; } # this is for debugging/special uses where you need to instruct # a user object on what database handle to use. returns the # handle that you gave it. sub set_dbcm { my $u = shift; return $u->{'_dbcm'} = shift; } # class method, returns { clusterid => [ uid, uid ], ... } sub split_by_cluster { my $class = shift; my @uids = @_; my $us = LJ::load_userids(@uids); my %clusters; foreach my $u ( values %$us ) { next unless $u; push @{ $clusters{ $u->clusterid } }, $u->id; } return \%clusters; } # all reads/writes to talk2 must be done inside a lock, so there's # no race conditions between reading from db and putting in memcache. # can't do a db write in between those 2 steps. the talk2 -> memcache # is elsewhere (LJ::Talk), but this $dbh->do wrapper is provided # here because non-talklib things modify the talk2 table, and it's # nice to centralize the locking rules. # # return value is return of $dbh->do. $errref scalar ref is optional, and # if set, gets value of $dbh->errstr # # write: (LJ::talk2_do) # GET_LOCK # update/insert into talk2 # RELEASE_LOCK # delete memcache # # read: (LJ::Talk::get_talk_data) # try memcache # GET_LOCk # read db # update memcache # RELEASE_LOCK sub talk2_do { my ( $u, $nodetype, $nodeid, $errref, $sql, @args ) = @_; return undef unless $nodetype =~ /^\w$/; return undef unless $nodeid =~ /^\d+$/; return undef unless $u->writer; my $dbcm = $u->{_dbcm}; my $userid = $u->userid; my $memkey = [ $userid, "talk2:$userid:$nodetype:$nodeid" ]; my $lockkey = $memkey->[1]; $dbcm->selectrow_array( "SELECT GET_LOCK(?,10)", undef, $lockkey ); my $ret = $u->do( $sql, undef, @args ); $$errref = $u->errstr if ref $errref && $u->err; $dbcm->selectrow_array( "SELECT RELEASE_LOCK(?)", undef, $lockkey ); LJ::MemCache::delete( $memkey, 0 ) if int($ret); return $ret; } sub uncache_prop { my ( $u, $name ) = @_; my $prop = LJ::get_prop( "user", $name ) or die; # FIXME: use exceptions my $userid = $u->userid; LJ::MemCache::delete( [ $userid, "uprop:$userid:$prop->{id}" ] ); delete $u->{$name}; return 1; } sub update_self { my ( $u, $ref ) = @_; return LJ::update_user( $u, $ref ); } # returns self (the $u object which can be used for $u->do) if # user is writable, else 0 sub writer { my $u = shift; return $u if $u->{'_dbcm'} ||= LJ::get_cluster_master($u); return 0; } ######################################################################## ### End LJ::User functions ######################################################################## ### Begin LJ functions package LJ; use Carp; ######################################################################## ### 5. Database and Memcache Functions =head2 Database and Memcache Functions (LJ) =cut sub memcache_get_u { my @keys = @_; my @ret; foreach my $ar ( values %{ LJ::MemCache::get_multi(@keys) || {} } ) { my $row = LJ::MemCache::array_to_hash( "user", $ar ) or next; my $u = LJ::User->new_from_row($row); push @ret, $u; } return wantarray ? @ret : $ret[0]; } # # name: LJ::memcache_kill # des: Kills a memcache entry, given a userid and type. # args: uuserid, type # des-uuserid: a userid or u object # des-type: memcached key type, will be used as "$type:$userid" # returns: results of LJ::MemCache::delete # sub memcache_kill { my ( $uuid, $type ) = @_; my $userid = LJ::want_userid($uuid); return undef unless $userid && $type; return LJ::MemCache::delete( [ $userid, "$type:$userid" ] ); } sub memcache_set_u { my $u = shift; return unless $u; my $expire = time() + 1800; my $ar = LJ::MemCache::hash_to_array( "user", $u ); return unless $ar; LJ::MemCache::set( [ $u->userid, "userid:" . $u->userid ], $ar, $expire ); LJ::MemCache::set( "uidof:" . $u->user, $u->userid ); } # FIXME: this should go away someday... see bug 2760 sub update_user { my ( $u, $ref ) = @_; $u = LJ::want_user($u) or return 0; my $uid = $u->id; my @sets; my @bindparams; my $used_raw = 0; while ( my ( $k, $v ) = each %$ref ) { if ( $k eq "raw" ) { $used_raw = 1; push @sets, $v; } elsif ( $k eq 'email' ) { LJ::set_email( $uid, $v ); } elsif ( $k eq 'password' ) { $u->set_password($v); } else { push @sets, "$k=?"; push @bindparams, $v; } } return 1 unless @sets; my $dbh = LJ::get_db_writer(); return 0 unless $dbh; { local $" = ","; my $where = "userid=$uid"; $dbh->do( "UPDATE user SET @sets WHERE $where", undef, @bindparams ); return 0 if $dbh->err; } if (@LJ::MEMCACHE_SERVERS) { LJ::memcache_kill( $uid, "userid" ); } if ($used_raw) { # for a load of userids from the master after update # so we pick up the values set via the 'raw' option LJ::DB::require_master( sub { LJ::load_userid($uid) } ); } else { while ( my ( $k, $v ) = each %$ref ) { my $cache = $LJ::REQ_CACHE_USER_ID{$uid} or next; $cache->{$k} = $v; } } # log this update LJ::Hooks::run_hooks( "update_user", userid => $uid, fields => $ref ); return 1; } # # name: LJ::wipe_major_memcache # des: invalidate all major memcache items associated with a given user. # args: u # returns: nothing # sub wipe_major_memcache { my $u = shift; my $userid = LJ::want_userid($u); foreach my $key ( "userid", "bio", "talk2ct", "talkleftct", "log2ct", "log2lt", "memkwid", "dayct2", "fgrp", "wt_edges", "wt_edges_rev", "tu", "upicinf", "upiccom", "upicurl", "upicdes", "intids", "memct", "lastcomm", "user_oauth_consumer", "user_oauth_access" ) { LJ::memcache_kill( $userid, $key ); } } # # name: LJ::_load_user_raw # des-db: $dbh/$dbr # des-key: either "userid" or "user" (the WHERE part) # des-vals: value or arrayref of values for key to match on # des-hook: optional code ref to run for each $u # returns: last $u found sub _load_user_raw { my ( $db, $key, $vals, $hook ) = @_; $hook ||= sub { }; $vals = [$vals] unless ref $vals eq "ARRAY"; my $use_isam; unless ( $LJ::CACHE_NO_ISAM{user} || scalar(@$vals) > 10 ) { eval { $db->do("HANDLER user OPEN"); }; if ( $@ || $db->err ) { $LJ::CACHE_NO_ISAM{user} = 1; } else { $use_isam = 1; } } my $last; if ($use_isam) { $key = "PRIMARY" if $key eq "userid"; foreach my $v (@$vals) { my $sth = $db->prepare("HANDLER user READ `$key` = (?) LIMIT 1"); $sth->execute($v); my $row = $sth->fetchrow_hashref; if ($row) { my $u = LJ::User->new_from_row($row); $hook->($u); $last = $u; } } $db->do("HANDLER user close"); } else { my $in = join( ", ", map { $db->quote($_) } @$vals ); my $sth = $db->prepare("SELECT * FROM user WHERE $key IN ($in)"); $sth->execute; while ( my $row = $sth->fetchrow_hashref ) { my $u = LJ::User->new_from_row($row); $hook->($u); $last = $u; } } return $last; } sub _set_u_req_cache { my $u = shift or die "no u to set"; # if we have an existing user singleton, upgrade it with # the latested data, but keep using its address if ( my $eu = $LJ::REQ_CACHE_USER_ID{ $u->userid } ) { LJ::assert_is( $eu->userid, $u->userid ); $eu->selfassert; $u->selfassert; $eu->{$_} = $u->{$_} foreach keys %$u; $u = $eu; } $LJ::REQ_CACHE_USER_NAME{ $u->user } = $u; $LJ::REQ_CACHE_USER_ID{ $u->userid } = $u; return $u; } ######################################################################## ### 23. Relationship Functions =head2 Relationship Functions (formerly ljrelation.pl) =cut # # name: LJ::get_reluser_id # des: for [dbtable[reluser2]], numbers 1 - 31999 are reserved for # livejournal stuff, whereas numbers 32000-65535 are used for local sites. # info: If you wish to add your own hooks to this, you should define a # hook "get_reluser_id" in ljlib-local.pl. No reluser2 [special[reluserdefs]] # types can be a single character, those are reserved for # the [dbtable[reluser]] table, so we don't have namespace problems. # args: type # des-type: the name of the type you're trying to access, e.g. "hide_comm_assoc" # returns: id of type, 0 means it's not a reluser2 type # sub get_reluser_id { my $type = shift; return 0 if length $type == 1; # must be more than a single character my $val = { 'hide_comm_assoc' => 1, }->{$type} + 0; return $val if $val; return 0 unless $type =~ /^local-/; return LJ::Hooks::run_hook( 'get_reluser_id', $type ) + 0; } # # name: LJ::load_rel_user # des: Load user relationship information. Loads all relationships of type 'type' in # which user 'userid' participates on the left side (is the source of the # relationship). # args: db?, userid, type # des-userid: userid or a user hash to load relationship information for. # des-type: type of the relationship # returns: reference to an array of userids # sub load_rel_user { my $db = LJ::DB::isdb( $_[0] ) ? shift : undef; my ( $userid, $type ) = @_; return undef unless $type and $userid; my $u = LJ::want_user($userid); $userid = LJ::want_userid($userid); my $typeid = LJ::get_reluser_id($type) + 0; if ($typeid) { # clustered reluser2 table $db = LJ::get_cluster_reader($u); return $db->selectcol_arrayref( "SELECT targetid FROM reluser2 WHERE userid=? AND type=?", undef, $userid, $typeid ); } else { # non-clustered reluser global table $db ||= LJ::get_db_reader(); return $db->selectcol_arrayref( "SELECT targetid FROM reluser WHERE userid=? AND type=?", undef, $userid, $type ); } } # # name: LJ::load_rel_user_cache # des: Loads user relationship information of the type 'type' where user # 'targetid' participates on the left side (is the source of the relationship) # trying memcache first. The results from this sub should be # treated as inaccurate and out of date. # args: userid, type # des-userid: userid or a user hash to load relationship information for. # des-type: type of the relationship # returns: reference to an array of userids # sub load_rel_user_cache { my ( $userid, $type ) = @_; return undef unless $type && $userid; my $u = LJ::want_user($userid); return undef unless $u; $userid = $u->{'userid'}; my $key = [ $userid, "reluser:$userid:$type" ]; my $res = LJ::MemCache::get($key); return $res if $res; $res = LJ::load_rel_user( $userid, $type ); my $exp = time() + 60 * 30; # 30 min LJ::MemCache::set( $key, $res, $exp ); return $res; } # # name: LJ::load_rel_target # des: Load user relationship information. Loads all relationships of type 'type' in # which user 'targetid' participates on the right side (is the target of the # relationship). # args: db?, targetid, type # des-targetid: userid or a user hash to load relationship information for. # des-type: type of the relationship # returns: reference to an array of userids # sub load_rel_target { my $db = LJ::DB::isdb( $_[0] ) ? shift : undef; my ( $targetid, $type ) = @_; return undef unless $type and $targetid; my $u = LJ::want_user($targetid); $targetid = LJ::want_userid($targetid); my $typeid = LJ::get_reluser_id($type) + 0; if ($typeid) { # clustered reluser2 table $db = LJ::get_cluster_reader($u); return $db->selectcol_arrayref( "SELECT userid FROM reluser2 WHERE targetid=? AND type=?", undef, $targetid, $typeid ); } else { # non-clustered reluser global table $db ||= LJ::get_db_reader(); return $db->selectcol_arrayref( "SELECT userid FROM reluser WHERE targetid=? AND type=?", undef, $targetid, $type ); } } # # name: LJ::load_rel_target_cache # des: Loads user relationship information of the type 'type' where user # 'targetid' participates on the right side (is the target of the relationship) # trying memcache first. The results from this sub should be # treated as inaccurate and out of date. # args: targetid, type # des-userid: userid or a user hash to load relationship information for. # des-type: type of the relationship # returns: reference to an array of userids # sub load_rel_target_cache { my ( $userid, $type ) = @_; return undef unless $type && $userid; my $u = LJ::want_user($userid); return undef unless $u; $userid = $u->{'userid'}; my $key = [ $userid, "reluser_rev:$userid:$type" ]; my $res = LJ::MemCache::get($key); return $res if $res; $res = LJ::load_rel_target( $userid, $type ); my $exp = time() + 60 * 30; # 30 min LJ::MemCache::set( $key, $res, $exp ); return $res; } # # name: LJ::_get_rel_memcache # des: Helper function: returns memcached value for a given (userid, targetid, type) triple, if valid. # args: userid, targetid, type # des-userid: source userid, nonzero # des-targetid: target userid, nonzero # des-type: type (reluser) or typeid (rel2) of the relationship # returns: undef on failure, 0 or 1 depending on edge existence # sub _get_rel_memcache { return undef unless @LJ::MEMCACHE_SERVERS; return undef unless LJ::is_enabled('memcache_reluser'); my ( $userid, $targetid, $type ) = @_; return undef unless $userid && $targetid && defined $type; # memcache keys my $relkey = [ $userid, "rel:$userid:$targetid:$type" ]; # rel $uid->$targetid edge my $modukey = [ $userid, "relmodu:$userid:$type" ]; # rel modtime for uid my $modtkey = [ $targetid, "relmodt:$targetid:$type" ]; # rel modtime for targetid # do a get_multi since $relkey and $modukey are both hashed on $userid my $memc = LJ::MemCache::get_multi( $relkey, $modukey ); return undef unless $memc && ref $memc eq 'HASH'; # [{0|1}, modtime] my $rel = $memc->{ $relkey->[1] }; return undef unless $rel && ref $rel eq 'ARRAY'; # check rel modtime for $userid my $relmodu = $memc->{ $modukey->[1] }; return undef if !$relmodu || $relmodu > $rel->[1]; # check rel modtime for $targetid my $relmodt = LJ::MemCache::get($modtkey); return undef if !$relmodt || $relmodt > $rel->[1]; # return memcache value if it's up-to-date return $rel->[0] ? 1 : 0; } # # name: LJ::_set_rel_memcache # des: Helper function: sets memcache values for a given (userid, targetid, type) triple # args: userid, targetid, type # des-userid: source userid, nonzero # des-targetid: target userid, nonzero # des-type: type (reluser) or typeid (rel2) of the relationship # returns: 1 on success, undef on failure # sub _set_rel_memcache { return 1 unless @LJ::MEMCACHE_SERVERS; my ( $userid, $targetid, $type, $val ) = @_; return undef unless $userid && $targetid && defined $type; $val = $val ? 1 : 0; # memcache keys my $relkey = [ $userid, "rel:$userid:$targetid:$type" ]; # rel $uid->$targetid edge my $modukey = [ $userid, "relmodu:$userid:$type" ]; # rel modtime for uid my $modtkey = [ $targetid, "relmodt:$targetid:$type" ]; # rel modtime for targetid my $now = time(); my $exp = $now + 3600 * 6; # 6 hour LJ::MemCache::set( $relkey, [ $val, $now ], $exp ); LJ::MemCache::set( $modukey, $now, $exp ); LJ::MemCache::set( $modtkey, $now, $exp ); # Also, delete these keys, since the contents have changed. LJ::MemCache::delete( [ $userid, "reluser:$userid:$type" ] ); LJ::MemCache::delete( [ $targetid, "reluser_rev:$targetid:$type" ] ); return 1; } # # name: LJ::check_rel # des: Checks whether two users are in a specified relationship to each other. # args: userid, targetid, type # des-userid: source userid, nonzero; may also be a user hash. # des-targetid: target userid, nonzero; may also be a user hash. # des-type: type of the relationship # returns: 1 if the relationship exists, 0 otherwise # sub check_rel { my ( $userid, $targetid, $type ) = @_; return undef unless $type && $userid && $targetid; my $u = LJ::want_user($userid); $userid = LJ::want_userid($userid); $targetid = LJ::want_userid($targetid); my $typeid = LJ::get_reluser_id($type) + 0; my $eff_type = $typeid || $type; my $key = "$userid-$targetid-$eff_type"; return $LJ::REQ_CACHE_REL{$key} if defined $LJ::REQ_CACHE_REL{$key}; # did we get something from memcache? my $memval = LJ::_get_rel_memcache( $userid, $targetid, $eff_type ); return $memval if defined $memval; # are we working on reluser or reluser2? my ( $db, $table ); if ($typeid) { # clustered reluser2 table $db = LJ::get_cluster_reader($u); $table = "reluser2"; } else { # non-clustered reluser table $db = LJ::get_db_reader(); $table = "reluser"; } # get data from db, force result to be {0|1} my $dbval = $db->selectrow_array( "SELECT COUNT(*) FROM $table " . "WHERE userid=? AND targetid=? AND type=? ", undef, $userid, $targetid, $eff_type ) ? 1 : 0; # set in memcache LJ::_set_rel_memcache( $userid, $targetid, $eff_type, $dbval ); # return and set request cache return $LJ::REQ_CACHE_REL{$key} = $dbval; } # # name: LJ::set_rel # des: Sets relationship information for two users. # args: dbs?, userid, targetid, type # des-dbs: Deprecated; optional, a master/slave set of database handles. # des-userid: source userid, or a user hash # des-targetid: target userid, or a user hash # des-type: type of the relationship # returns: 1 if set succeeded, otherwise undef # sub set_rel { my ( $userid, $targetid, $type ) = @_; return undef unless $type and $userid and $targetid; my $u = LJ::want_user($userid); $userid = LJ::want_userid($userid); $targetid = LJ::want_userid($targetid); my $typeid = LJ::get_reluser_id($type) + 0; my $eff_type = $typeid || $type; # working on reluser or reluser2? my ( $db, $table ); if ($typeid) { # clustered reluser2 table $db = LJ::get_cluster_master($u); $table = "reluser2"; } else { # non-clustered reluser global table $db = LJ::get_db_writer(); $table = "reluser"; } return undef unless $db; # set in database $db->do( "REPLACE INTO $table (userid, targetid, type) VALUES (?, ?, ?)", undef, $userid, $targetid, $eff_type ); return undef if $db->err; # set in memcache LJ::_set_rel_memcache( $userid, $targetid, $eff_type, 1 ); return 1; } # # name: LJ::set_rel_multi # des: Sets relationship edges for lists of user tuples. # args: edges # des-edges: array of arrayrefs of edges to set: [userid, targetid, type]. # Where: # userid: source userid, or a user hash; # targetid: target userid, or a user hash; # type: type of the relationship. # returns: 1 if all sets succeeded, otherwise undef # sub set_rel_multi { return _mod_rel_multi( { mode => 'set', edges => \@_ } ); } # # name: LJ::clear_rel_multi # des: Clear relationship edges for lists of user tuples. # args: edges # des-edges: array of arrayrefs of edges to clear: [userid, targetid, type]. # Where: # userid: source userid, or a user hash; # targetid: target userid, or a user hash; # type: type of the relationship. # returns: 1 if all clears succeeded, otherwise undef # sub clear_rel_multi { return _mod_rel_multi( { mode => 'clear', edges => \@_ } ); } # # name: LJ::_mod_rel_multi # des: Sets/Clears relationship edges for lists of user tuples. # args: keys, edges # des-keys: keys: mode => {clear|set}. # des-edges: edges => array of arrayrefs of edges to set: [userid, targetid, type] # Where: # userid: source userid, or a user hash; # targetid: target userid, or a user hash; # type: type of the relationship. # returns: 1 if all updates succeeded, otherwise undef # sub _mod_rel_multi { my $opts = shift; return undef unless @{ $opts->{edges} }; my $mode = $opts->{mode} eq 'clear' ? 'clear' : 'set'; my $memval = $mode eq 'set' ? 1 : 0; my @reluser = (); # [userid, targetid, type] my @reluser2 = (); foreach my $edge ( @{ $opts->{edges} } ) { my ( $userid, $targetid, $type ) = @$edge; $userid = LJ::want_userid($userid); $targetid = LJ::want_userid($targetid); next unless $type && $userid && $targetid; my $typeid = LJ::get_reluser_id($type) + 0; my $eff_type = $typeid || $type; # working on reluser or reluser2? push @{ $typeid ? \@reluser2 : \@reluser }, [ $userid, $targetid, $eff_type ]; } # now group reluser2 edges by clusterid my %reluser2 = (); # cid => [userid, targetid, type] my $users = LJ::load_userids( map { $_->[0] } @reluser2 ); foreach (@reluser2) { my $cid = $users->{ $_->[0] }->{clusterid} or next; push @{ $reluser2{$cid} }, $_; } @reluser2 = (); # try to get all required cluster masters before we start doing database updates my %cache_dbcm = (); foreach my $cid ( keys %reluser2 ) { next unless @{ $reluser2{$cid} }; # return undef immediately if we won't be able to do all the updates $cache_dbcm{$cid} = LJ::get_cluster_master($cid) or return undef; } # if any error occurs with a cluster, we'll skip over that cluster and continue # trying to process others since we've likely already done some amount of db # updates already, but we'll return undef to signify that everything did not # go smoothly my $ret = 1; # do clustered reluser2 updates foreach my $cid ( keys %cache_dbcm ) { # array of arrayrefs: [userid, targetid, type] my @edges = @{ $reluser2{$cid} }; # set in database, then in memcache. keep the two atomic per clusterid my $dbcm = $cache_dbcm{$cid}; my @vals = map { @$_ } @edges; if ( $mode eq 'set' ) { my $bind = join( ",", map { "(?,?,?)" } @edges ); $dbcm->do( "REPLACE INTO reluser2 (userid, targetid, type) VALUES $bind", undef, @vals ); } if ( $mode eq 'clear' ) { my $where = join( " OR ", map { "(userid=? AND targetid=? AND type=?)" } @edges ); $dbcm->do( "DELETE FROM reluser2 WHERE $where", undef, @vals ); } # don't update memcache if db update failed for this cluster if ( $dbcm->err ) { $ret = undef; next; } # updates to this cluster succeeded, set memcache LJ::_set_rel_memcache( @$_, $memval ) foreach @edges; } # do global reluser updates if (@reluser) { # nothing to do after this block but return, so we can # immediately return undef from here if there's a problem my $dbh = LJ::get_db_writer() or return undef; my @vals = map { @$_ } @reluser; if ( $mode eq 'set' ) { my $bind = join( ",", map { "(?,?,?)" } @reluser ); $dbh->do( "REPLACE INTO reluser (userid, targetid, type) VALUES $bind", undef, @vals ); } if ( $mode eq 'clear' ) { my $where = join( " OR ", map { "userid=? AND targetid=? AND type=?" } @reluser ); $dbh->do( "DELETE FROM reluser WHERE $where", undef, @vals ); } # don't update memcache if db update failed for this cluster return undef if $dbh->err; # $_ = [userid, targetid, type] for each iteration LJ::_set_rel_memcache( @$_, $memval ) foreach @reluser; } return $ret; } # # name: LJ::clear_rel # des: Deletes a relationship between two users or all relationships of a particular type # for one user, on either side of the relationship. # info: One of userid,targetid -- bit not both -- may be '*'. In that case, # if, say, userid is '*', then all relationship edges with target equal to # targetid and of the specified type are deleted. # If both userid and targetid are numbers, just one edge is deleted. # args: dbs?, userid, targetid, type # des-dbs: Deprecated; optional, a master/slave set of database handles. # des-userid: source userid, or a user hash, or '*' # des-targetid: target userid, or a user hash, or '*' # des-type: type of the relationship # returns: 1 if clear succeeded, otherwise undef # sub clear_rel { my ( $userid, $targetid, $type ) = @_; return undef if $userid eq '*' and $targetid eq '*'; my $u; $u = LJ::want_user($userid) unless $userid eq '*'; $userid = LJ::want_userid($userid) unless $userid eq '*'; $targetid = LJ::want_userid($targetid) unless $targetid eq '*'; return undef unless $type && $userid && $targetid; my $typeid = LJ::get_reluser_id($type) + 0; if ($typeid) { # clustered reluser2 table return undef unless $u->writer; $u->do( "DELETE FROM reluser2 WHERE " . ( $userid ne '*' ? "userid=$userid AND " : "" ) . ( $targetid ne '*' ? "targetid=$targetid AND " : "" ) . "type=$typeid" ); return undef if $u->err; } else { # non-clustered global reluser table my $dbh = LJ::get_db_writer() or return undef; my $qtype = $dbh->quote($type); $dbh->do( "DELETE FROM reluser WHERE " . ( $userid ne '*' ? "userid=$userid AND " : "" ) . ( $targetid ne '*' ? "targetid=$targetid AND " : "" ) . "type=$qtype" ); return undef if $dbh->err; } # if one of userid or targetid are '*', then we need to note the modtime # of the reluser edge from the specified id (the one that's not '*') # so that subsequent gets on rel:userid:targetid:type will know to ignore # what they got from memcache my $eff_type = $typeid || $type; if ( $userid eq '*' ) { LJ::MemCache::set( [ $targetid, "relmodt:$targetid:$eff_type" ], time() ); } elsif ( $targetid eq '*' ) { LJ::MemCache::set( [ $userid, "relmodu:$userid:$eff_type" ], time() ); # if neither userid nor targetid are '*', then just call _set_rel_memcache # to update the rel:userid:targetid:type memcache key as well as the # userid and targetid modtime keys } else { LJ::_set_rel_memcache( $userid, $targetid, $eff_type, 0 ); } return 1; } 1;