#!/usr/bonsaitools/bin/perl -w # -*- Mode: perl; indent-tabs-mode: nil -*- # # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of # the License at http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or # implied. See the License for the specific language governing # rights and limitations under the License. # # The Original Code is the Bugzilla Bug Tracking System. # # The Initial Developer of the Original Code is Netscape Communications # Corporation. Portions created by Netscape are # Copyright (C) 1998 Netscape Communications Corporation. All # Rights Reserved. # # Contributor(s): Terry Weissman # David Gardiner use strict; require "globals.pl"; require "defparams.pl"; # Shut up misguided -w warnings about "used only once". "use vars" just # doesn't work for me. sub sillyness { my $zz; open SAVEOUT,">/dev/null"; $zz = $::db; $zz = $::dbwritesallowed; } my $verbose = 0; my $syncall = 0; my $shutdown = 0; my $tempdir = "data"; my $force = 0; my $shutdown_msg = "Bugzilla is temporarily disabled while the database is backed up. Try again in a few minutes."; sub Usage { print "Usage: syncshadowdb [-v] [-syncall] [-shutdown] [-tempdir dirname] [-force]\n"; exit; } while (my $opt = shift @ARGV) { if ($opt eq '-v') { $verbose = 1; } elsif ($opt eq '-syncall') { $syncall = 1; $verbose = 1; } elsif ($opt eq '-shutdown') { $shutdown = 1; } elsif ($opt eq '-tempdir') { my $dir = shift @ARGV; if (-d $dir) { $tempdir = $dir; } else { print "$dir does not exist or is not a directory. No syncing performed"; exit; } } elsif ($opt eq '-force') { $force = 1; } elsif ($opt eq '--') { # do nothing - null parameter so we can use # multi-param system() call in globals.pl } else { Usage(); } } $| = 1; my $logtostderr = 0; sub Verbose ($) { my ($str) = (@_); if ($verbose) { if ($logtostderr) { print STDERR $str, "\n"; } else { print $str, "\n"; } } } if (!Param("shadowdb")) { Verbose("We don't have shadow databases turned on; no syncing performed."); exit; } if (Param("shutdownhtml") && ! $force) { Verbose("Bugzilla was shutdown prior to running syncshadowdb. \n" . " If you wish to sync anyway, use the -force command line option"); exit; } my $wasshutdown = ""; if ($shutdown) { Verbose ("Shutting down bugzilla and waiting for connections to clear"); # Record the old shutdownhtml so it can be restored at the end (this will # only be an issue if we are called using the -force command line param) $wasshutdown = Param("shutdownhtml"); $::param{'shutdownhtml'} = $shutdown_msg; WriteParams(); # Now we need to wait for existing connections to this database to clear. We # do this by looking for connections to the main or shadow database using # 'mysqladmin processlist' my $cmd = "$::mysqlpath/mysqladmin -u $::db_user"; if ($::db_pass) { $cmd .= " -p$::db_pass" } $cmd .= " processlist"; my $found_proc = 1; # We need to put together a nice little regular expression to use in the # following loop that'll tell us if the return from mysqladmin contains # either the main or shadow database. my @dbs = ($::db_name, Param("shadowdb")); my $db_expr = "^\\s*(" . join ("\|", @dbs) . ")\\s*\$"; # Don't let this thing wait forever... my $starttime = time(); while ($found_proc) { $found_proc = 0; open (PROC, $cmd . "|"); my @output = ; close (PROC); foreach my $line(@output) { my @info = split (/\|/, $line); # Ignore any line that doesn't have 9 pieces of info # or contain Id (pretty printing crap) if ($#info != 9 || $line =~ /Id/) { next } if ($info[4] =~ m/$db_expr/) { $found_proc = 1; } } # If there are still active connections to Bugzilla 10 minutes after # shutting it down, then something is wrong. if ((time() - $starttime) > 600) { # There should be a better way to notify the admin of something bad like # this happening. Verbose ("*** Waited for 10 minutes and there were still active \n" . " connections to the bugzilla database. Giving up."); $::param{'shutdownhtml'} = $wasshutdown; WriteParams(); exit; } } } my $wasusing = Param("queryagainstshadowdb"); $::param{'queryagainstshadowdb'} = 1; # Force us to be able to use the # shadowdb, even if other processes # are not supposed to. ConnectToDatabase(1); Verbose("Acquiring lock"); if ( $syncall == 1) { SendSQL("SELECT GET_LOCK('synclock', 2700)"); } else { SendSQL("SELECT GET_LOCK('synclock', 1)"); } if (!FetchOneColumn()) { Verbose("Couldn't get the lock to do the shadow database syncing."); exit; } my $shadowtable = "$::db_name.shadowlog"; if (!$syncall) { Verbose("Looking for requests to sync the whole database."); SendSQL("SELECT id FROM $shadowtable " . "WHERE reflected = 0 AND command = 'SYNCUP'"); if (FetchOneColumn()) { $syncall = 1; } } if ($syncall) { Verbose("Syncing up the shadow database by copying entire database in."); if ($wasusing) { $::param{'queryagainstshadowdb'} = 0; WriteParams(); if (! $shutdown) { Verbose("Disabled reading from the shadowdb. Sleeping 10 seconds to let other procs catch up."); sleep(10); } $::param{'queryagainstshadowdb'} = 1; } my @tables; SendSQL("SHOW TABLES"); my $query = ""; while (MoreSQLData()) { my $table = FetchOneColumn(); push(@tables, $table); if ($query) { $query .= ", $table WRITE"; } else { $query = "LOCK TABLES $table WRITE"; } } if (@tables) { Verbose("Locking entire shadow database"); SendSQL($query); foreach my $table (@tables) { Verbose("Dropping old shadow table $table"); SendSQL("DROP TABLE $table"); } SendSQL("UNLOCK TABLES"); } # Carefully lock the whole real database for reading, except for the # shadowlog table, which we lock for writing. Then dump everything # into the shadowdb database. Then mark everything in the shadowlog # as reflected. Only then unlock everything. This sequence causes # us to be sure not to miss anything or get something twice. SendSQL("USE $::db_name"); SendSQL("SHOW TABLES"); @tables = (); $query = "LOCK TABLES shadowlog WRITE"; while (MoreSQLData()) { my $table = FetchOneColumn(); if ($table ne "shadowlog") { $query .= ", $table READ"; push(@tables, $table); } } Verbose("Locking entire database"); SendSQL($query); my $tempfile = "$tempdir/tmpsyncshadow.$$"; Verbose("Dumping database to a temp file ($tempfile)."); my @ARGS = ("-u", $::db_user); if ($::db_pass) { push @ARGS, "-p$::db_pass" } push @ARGS, "-l", "-e", $::db_name, @tables; open SAVEOUT, ">&STDOUT"; # stash the original output stream open STDOUT, ">$tempfile"; # redirect to file select STDOUT; $| = 1; # disable buffering system("$::mysqlpath/mysqldump", @ARGS); open STDOUT, ">&SAVEOUT"; # redirect back to original stream Verbose("Restoring from tempfile into shadowdb"); my $extra = "-u $::db_user"; if ($::db_pass) { $extra .= " -p$::db_pass"; } if ($verbose) { $extra .= " -v"; } open(MYSQL, "cat $tempfile | $::mysqlpath/mysql $extra " . Param("shadowdb") . "|") || die "Couldn't do db copy"; my $count = 0; while () { print "."; $count++; if ($count % 70 == 0) { print "\n"; } } close(MYSQL); unlink($tempfile); Verbose(""); $::dbwritesallowed = 1; # SendSQL("UPDATE shadowlog SET reflected = 1 WHERE reflected = 0", 1); SendSQL("DELETE FROM shadowlog", 1); SendSQL("UNLOCK TABLES"); if ($wasusing) { Verbose("Reenabling other processes to read from the shadow db"); $::param{'queryagainstshadowdb'} = 1; WriteParams(); } if ($shutdown) { Verbose("Restoring the original shutdown message (if any)"); $::param{'shutdownhtml'} = $wasshutdown; WriteParams(); } Verbose("OK, done."); } Verbose("Looking for commands to execute."); $::dbwritesallowed = 1; # Make us low priority, to not block anyone who is trying to actually use # the shadowdb. Note that this is carefully coded to ignore errors; we want # to keep going even on older mysqld's that don't have the # SQL_LOW_PRIORITY_UPDATES option. $::db->do("SET OPTION SQL_LOW_PRIORITY_UPDATES = 1"); while (1) { SendSQL("SELECT id, command FROM $shadowtable WHERE reflected = 0 " . "ORDER BY id LIMIT 1"); my ($id, $command) = (FetchSQLData()); if (!$id) { last; } Verbose("Executing command in shadow db: $command"); SendSQL($command, 1); SendSQL("UPDATE $shadowtable SET reflected = 1 WHERE id = $id", 1); } Verbose("Releasing lock."); SendSQL("SELECT RELEASE_LOCK('synclock')");