mourningdove/cgi-bin/DW/User/Edges/WatchTrust/Loader.pm

380 lines
12 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
#
# DW::User::Edges::WatchTrust::Loader
#
# Helper functions for loading data from memcache and the database for watch and
# trust edge data. These functions are not directly callable by users, only
# the WatchTrust edge system should call these.
#
# DO NOT CALL THESE FUNCTIONS FROM OUTSIDE THE WT SYSTEM.
#
# Authors:
# Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2009 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#
package DW::User::Edges::WatchTrust::Loader;
use strict;
use Carp qw/ confess /;
# returns trustmask between two users
sub _trustmask {
my ( $from_userid, $to_userid ) = @_;
my $memkey = [ $from_userid, "trustmask:$from_userid:$to_userid" ];
my $mask = LJ::MemCache::get($memkey);
unless ( defined $mask ) {
my $dbr = LJ::get_db_reader();
die "No database reader available" unless $dbr;
$mask = $dbr->selectrow_array(
'SELECT groupmask FROM wt_edges WHERE from_userid = ? AND to_userid = ?',
undef, $from_userid, $to_userid );
return 0 if $dbr->err;
$mask = $mask ? $mask + 0 : 0; # force numeric
LJ::MemCache::set( $memkey, $mask, 3600 );
}
return $mask;
}
# actually get friend/friendof uids, should not be called directly
# FYI: in original LJ this function had a Gearman ability that was dropped when we
# migrated to DW. that functionality might be necessary for load reasons for large
# communities in the future and may need to be readded.
sub _wt_userids {
my ( $u, %args ) = @_;
my $limit = int( delete $args{limit} ) || 50000;
my $mode = delete $args{mode};
my $reverse = delete $args{reverse} || 0;
confess 'unknown option' if %args;
my $sql;
my $memkey;
if ( $mode eq 'watch' ) {
if ($reverse) {
$sql =
"SELECT from_userid FROM wt_edges WHERE to_userid=? AND groupmask & 1<<61 LIMIT $limit";
$memkey = [ $u->id, "watched_by:" . $u->id ];
}
else {
$sql =
"SELECT to_userid FROM wt_edges WHERE from_userid=? AND groupmask & 1<<61 LIMIT $limit";
$memkey = [ $u->id, "watched:" . $u->id ];
}
}
elsif ( $mode eq 'trust' ) {
if ($reverse) {
$sql =
"SELECT from_userid FROM wt_edges WHERE to_userid=? AND groupmask & 1 LIMIT $limit";
$memkey = [ $u->id, "trusted_by:" . $u->id ];
}
else {
$sql =
"SELECT to_userid FROM wt_edges WHERE from_userid=? AND groupmask & 1 LIMIT $limit";
$memkey = [ $u->id, "trusted:" . $u->id ];
}
}
else {
confess "mode must either be 'watch' or 'trust'";
}
if ( my $pack = LJ::MemCache::get($memkey) ) {
my ( $slimit, @uids ) = unpack( "N*", $pack );
# value in memcache is good if stored limit (from last time)
# is >= the limit currently being requested. we just may
# have to truncate it to match the requested limit
if ( $slimit >= $limit ) {
@uids = @uids[ 0 .. $limit - 1 ] if @uids > $limit;
return @uids;
}
# value in memcache is also good if number of items is less
# than the stored limit... because then we know it's the full
# set that got stored, not a truncated version.
return @uids if @uids < $slimit;
}
my $dbr = LJ::get_db_reader();
my $uids = $dbr->selectcol_arrayref( $sql, undef, $u->id );
# if the list of uids is greater than 950k
# -- slow but this definitely works
my $pack = pack( "N*", $limit );
foreach (@$uids) {
last if length $pack > 1024 * 950;
$pack .= pack( "N*", $_ );
}
LJ::MemCache::add( $memkey, $pack, 3600 ) if $uids;
return @$uids;
}
# helper to filter the _wt_list by a groupmask AND
sub _filter_wt_list {
my ( $mask, $raw ) = @_;
return {} unless $mask; # undef/0 = no matches!
return undef unless defined $raw && ref $raw eq 'HASH';
return $raw unless keys %$raw;
return {
map { $_ => $raw->{$_} }
grep { $raw->{$_}->{groupmask} & $mask }
keys %$raw
};
}
# helper, simply passes down to _wt_list_memc and filters
sub _watch_list_memc { return _filter_wt_list( 1 << 61, _wt_list_memc(@_) ); }
sub _watch_list_db { return _filter_wt_list( 1 << 61, _wt_list_db(@_) ); }
sub _trust_list_memc { return _filter_wt_list( 1, _wt_list_memc(@_) ); }
sub _trust_list_db { return _filter_wt_list( 1, _wt_list_db(@_) ); }
sub _trust_group_members_memc { return _filter_wt_list( shift(), _wt_list_memc(@_) ); }
sub _trust_group_members_db { return _filter_wt_list( shift(), _wt_list_db(@_) ); }
# attempt to load a user's watch list from memcache
sub _wt_list_memc {
my ( $u, %args ) = @_;
# variable setup
my %rows; # rows to be returned
my $userid = $u->id; # helper to use it a lot
my $ver = 2; # memcache data version
my $packfmt = "NH6H6QC"; # pack format
my $packlen = 19; # length of $packfmt in bytes
my @cols = qw/ to_userid fgcolor bgcolor groupmask showbydefault /;
# first, check memcache
my $key = ( $args{community_okay} && $u->is_community ) ? 'c_wt_list' : 'wt_list';
my $memkey = [ $userid, "$key:$userid" ];
my $memdata = LJ::MemCache::get($memkey);
return undef unless $memdata;
# first byte of object is data version
# only version 1 is meaningful right now
my $memver = substr( $memdata, 0, 1, '' );
return undef unless $memver == $ver;
# get each $packlen-byte row
while ( length($memdata) >= $packlen ) {
my @row = unpack( $packfmt, substr( $memdata, 0, $packlen, '' ) );
# add "#" to beginning of colors
$row[$_] = "\#$row[$_]" foreach 1 .. 2;
# turn unpacked row into hashref
my $to_userid = $row[0];
my $idx = 1;
foreach my $col ( @cols[ 1 .. $#cols ] ) {
$rows{$to_userid}->{$col} = $row[ $idx++ ];
}
}
# got from memcache, return
return \%rows;
}
# attempt to load a user's watch list from the database
sub _wt_list_db {
my ( $u, %args ) = @_;
my $userid = $u->id;
my $dbh = LJ::get_db_writer();
my $lockname = "get_wt_list:$userid";
my $release_lock = sub {
LJ::DB::release_lock( $dbh, "global", $lockname );
return $_[0];
};
# get a lock
my $lock = LJ::DB::get_lock( $dbh, "global", $lockname );
return {} unless $lock;
# in lock, try memcache first (unless told not to)
my $memc =
$args{force_database}
? undef
: _wt_list_memc( $u, community_okay => $args{community_okay} );
return $release_lock->($memc) if $memc;
# we are now inside the lock, but memcache was empty, so we must query
# the database to get the data
# memcache data info
my $ver = 2; # memcache data version
my $key = ( $args{community_okay} && $u->is_community ) ? 'c_wt_list' : 'wt_list';
my $memkey = [ $userid, "$key:$userid" ];
my $packfmt = "NH6H6QC"; # pack format
my $packlen = 19; # length of $packfmt
# columns we're selecting
my $mempack = $ver; # full packed string to insert into memcache, byte 1 is dversion
my %rows; # rows object to be returned, all groupmasks match
# at this point we branch. if we're trying to get the list of things the community
# watches - for usage in the friends page only - then we change paths.
if ( $u->is_community && $args{community_okay} ) {
# simply get userids from elsewhen to build %rows
foreach my $uid ( $u->member_userids ) {
# pack data into list, but only store up to 950k of data before
# bailing. (practical limit of 64k watch list entries.)
#
# also note that we fake a lot of this, since communities don't actually
# have a watch list.
my $newpack = pack( $packfmt, ( $uid, '000000', 'ffffff', 1 << 61, '1' ) );
last if length($mempack) + length($newpack) > 950 * 1024;
$mempack .= $newpack;
# more faking it for fun and profit
$rows{$uid} = {
to_userid => $uid,
fgcolor => '#000000',
bgcolor => '#ffffff',
groupmask => 1 << 61,
showbydefault => '1',
};
}
# now stuff in memcache and bail
LJ::MemCache::set( $memkey, $mempack );
return $release_lock->( \%rows );
}
# at this point, if they're not an individual, then throw an empty set of data in memcache
# and bail out. only individuals have watch lists.
unless ( $u->is_individual ) {
LJ::MemCache::set( $memkey, $mempack );
return $release_lock->( \%rows );
}
# actual watching path
my @cols = qw/ to_userid fgcolor bgcolor groupmask showbydefault /;
# try the SQL on the master database
my $sth = $dbh->prepare( 'SELECT to_userid, fgcolor, bgcolor, groupmask, showbydefault '
. 'FROM wt_edges WHERE from_userid = ?' );
$sth->execute($userid);
confess $dbh->errstr if $dbh->err;
# iterate over each row and prepare result
while ( my @row = $sth->fetchrow_array ) {
# convert color columns to hex
$row[$_] = sprintf( "%06x", $row[$_] ) foreach 1 .. 2;
# pack data into list, but only store up to 950k of data before
# bailing. (practical limit of 64k watch list entries.)
my $newpack = pack( $packfmt, @row );
last if length($mempack) + length($newpack) > 950 * 1024;
$mempack .= $newpack;
# add "#" to beginning of colors
$row[$_] = "\#$row[$_]" foreach 1 .. 2;
my $to_userid = $row[0];
my $idx = 1;
foreach my $col ( @cols[ 1 .. $#cols ] ) {
$rows{$to_userid}->{$col} = $row[ $idx++ ];
}
}
# now stuff in memcache
LJ::MemCache::set( $memkey, $mempack );
# finished with lock, release it
return $release_lock->( \%rows );
}
# returns a hashref for a trust group
sub _trust_groups {
my ( $u, $bit, $name ) = @_;
# memcache data version number
my $ver = 2;
# helper function for iterating through groups
my $fg;
my $find_grp = sub {
# $fg format:
# [ version, [userid, bitnum, name, sortorder, public], [...], ... ]
my $memver = shift @$fg;
return undef unless $memver == $ver;
# bit number was specified
if ($bit) {
foreach (@$fg) {
return LJ::MemCache::array_to_hash( 'trust_group', [ $memver, @$_ ] )
if $_->[1] == $bit;
}
return undef;
}
# group name was specified
if ($name) {
foreach (@$fg) {
return LJ::MemCache::array_to_hash( 'trust_group', [ $memver, @$_ ] )
if lc( $_->[2] ) eq $name;
}
return undef;
}
# no arg, return entire object
if (wantarray) { # group list sorted by sortorder || name order
return map { LJ::MemCache::array_to_hash( 'trust_group', [ $memver, @$_ ] ) }
sort { $a->[3] <=> $b->[3] || $a->[2] cmp $b->[2] } @$fg;
}
else { # ref to hash keyed by bitnum
return {
map { $_->[1] => LJ::MemCache::array_to_hash( 'trust_group', [ $memver, @$_ ] ) }
@$fg
};
}
};
# check memcache
my $userid = $u->id;
my $memkey = [ $userid, "trust_group:$userid" ];
$fg = LJ::MemCache::get($memkey);
return $find_grp->() if $fg;
# check database
$fg = [$ver];
my $db = LJ::get_cluster_def_reader($u);
return undef unless $db;
my $sth = $db->prepare( 'SELECT userid, groupnum, groupname, sortorder, is_public '
. 'FROM trust_groups WHERE userid = ?' );
$sth->execute($userid);
return LJ::error($db) if $db->err;
my @row;
push @$fg, [@row] while @row = $sth->fetchrow_array;
# set in memcache
LJ::MemCache::set( $memkey, $fg );
return $find_grp->();
}
1;