#!/usr/bin/perl BEGIN { die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n" unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH}; unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib"; }; use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); use Test::More; # TableSyncer and its required modules: use OptionParser; use NibbleIterator; use TableSyncer; use MasterSlave; use Quoter; use RowChecksum; use Retry; use TableParser; use TableNibbler; use TableParser; use ChangeHandler; use RowDiff; use RowSyncer; use RowSyncerBidirectional; use RowChecksum; use DSNParser; use Cxn; use Transformers; use Sandbox; use PerconaTest; use constant MKDEBUG => $ENV{MKDEBUG} || 0; $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} = 1; my $dp = new DSNParser(opts=>$dsn_opts); my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp); my $dbh = $sb->get_dbh_for('master'); my $src_dbh = $sb->get_dbh_for('master'); my $dst_dbh = $sb->get_dbh_for('slave1'); if ( !$src_dbh || !$dbh ) { plan skip_all => 'Cannot connect to sandbox master'; } elsif ( !$dst_dbh ) { plan skip_all => 'Cannot connect to sandbox slave'; } else { plan tests => 33; } $sb->create_dbs($dbh, ['test']); $sb->load_file('master', 't/lib/samples/before-TableSyncChunk.sql'); # ########################################################################### # Make a TableSyncer object. # ########################################################################### my $ms = new MasterSlave(); my $o = new OptionParser(description => 'TableSyncer'); my $q = new Quoter(); my $tp = new TableParser(Quoter => $q); my $tn = new TableNibbler(TableParser => $tp, Quoter => $q); my $rc = new RowChecksum(OptionParser => $o, Quoter => $q); my $rd = new RowDiff(dbh=>$dbh); my $rt = new Retry(); my $syncer = new TableSyncer( MasterSlave => $ms, OptionParser => $o, Quoter => $q, TableParser => $tp, TableNibbler => $tn, RowChecksum => $rc, RowDiff => $rd, Retry => $rt, ); isa_ok($syncer, 'TableSyncer'); $o->get_specs("$trunk/bin/pt-table-sync"); $o->get_opts(); my $src_cxn = new Cxn( DSNParser => $dp, OptionParser => $o, dsn_string => "h=127.1,P=12345,u=msandbox,p=msandbox", dbh => $src_dbh, ); $src_cxn->{is_source} = 1; my $dst_cxn = new Cxn( DSNParser => $dp, OptionParser => $o, dsn_string => "h=127.1,P=12346,u=msandbox,p=msandbox", dbh => $dst_dbh, ); # Global vars used/set by the subs below and accessed throughout the tests. my $src; my $dst; my $tbl_struct; my %actions; my @rows; sub new_ch { my ( $dbh, $queue ) = @_; my $ch = new ChangeHandler( Quoter => $q, left_db => $src->{tbl}->{db}, left_tbl => $src->{tbl}->{tbl}, right_db => $dst->{tbl}->{db}, right_tbl => $dst->{tbl}->{tbl}, actions => [ sub { my ( $sql, $change_dbh ) = @_; push @rows, $sql; if ( $change_dbh ) { # dbh passed through change() or process_rows() $change_dbh->do($sql); } elsif ( $dbh ) { # dbh passed to this sub $dbh->do($sql); } else { # default dst dbh for this test script $dst_cxn->dbh()->do($sql); } } ], replace => 0, queue => defined $queue ? $queue : 1, ); $ch->fetch_back($src_cxn->dbh()); return $ch; } # Shortens/automates a lot of the setup needed for calling # TableSyncer::sync_table. At minimum, you can pass just # the src and dst args which are db.tbl args to sync. Various # global vars are set: @rows, %actions, etc. sub sync_table { my ( %args ) = @_; my ($src_db_tbl, $dst_db_tbl) = @args{qw(src dst)}; my ($src_db, $src_tbl) = $q->split_unquote($src_db_tbl); my ($dst_db, $dst_tbl) = $q->split_unquote($dst_db_tbl); @ARGV = $args{argv} ? @{$args{argv}} : (); $o->get_opts(); $tbl_struct = $tp->parse( $tp->get_create_table($src_cxn->dbh(), $src_db, $src_tbl)); $src = { Cxn => $src_cxn, misc_dbh => $src_cxn->dbh(), tbl => { db => $src_db, tbl => $src_tbl, tbl_struct => $tbl_struct, }, }; $dst = { Cxn => $dst_cxn, misc_dbh => $src_cxn->dbh(), tbl => { db => $dst_db, tbl => $dst_tbl, tbl_struct => $tbl_struct, }, }; @rows = (); my $ch = $args{ChangeHandler} || new_ch(); my $rs = $args{RowSyncer} || new RowSyncer(ChangeHandler => $ch, OptionParser => $o); return if $args{fake}; %actions = $syncer->sync_table( src => $src, dst => $dst, RowSyncer => $rs, ChangeHandler => $ch, trace => 0, changing_src => $args{changing_src}, one_nibble => $args{one_nibble}, ); return \%actions; } # ########################################################################### # Test sync_table() for each plugin with a basic, 4 row data set. # ########################################################################### # test1 has 4 rows and test2, which is the same struct, is empty. # So after sync, test2 should have the same 4 rows as test1. my $test1_rows = [ [qw(1 en)], [qw(2 ca)], [qw(3 ab)], [qw(4 bz)], ]; my $inserts = [ "INSERT INTO `test`.`test2`(`a`, `b`) VALUES ('1', 'en')", "INSERT INTO `test`.`test2`(`a`, `b`) VALUES ('2', 'ca')", "INSERT INTO `test`.`test2`(`a`, `b`) VALUES ('3', 'ab')", "INSERT INTO `test`.`test2`(`a`, `b`) VALUES ('4', 'bz')", ]; # First, do a dry run sync, so nothing should happen. $dst_dbh->do('TRUNCATE TABLE test.test2'); my $output = output( sub { sync_table( src => "test.test1", dst => "test.test2", argv => [qw(--explain)], ); } ); is_deeply( \%actions, { DELETE => 0, INSERT => 0, REPLACE => 0, UPDATE => 0, }, 'Dry run, no changes' ); is_deeply( \@rows, [], 'Dry run, no SQL statements made' ); is_deeply( $dst_dbh->selectall_arrayref('SELECT * FROM test.test2 ORDER BY a, b'), [], 'Dry run, no rows changed' ); # Now do the real syncs that should insert 4 rows into test2. sync_table( src => "test.test1", dst => "test.test2", ); is_deeply( \%actions, { DELETE => 0, INSERT => 4, REPLACE => 0, UPDATE => 0, }, 'Basic sync 4 INSERT' ); is_deeply( \@rows, $inserts, 'Basic sync ChangeHandler INSERT statements' ); is_deeply( $dst_dbh->selectall_arrayref('SELECT * FROM test.test2 ORDER BY a, b'), $test1_rows, 'Basic sync dst rows match src rows' ); # ############################################################################# # Check that the plugins can resolve unique key violations. # ############################################################################# sync_table( src => "test.test3", dst => "test.test4", argv => [qw(--chunk-size 1)], one_nibble => 0, ); is_deeply( $dst_dbh->selectall_arrayref('select * from test.test4 order by a', { Slice => {}} ), [ { a => 1, b => 2 }, { a => 2, b => 1 } ], 'Resolves unique key violations' ); # ########################################################################### # Test locking. # ########################################################################### sub clear_genlogs { my ($msg) = @_; if ( $msg ) { `echo "xxx $msg" >> /tmp/12345/data/genlog`; `echo "xxx $msg" >> /tmp/12346/data/genlog`; } else { `echo > /tmp/12345/data/genlog`; `echo > /tmp/12346/data/genlog`; } warn "cleared" } sync_table( src => "test.test1", dst => "test.test2", argv => [qw(--lock 1)], ); # The locks should be released. ok($src_dbh->do('select * from test.test4'), 'Chunk locks released'); sync_table( src => "test.test1", dst => "test.test2", argv => [qw(--lock 2)], ); # The locks should be released. ok($src_dbh->do('select * from test.test4'), 'Table locks released'); sync_table( src => "test.test1", dst => "test.test2", argv => [qw(--lock 3)], ); ok( $dbh->do('replace into test.test3 select * from test.test3 limit 0'), 'Does not lock in level 3 locking' ); eval { $syncer->lock_and_wait( lock_level => 3, host => $src, src => $src, ); }; is($EVAL_ERROR, '', 'Locks in level 3'); # See DBI man page. use POSIX ':signal_h'; my $mask = POSIX::SigSet->new(SIGALRM); # signals to mask in the handler my $action = POSIX::SigAction->new( sub { die "maatkit timeout" }, $mask, ); my $oldaction = POSIX::SigAction->new(); sigaction( SIGALRM, $action, $oldaction ); throws_ok ( sub { alarm 1; $dbh->do('replace into test.test3 select * from test.test3 limit 0'); }, qr/maatkit timeout/, "Level 3 lock NOT released", ); # Kill the DBHs it in the right order: there's a connection waiting on # a lock. $src_cxn->dbh()->disconnect(); $dst_cxn->dbh()->disconnect(); $dst_cxn->connect(); $src_cxn->connect(); # ########################################################################### # Test TableSyncGroupBy. # ########################################################################### $sb->load_file('master', 't/lib/samples/before-TableSyncGroupBy.sql'); PerconaTest::wait_for_table($dst_cxn->dbh(), "test.test2", "a=4"); sync_table( src => "test.test1", dst => "test.test2", ); is_deeply( $dst_cxn->dbh()->selectall_arrayref('select * from test.test2 order by a, b, c', { Slice => {}} ), [ { a => 1, b => 2, c => 3 }, { a => 1, b => 2, c => 3 }, { a => 1, b => 2, c => 3 }, { a => 1, b => 2, c => 3 }, { a => 2, b => 2, c => 3 }, { a => 2, b => 2, c => 3 }, { a => 2, b => 2, c => 3 }, { a => 2, b => 2, c => 3 }, { a => 3, b => 2, c => 3 }, { a => 3, b => 2, c => 3 }, ], 'Table synced with GroupBy', ); # ############################################################################# # Issue 96: mk-table-sync: Nibbler infinite loop # ############################################################################# $sb->load_file('master', 't/lib/samples/issue_96.sql'); PerconaTest::wait_for_table($dst_cxn->dbh(), "issue_96.t2", "from_city='jr'"); # Make paranoid-sure that the tables differ. my $r1 = $src_cxn->dbh()->selectall_arrayref('SELECT from_city FROM issue_96.t WHERE package_id=4'); my $r2 = $dst_cxn->dbh()->selectall_arrayref('SELECT from_city FROM issue_96.t2 WHERE package_id=4'); is_deeply( [ $r1->[0]->[0], $r2->[0]->[0] ], [ 'ta', 'zz' ], 'Infinite loop table differs (issue 96)' ); sync_table( src => "issue_96.t", dst => "issue_96.t2", ); $r1 = $src_cxn->dbh()->selectall_arrayref('SELECT from_city FROM issue_96.t WHERE package_id=4'); $r2 = $dst_cxn->dbh()->selectall_arrayref('SELECT from_city FROM issue_96.t2 WHERE package_id=4'); # Other tests below rely on this table being synced, so die # if it fails to sync. is( $r1->[0]->[0], $r2->[0]->[0], 'Sync infinite loop table (issue 96)' ) or die "Failed to sync issue_96.t"; # ############################################################################# # Test check_permissions(). # ############################################################################# # Re-using issue_96.t from above. is( $syncer->have_all_privs($src_cxn->dbh(), 'issue_96', 't'), 1, 'Have all privs' ); diag(`/tmp/12345/use -u root -e "CREATE USER 'bob'\@'\%' IDENTIFIED BY 'bob'"`); diag(`/tmp/12345/use -u root -e "GRANT select ON issue_96.t TO 'bob'\@'\%'"`); my $bob_dbh = DBI->connect( "DBI:mysql:;host=127.0.0.1;port=12345", 'bob', 'bob', { PrintError => 0, RaiseError => 1 }); is( $syncer->have_all_privs($bob_dbh, 'issue_96', 't'), 0, "Don't have all privs, just select" ); diag(`/tmp/12345/use -u root -e "GRANT insert ON issue_96.t TO 'bob'\@'\%'"`); is( $syncer->have_all_privs($bob_dbh, 'issue_96', 't'), 0, "Don't have all privs, just select and insert" ); diag(`/tmp/12345/use -u root -e "GRANT update ON issue_96.t TO 'bob'\@'\%'"`); is( $syncer->have_all_privs($bob_dbh, 'issue_96', 't'), 0, "Don't have all privs, just select, insert and update" ); diag(`/tmp/12345/use -u root -e "GRANT delete ON issue_96.t TO 'bob'\@'\%'"`); is( $syncer->have_all_privs($bob_dbh, 'issue_96', 't'), 1, "Bob got his privs" ); diag(`/tmp/12345/use -u root -e "DROP USER 'bob'"`); # ########################################################################### # Test that the calback gives us the src and dst sql. # ########################################################################### # Re-using issue_96.t from above. The tables are already in sync so there # should only be 1 sync cycle. $output = output( sub { sync_table( src => "issue_96.t", dst => "issue_96.t2", argv => [qw(--chunk-size 1000 --explain)], ); } ); # TODO: improve this test like( $output, qr/AS crc FROM `issue_96`.`t`/, "--explain" ); # ############################################################################# # Issue 464: Make mk-table-sync do two-way sync # ############################################################################# diag(`$trunk/sandbox/start-sandbox master 12348 >/dev/null`); my $dbh3 = $sb->get_dbh_for('master1'); SKIP: { skip 'Cannot connect to sandbox master', 7 unless $dbh; skip 'Cannot connect to second sandbox master', 7 unless $dbh3; my $sync_chunk; # Switch "source" to master2 (12348). $dst_cxn = new Cxn( DSNParser => $dp, OptionParser => $o, dsn_string => "h=127.1,P=12345,u=msandbox,p=msandbox", dbh => $dbh3, ); # Proper data on both tables after bidirectional sync. my $bidi_data = [ [1, 'abc', 1, '2010-02-01 05:45:30'], [2, 'def', 2, '2010-01-31 06:11:11'], [3, 'ghi', 5, '2010-02-01 09:17:52'], [4, 'jkl', 6, '2010-02-01 10:11:33'], [5, undef, 0, '2010-02-02 05:10:00'], [6, 'p', 4, '2010-01-31 10:17:00'], [7, 'qrs', 5, '2010-02-01 10:11:11'], [8, 'tuv', 6, '2010-01-31 10:17:20'], [9, 'wxy', 7, '2010-02-01 10:17:00'], [10, 'z', 8, '2010-01-31 10:17:08'], [11, '?', 0, '2010-01-29 11:17:12'], [12, '', 0, '2010-02-01 11:17:00'], [13, 'hmm', 1, '2010-02-02 12:17:31'], [14, undef, 0, '2010-01-31 10:17:00'], [15, 'gtg', 7, '2010-02-02 06:01:08'], [17, 'good', 1, '2010-02-02 21:38:03'], [20, 'new', 100, '2010-02-01 04:15:36'], ]; # ######################################################################## # First bidi test with chunk size=2, roughly 9 chunks. # ######################################################################## # Load "master" data. $sb->load_file('master', 't/pt-table-sync/samples/bidirectional/table.sql'); $sb->load_file('master', 't/pt-table-sync/samples/bidirectional/master-data.sql'); # Load remote data. $sb->load_file('master1', 't/pt-table-sync/samples/bidirectional/table.sql'); $sb->load_file('master1', 't/pt-table-sync/samples/bidirectional/remote-1.sql'); # This is hack to get things setup correctly. sync_table( src => "bidi.t", dst => "bidi.t", ChangeHandler => 1, RowSyncer => 1, fake => 1, ); my $ch = new_ch($dbh3, 0); my $rs = new RowSyncerBidirectional( ChangeHandler => $ch, OptionParser => $o, ); sync_table( src => "bidi.t", dst => "bidi.t", changing_src => 1, argv => [qw(--chunk-size 2 --conflict-error ignore --conflict-column ts --conflict-comparison newest)], ChangeHandler => $ch, RowSyncer => $rs, ); my $res = $src_cxn->dbh()->selectall_arrayref('select * from bidi.t order by id'); is_deeply( $res, $bidi_data, 'Bidirectional sync "master" (chunk size 2)' ); $res = $dbh3->selectall_arrayref('select * from bidi.t order by id'); is_deeply( $res, $bidi_data, 'Bidirectional sync remote-1 (chunk size 2)' ); # ######################################################################## # Test it again with a larger chunk size, roughly half the table. # ######################################################################## $sb->load_file('master', 't/pt-table-sync/samples/bidirectional/table.sql'); $sb->load_file('master', 't/pt-table-sync/samples/bidirectional/master-data.sql'); $sb->load_file('master1', 't/pt-table-sync/samples/bidirectional/table.sql'); $sb->load_file('master1', 't/pt-table-sync/samples/bidirectional/remote-1.sql'); # This is hack to get things setup correctly. sync_table( src => "bidi.t", dst => "bidi.t", ChangeHandler => 1, RowSyncer => 1, fake => 1, ); $ch = new_ch($dbh3, 0); $rs = new RowSyncerBidirectional( ChangeHandler => $ch, OptionParser => $o, ); sync_table( src => "bidi.t", dst => "bidi.t", changing_src => 1, argv => [qw(--chunk-size 10 --conflict-error ignore --conflict-column ts --conflict-comparison newest)], ChangeHandler => $ch, RowSyncer => $rs, ); $res = $src_cxn->dbh()->selectall_arrayref('select * from bidi.t order by id'); is_deeply( $res, $bidi_data, 'Bidirectional sync "master" (chunk size 10)' ); $res = $dbh3->selectall_arrayref('select * from bidi.t order by id'); is_deeply( $res, $bidi_data, 'Bidirectional sync remote-1 (chunk size 10)' ); # ######################################################################## # Chunk whole table. # ######################################################################## $sb->load_file('master', 't/pt-table-sync/samples/bidirectional/table.sql'); $sb->load_file('master', 't/pt-table-sync/samples/bidirectional/master-data.sql'); $sb->load_file('master1', 't/pt-table-sync/samples/bidirectional/table.sql'); $sb->load_file('master1', 't/pt-table-sync/samples/bidirectional/remote-1.sql'); # This is hack to get things setup correctly. sync_table( src => "bidi.t", dst => "bidi.t", ChangeHandler => 1, RowSyncer => 1, fake => 1, ); $ch = new_ch($dbh3, 0); $rs = new RowSyncerBidirectional( ChangeHandler => $ch, OptionParser => $o, ); sync_table( src => "bidi.t", dst => "bidi.t", changing_src => 1, argv => [qw(--chunk-size 1000 --conflict-error ignore --conflict-column ts --conflict-comparison newest)], ChangeHandler => $ch, RowSyncer => $rs, ); $res = $src_cxn->dbh()->selectall_arrayref('select * from bidi.t order by id'); is_deeply( $res, $bidi_data, 'Bidirectional sync "master" (whole table chunk)' ); $res = $dbh3->selectall_arrayref('select * from bidi.t order by id'); is_deeply( $res, $bidi_data, 'Bidirectional sync remote-1 (whole table chunk)' ); # ######################################################################## # See TableSyncer.pm for why this is so. # ######################################################################## # $args{ChangeHandler} = new_ch($dbh3, 1); # throws_ok( # sub { $syncer->sync_table(%args, bidirectional => 1) }, # qr/Queueing does not work with bidirectional syncing/, # 'Queueing does not work with bidirectional syncing' #); diag(`$trunk/sandbox/stop-sandbox 12348 >/dev/null &`); # Set dest back to slave1 (12346). $dst_cxn = new Cxn( DSNParser => $dp, OptionParser => $o, dsn_string => "h=127.1,P=12346,u=msandbox,p=msandbox", dbh => $dst_dbh, ); } # ############################################################################# # Test with transactions. # ############################################################################# # Sandbox::get_dbh_for() defaults to AutoCommit=1. Autocommit must # be off else commit() will cause an error. $dbh = $sb->get_dbh_for('master', {AutoCommit=>0}); $src_cxn->dbh()->disconnect(); $dst_cxn->dbh()->disconnect(); $src_cxn->set_dbh($sb->get_dbh_for('master', {AutoCommit=>0})); $dst_cxn->set_dbh($sb->get_dbh_for('slave1', {AutoCommit=>0})); sync_table( src => "test.test1", dst => "test.test1", argv => [qw(--transaction --lock 1)], ); # There are no diffs. This just tests that the code doesn't crash # when transaction is true. is_deeply( \@rows, [], "Sync with transaction" ); sync_table( src => "sakila.actor", dst => "sakila.actor", fake => 1, # don't actually sync ); $syncer->lock_and_wait( lock_level => 1, host => $src, src => $src, ); my $cid = $src_cxn->dbh()->selectrow_arrayref("SELECT CONNECTION_ID()")->[0]; $src_cxn->dbh()->do("SELECT * FROM sakila.actor WHERE 1=1 LIMIT 2 FOR UPDATE"); my $idb_status = $src_cxn->dbh()->selectrow_hashref("SHOW /*!40100 ENGINE*/ INNODB STATUS"); $src_cxn->dbh()->commit(); like( $idb_status->{status}, qr/MySQL thread id $cid, query id \d+/, "Open transaction" ); # ############################################################################# # Issue 672: mk-table-sync should COALESCE to avoid undef # ############################################################################# $sb->load_file('master', "t/lib/samples/empty_tables.sql"); PerconaTest::wait_for_table($dst_cxn->dbh(), 'et.et1'); sync_table( src => 'et.et1', dst => 'et.et1', ); is_deeply( \@rows, [], "Sync empty tables" ); # ############################################################################# # Retry wait. # ############################################################################# diag(`/tmp/12346/use -e "stop slave"`); $output = ''; { local *STDERR; open STDERR, '>', \$output; sync_table( src => "sakila.actor", dst => "sakila.actor", fake => 1, # don't actually sync argv => [qw(--lock 1 --wait 60)], ); throws_ok( sub { $syncer->lock_and_wait( lock_level => 1, host => $dst, src => $src, wait_retry_args => { wait => 1, tries => 2, }, ); }, qr/Slave did not catch up to its master after 2 attempts of waiting 60/, "Retries wait" ); } diag(`$trunk/sandbox/test-env reset`); # ############################################################################# # Done. # ############################################################################# { local *STDERR; open STDERR, '>', \$output; $syncer->_d('Complete test coverage'); } like( $output, qr/Complete test coverage/, '_d() works' ); $sb->wipe_clean($src_cxn->dbh()); $sb->wipe_clean($dst_cxn->dbh()); exit;