5 use lib
"$ENV{LJHOME}/cgi-bin";
9 # need ljconfig to set up database connection
13 $LJ::DBIRole
= new DBI
::Role
{
15 my ($dsn, $user, $pass, $role) = @_;
16 return $LJ::MASTER_DB_TIMEOUT
|| 0 if $role && $role eq "master";
17 return $LJ::DB_TIMEOUT
;
19 'sources' => \
%LJ::DBINFO
,
20 'default_db' => "livejournal",
22 'time_report' => \
&LJ
::dbtime_callback
,
30 # name: LJ::DB::time_range_to_ids
31 # des: Performs a binary search on a table's primary id key looking
32 # for time boundaries as specified. Returns the boundary ids
33 # that were found, effectively simulating a key on 'time' for
34 # the specified table.
35 # info: This function shouldn't normally be used, but there are
36 # rare instances where it's useful.
38 # des-opts: A hashref of keys. Keys are:
39 # 'table' - table name to query;
40 # 'roles' - arrayref of db roles to use, in order. Defaults to ['slow'];
41 # 'idcol' - name of 'id' primary key column.
42 # 'timecol' - name of unixtime column to use for constraint;
43 # 'starttime' - starting unixtime time of rows to match;
44 # 'endtime' - ending unixtime of rows to match.
45 # returns: startid, endid; id boundaries which should be used by
49 sub time_range_to_ids
{
52 my $table = delete $args{table
} or croak
("no table arg");
53 my $idcol = delete $args{idcol
} or croak
("no idcol arg");
54 my $timecol = delete $args{timecol
} or croak
("no timecol arg");
55 my $starttime = delete $args{starttime
} or croak
("no starttime arg");
56 my $endtime = delete $args{endtime
} or croak
("no endtime arg");
57 my $roles = delete $args{roles
};
58 unless (ref $roles eq 'ARRAY' && @
$roles) {
61 croak
("bogus args: " . join(",", keys %args))
64 my $db = LJ
::get_dbh
(@
$roles)
65 or die "unable to acquire db handle, roles=", join(",", @
$roles);
67 my ($db_min_id, $db_max_id) = $db->selectrow_array
68 ("SELECT MIN($idcol), MAX($idcol) FROM $table");
69 die $db->errstr if $db->err;
70 die "error finding min/max ids: $db_max_id < $db_min_id"
71 if $db_max_id < $db_min_id;
74 my ($startid, $endid);
77 foreach my $curr_ref ([$starttime => \
$startid], [$endtime => \
$endid]) {
78 my ($want_time, $dest_ref) = @
$curr_ref;
80 my ($min_id, $max_id) = ($db_min_id, $db_max_id);
86 while ($ct++ < $ct_max) {
87 die "unable to find row after $ct tries" if $ct > 100;
89 my $curr_id = $min_id + int(($max_id - $min_id) / 2)+0;
92 "SELECT $idcol, $timecol FROM $table " .
93 "WHERE $idcol>=$curr_id ORDER BY 1 LIMIT 1";
95 $last_time = $curr_time;
96 ($curr_id, $curr_time) = $db->selectrow_array($sql);
97 die $db->errstr if $db->err;
99 # stop condition, two trigger cases:
100 # * we've found exactly the time we want
101 # * we're still narrowing but not finding rows in between, stop here with
102 # the current time being just short of what we were trying to find
103 if ($curr_time == $want_time || $curr_time == $last_time) {
105 # if we never modified the max id, then we
106 # have searched to the end without finding
107 # what we were looking for
108 if ($max_id == $db_max_id && $curr_time <= $want_time) {
109 $$dest_ref = $max_id;
112 } elsif ($min_id == $db_min_id && $curr_time >= $want_time) {
113 $$dest_ref = $min_id;
116 $$dest_ref = $curr_id;
121 # need to traverse into the larger half
122 if ($curr_time < $want_time) {
127 # need to traverse into the smaller half
128 if ($curr_time > $want_time) {
135 return ($startid, $endid);
138 sub _connection_options
{
145 if ( $LJ::IS_DEV_SERVER
) {
146 $options->{'RaiseError'} = 1;
149 $options->{'PrintError'} = 1;
152 return { 'connection_opts' => $options };
156 return $LJ::DBIRole
->get_dbh( _connection_options
(), @_ );
161 my $dbh = dbh_by_role
("master")
162 or die "Couldn't contact master to find name of '$name'\n";
164 my $fdsn = $dbh->selectrow_array("SELECT fdsn FROM dbinfo WHERE name=?", undef, $name);
165 die "No fdsn found for db name '$name'\n" unless $fdsn;
167 return $LJ::DBIRole
->get_dbh_conn( _connection_options
(), $fdsn );
173 return $LJ::DBIRole
->get_dbh_conn( _connection_options
(), $fdsn );
176 sub root_dbh_by_name
{
178 my $dbh = dbh_by_role
("master")
179 or die "Couldn't contact master to find name of '$name'";
181 my $fdsn = $dbh->selectrow_array("SELECT rootfdsn FROM dbinfo WHERE name=?", undef, $name);
182 die "No rootfdsn found for db name '$name'\n" unless $fdsn;
184 return $LJ::DBIRole
->get_dbh_conn( _connection_options
(), $fdsn );
187 sub user_cluster_details
{
189 my $dbh = dbh_by_role
("master") or die;
191 my $role = $dbh->selectrow_array("SELECT role FROM dbweights w, dbinfo i WHERE i.name=? AND i.dbid=w.dbid",
193 return () unless $role && $role =~ /^cluster(\d+)([ab])$/;
202 # when calling a supported function (currently: LJ::load_user() or LJ::load_userid*), LJ::SMS::load_mapping()
203 # ignores in-process request cache, memcache, and selects directly
204 # from the global master
206 # called as: require_master(sub { block })
208 my $callback = shift;
209 croak
"invalid code ref passed to require_master"
210 unless ref $callback eq 'CODE';
212 # run code in the block with local var set
213 local $LJ::_PRAGMA_FORCE_MASTER
= 1;
214 return $callback->();
219 local $LJ::MemCache
::GET_DISABLED
= 1;
224 my ($cond, $sb) = @_;
225 return no_cache
($sb) if $cond;
231 local $LJ::NO_ML_CACHE
= 1;
238 # des: Given one or more roles, returns a database handle.
245 my $opts = ref $_[0] eq "HASH" ?
shift : {};
247 unless (exists $opts->{'max_repl_lag'}) {
248 # for slave or cluster<n>slave roles, don't allow lag
249 if ($_[0] =~ /slave$/) {
250 $opts->{'max_repl_lag'} = $LJ::MAX_REPL_LAG
|| 100_000
;
254 if ($LJ::DEBUG
{'get_dbh'} && $_[0] ne "logs") {
255 my $errmsg = "get_dbh(@_) at \n";
257 while (my ($p, $f, $l) = caller($i++)) {
259 $errmsg .= " $p, $f, $l\n";
266 my $err = LJ
::errobj
("Database::Unavailable",
268 return $err->cond_throw;
271 foreach my $role (@_) {
272 # let site admin turn off global master write access during
274 return $nodb->([@_]) if $LJ::DISABLE_MASTER
&& $role eq "master";
275 my $db = LJ
::get_dbirole_dbh
($opts, $role);
278 return $nodb->([@_]);
282 return LJ
::get_dbh
("master") if $LJ::_PRAGMA_FORCE_MASTER
;
283 return LJ
::get_dbh
("slave", "master");
287 return LJ
::get_dbh
("master");
290 # Use the uniq DB if available, otherwise fall back to the main database
291 # Force the use of the uniq DB by setting $LJ::_PRAGMA_FORCE_UNIQ
292 sub get_uniq_db_reader
{
293 if ($LJ::_PRAGMA_FORCE_UNIQ
) {
294 return $LJ::_PRAGMA_FORCE_MASTER ?
295 LJ
::get_dbh
("uniq_master") :
296 LJ
::get_dbh
("uniq_slave", "uniq_master");
298 return LJ
::get_dbh
("uniq_master", "master") if $LJ::_PRAGMA_FORCE_MASTER
;
299 return LJ
::get_dbh
("uniq_slave", "uniq_master", "slave", "master");
302 sub get_uniq_db_writer
{
303 return LJ
::get_dbh
("uniq_master") if $LJ::_PRAGMA_FORCE_UNIQ
;
304 return LJ
::get_dbh
("uniq_master", "master");
308 # name: LJ::get_cluster_reader
310 # des: Returns a cluster slave for a user, or cluster master if no slaves exist.
312 # des-uarg: Either a userid scalar or a user object.
313 # returns: DB handle. Or undef if all dbs are unavailable.
315 sub get_cluster_reader
317 return LJ
::get_cluster_master
(@_);
321 # name: LJ::get_cluster_def_reader
323 # des: Returns a definitive cluster reader for a given user, used
324 # when the caller wants the master handle, but will only
327 # des-uarg: Either a clusterid scalar or a user object.
328 # returns: DB handle. Or undef if definitive reader is unavailable.
330 sub get_cluster_def_reader
332 return LJ
::get_cluster_master
(@_);
336 # name: LJ::get_cluster_master
338 # des: Returns a cluster master for a given user, used when the caller
339 # might use it to do a write (insert/delete/update/etc...)
341 # des-uarg: Either a clusterid scalar or a user object.
342 # returns: DB handle. Or undef if master is unavailable.
344 sub get_cluster_master
346 my @dbh_opts = scalar(@_) == 2 ?
(shift @_) : ();
348 my $id = LJ
::isu
($arg) ?
$arg->{'clusterid'} : $arg;
349 return undef unless ($id);
350 return undef if $LJ::READONLY_CLUSTER
{$id};
351 return LJ
::get_dbh
(@dbh_opts, LJ
::master_role
($id));
355 ## ouptut: hashiref like {active => 'a', dead => 'b' }
356 sub _cluster_config
{
359 my $block_id = 'cluster_config.rc';
360 my $block = LJ
::ExtBlock
->load_by_id($block_id, {cache_valid
=> 15});
361 return ($block && $block->data->{$cid}) ?
$block->data->{$cid} : {};
365 # input: LJ::User object or cluster id
366 # output: the DBI::Role role name of a cluster master (like 'cluster10a')
370 my $cid = LJ
::isu
($arg) ?
$arg->{'clusterid'} : $arg;
371 if ($LJ::IS_DEV_SERVER
) {
372 return "cluster${cid}";
374 my $ab = _cluster_config
($cid)->{'active'} || 'a';
375 return "cluster${cid}${ab}";
379 # input: LJ::User object or cluster id
380 # output: role name of inactive server, or undef if inactive server is dead
381 sub get_inactive_role
{
384 my $cid = LJ
::isu
($arg) ?
$arg->{'clusterid'} : $arg;
385 if ($LJ::IS_DEV_SERVER
) {
386 return "cluster${cid}";
388 my $c = _cluster_config
($cid);
389 my $ab = ($c && $c->{'active'} && $c->{'active'} eq 'b') ?
'a' : 'b';
390 if ($c && $c->{'dead'} && $c->{'dead'} eq $ab) {
391 ## oops, inactive is dead
394 return "cluster${cid}${ab}";
400 # name: LJ::get_dbirole_dbh
402 # des: Internal function for get_dbh(). Uses the DBIRole to fetch a dbh, with
403 # hooks into db stats-generation if that's turned on.
406 # des-opts: A hashref of options.
407 # des-role: The database role.
410 sub get_dbirole_dbh
{
411 my $dbh = $LJ::DBIRole
->get_dbh( LJ
::DB
::_connection_options
(), @_ )
414 if ( $LJ::DB_LOG_HOST
&& $LJ::HAVE_DBI_PROFILE
) {
415 $LJ::DB_REPORT_HANDLES
{ $dbh->{Name
} } = $dbh;
417 # :TODO: Explain magic number
418 $dbh->{Profile
} ||= "2/DBI::Profile";
420 # And turn off useless (to us) on_destroy() reports, too.
421 undef $DBI::Profile
::ON_DESTROY_DUMP
;
429 # des: get a MySQL lock on a given key/dbrole combination.
430 # returns: undef if called improperly, true on success, die() on failure
431 # args: db, dbrole, lockname, wait_time?
432 # des-dbrole: the role this lock should be gotten on, either 'global' or 'user'.
433 # des-lockname: the name to be used for this lock.
434 # des-wait_time: an optional timeout argument, defaults to 10 seconds.
438 my ($db, $dbrole, $lockname, $wait_time) = @_;
439 return undef unless $db && $lockname;
440 return undef unless $dbrole eq 'global' || $dbrole eq 'user';
442 my $curr_sub = join(", ", ((caller 1)[0..3])); # caller of current sub
444 # die if somebody already has a lock
445 use Carp qw
/cluck confess/;
446 confess
"LOCK ERROR: can't get lock from\n$curr_sub\nbecause it's already taken from\n$LJ::LOCK_OUT{$dbrole}\n"
447 if exists $LJ::LOCK_OUT
{$dbrole};
449 # get a lock from mysql
451 # NOTE: we have to get the result of GET_LOCK, so do NOT use $db->do()
452 my ($got) = $db->selectrow_array( 'SELECT GET_LOCK(?,?)', undef, $lockname, $wait_time );
453 return undef unless $got;
455 # successfully got a lock
456 $LJ::LOCK_OUT
{$dbrole} = $curr_sub;
462 # des: see if we <strong>could</strong> get a MySQL lock on
463 # a given key/dbrole combination, but don't actually get it.
464 # returns: undef if called improperly, true on success, die() on failure
466 # des-dbrole: the role this lock should be gotten on, either 'global' or 'user'.
470 my ($db, $dbrole) = @_;
471 return undef unless $db && ($dbrole eq 'global' || $dbrole eq 'user');
473 # die if somebody already has a lock
474 if ($LJ::LOCK_OUT
{$dbrole}) {
475 my $curr_sub = (caller 1)[3]; # caller of current sub
476 die "LOCK ERROR: $curr_sub; can't get lock from $LJ::LOCK_OUT{$dbrole}\n";
479 # see if a lock is already out
480 return undef if exists $LJ::LOCK_OUT
{$dbrole};
486 # name: LJ::release_lock
487 # des: release a MySQL lock on a given key/dbrole combination.
488 # returns: undef if called improperly, true on success, die() on failure
489 # args: db, dbrole, lockname
490 # des-dbrole: role on which to get this lock, either 'global' or 'user'.
491 # des-lockname: the name to be used for this lock
495 my ($db, $dbrole, $lockname) = @_;
496 return undef unless $db && $lockname;
497 return undef unless $dbrole eq 'global' || $dbrole eq 'user';
499 # get a lock from mysql
500 $db->do("SELECT RELEASE_LOCK(?)", undef, $lockname);
501 delete $LJ::LOCK_OUT
{$dbrole};
507 my ( $db, $dbrole, $lockname ) = @_;
509 return unless $db && $lockname;
510 return unless $dbrole eq 'global' || $dbrole eq 'user';
512 my ($connid) = $db->selectrow_array( 'SELECT IS_USED_LOCK(?)',
515 return $connid ?
1 : 0;
519 my ( $db, $dbrole, $lockname ) = @_;
521 return unless $db && $lockname;
522 return unless $dbrole eq 'global' || $dbrole eq 'user';
524 return ! lock_taken
( $db, $dbrole, $lockname );
528 # name: LJ::disconnect_dbs
529 # des: Clear cached DB handles
532 # clear cached handles
533 $LJ::DBIRole
->disconnect_all( { except
=> [qw(logs)] });
537 # name: LJ::use_diff_db
539 # des: given two DB roles, returns true only if it is certain the two roles are
540 # served by different database servers.
541 # info: This is useful for, say, the moveusercluster script: You would not want
542 # to select something from one DB, copy it into another, and then delete it from the
543 # source if they were both the same machine.
549 $LJ::DBIRole
->use_diff_db(@_);
552 # to be called as &nodb; (so this function sees caller's @_)
555 ref $_[0] eq "LJ::DBSet" || ref $_[0] eq "DBI::db" ||
556 ref $_[0] eq "Apache::DBI::db";
559 sub dbtime_callback
{
560 my ($dsn, $dbtime, $time) = @_;
561 my $diff = abs($dbtime - $time);
563 $dsn =~ /host=([^:\;\|]*)/;
565 print STDERR
"Clock skew of $diff seconds between web($LJ::SERVER_NAME) and db($db)\n";
569 sub foreach_cluster
{
571 my $opts = shift || {};
573 foreach my $cluster_id (@LJ::CLUSTERS
) {
574 if ($opts->{active
}) {
575 my $dbh = LJ
::get_cluster_master
($cluster_id);
576 $coderef->($cluster_id, $dbh);
578 my $dbr = LJ
::DBUtil
->get_inactive_db($cluster_id, $opts->{verbose
});
579 $coderef->($cluster_id, $dbr);
585 sub isdb
{ return ref $_[0] && (ref $_[0] eq "DBI::db" ||
586 ref $_[0] eq "Apache::DBI::db"); }
589 sub bindstr
{ return join(', ', map { '?' } @_); }
591 package LJ
::Error
::Database
::Unavailable
;
592 sub fields
{ qw(roles) } # arrayref of roles requested
596 my $ct = @
{$self->field('roles')};
597 my $clist = join(", ", @
{$self->field('roles')});
599 "Database unavailable for role $clist" :
600 "Database unavailable for roles $clist";
604 package LJ
::Error
::Database
::Failure
;
605 sub fields
{ qw(db) }
607 sub user_caused
{ 0 }
611 my $code = $self->err;
612 my $txt = $self->errstr;
613 return "Database error code $code: $txt";
618 return $self->field('db')->err;
623 return $self->field('db')->errstr;