#!/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 <terry@mozilla.org>
#                 David Gardiner <david.gardiner@unisa.edu.au>

use diagnostics;
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 = <PROC>;
        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 (<MYSQL>) {
        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')");