diff --git a/bin/pt-config-diff b/bin/pt-config-diff index 84edccb8..8ebd2a10 100755 --- a/bin/pt-config-diff +++ b/bin/pt-config-diff @@ -1466,6 +1466,7 @@ use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); use Scalar::Util qw(blessed); + use constant { PTDEBUG => $ENV{PTDEBUG} || 0, PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0, @@ -1572,21 +1573,6 @@ sub name { return $self->{hostname} || $self->{dsn_name} || 'unknown host'; } -sub is_cluster_node { - my ($self) = @_; - return $self->{is_cluster_node} if defined $self->{is_cluster_node}; - - my $sql = "SHOW VARIABLES LIKE 'wsrep_on'"; - PTDEBUG && _d($sql); - my $row = $self->{dbh}->selectrow_arrayref($sql); - PTDEBUG && _d(defined $row ? @$row : 'undef'); - $self->{is_cluster_node} = $row && $row->[1] - ? ($row->[1] eq 'ON' || $row->[1] eq '1') - : 0; - - return $self->{is_cluster_node}; -} - sub DESTROY { my ($self) = @_; if ( $self->{dbh} diff --git a/bin/pt-kill b/bin/pt-kill index 72c9f3f8..7af09248 100755 --- a/bin/pt-kill +++ b/bin/pt-kill @@ -2478,16 +2478,20 @@ $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Quotekeys = 0; +local $EVAL_ERROR; +eval { + require Quoter; +}; + sub new { my ( $class, %args ) = @_; - my @required_args = qw(Quoter); - foreach my $arg ( @required_args ) { - die "I need a $arg argument" unless $args{$arg}; - } my $self = { %args }; + $self->{Quoter} ||= Quoter->new(); return bless $self, $class; } +sub Quoter { shift->{Quoter} } + sub get_create_table { my ( $self, $dbh, $db, $tbl ) = @_; die "I need a dbh parameter" unless $dbh; @@ -4711,6 +4715,7 @@ use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); use Scalar::Util qw(blessed); + use constant { PTDEBUG => $ENV{PTDEBUG} || 0, PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0, @@ -4817,21 +4822,6 @@ sub name { return $self->{hostname} || $self->{dsn_name} || 'unknown host'; } -sub is_cluster_node { - my ($self) = @_; - return $self->{is_cluster_node} if defined $self->{is_cluster_node}; - - my $sql = "SHOW VARIABLES LIKE 'wsrep_on'"; - PTDEBUG && _d($sql); - my $row = $self->{dbh}->selectrow_arrayref($sql); - PTDEBUG && _d(defined $row ? @$row : 'undef'); - $self->{is_cluster_node} = $row && $row->[1] - ? ($row->[1] eq 'ON' || $row->[1] eq '1') - : 0; - - return $self->{is_cluster_node}; -} - sub DESTROY { my ($self) = @_; if ( $self->{dbh} diff --git a/bin/pt-online-schema-change b/bin/pt-online-schema-change index a1170a48..5829bfc7 100755 --- a/bin/pt-online-schema-change +++ b/bin/pt-online-schema-change @@ -2712,16 +2712,20 @@ $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Quotekeys = 0; +local $EVAL_ERROR; +eval { + require Quoter; +}; + sub new { my ( $class, %args ) = @_; - my @required_args = qw(Quoter); - foreach my $arg ( @required_args ) { - die "I need a $arg argument" unless $args{$arg}; - } my $self = { %args }; + $self->{Quoter} ||= Quoter->new(); return bless $self, $class; } +sub Quoter { shift->{Quoter} } + sub get_create_table { my ( $self, $dbh, $db, $tbl ) = @_; die "I need a dbh parameter" unless $dbh; @@ -3341,6 +3345,7 @@ use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); use Scalar::Util qw(blessed); + use constant { PTDEBUG => $ENV{PTDEBUG} || 0, PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0, @@ -3447,21 +3452,6 @@ sub name { return $self->{hostname} || $self->{dsn_name} || 'unknown host'; } -sub is_cluster_node { - my ($self) = @_; - return $self->{is_cluster_node} if defined $self->{is_cluster_node}; - - my $sql = "SHOW VARIABLES LIKE 'wsrep_on'"; - PTDEBUG && _d($sql); - my $row = $self->{dbh}->selectrow_arrayref($sql); - PTDEBUG && _d(defined $row ? @$row : 'undef'); - $self->{is_cluster_node} = $row && $row->[1] - ? ($row->[1] eq 'ON' || $row->[1] eq '1') - : 0; - - return $self->{is_cluster_node}; -} - sub DESTROY { my ($self) = @_; if ( $self->{dbh} diff --git a/bin/pt-table-checksum b/bin/pt-table-checksum index 5f92d279..597d0d20 100755 --- a/bin/pt-table-checksum +++ b/bin/pt-table-checksum @@ -21,6 +21,7 @@ BEGIN { OptionParser Mo Cxn + Percona::XtraDB::Cluster Quoter VersionParser TableParser @@ -3211,6 +3212,7 @@ use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); use Scalar::Util qw(blessed); + use constant { PTDEBUG => $ENV{PTDEBUG} || 0, PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0, @@ -3317,21 +3319,6 @@ sub name { return $self->{hostname} || $self->{dsn_name} || 'unknown host'; } -sub is_cluster_node { - my ($self) = @_; - return $self->{is_cluster_node} if defined $self->{is_cluster_node}; - - my $sql = "SHOW VARIABLES LIKE 'wsrep_on'"; - PTDEBUG && _d($sql); - my $row = $self->{dbh}->selectrow_arrayref($sql); - PTDEBUG && _d(defined $row ? @$row : 'undef'); - $self->{is_cluster_node} = $row && $row->[1] - ? ($row->[1] eq 'ON' || $row->[1] eq '1') - : 0; - - return $self->{is_cluster_node}; -} - sub DESTROY { my ($self) = @_; if ( $self->{dbh} @@ -3357,6 +3344,104 @@ sub _d { # End Cxn package # ########################################################################### +# ########################################################################### +# Percona::XtraDB::Cluster package +# This package is a copy without comments from the original. The original +# with comments and its test file can be found in the Bazaar repository at, +# lib/Percona/XtraDB/Cluster.pm +# t/lib/Percona/XtraDB/Cluster.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ + +package Percona::XtraDB::Cluster; +use Mo; +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +sub is_cluster_node { + my ($self, $cxn) = @_; + return $self->{is_cluster_node}->{$cxn} if defined $self->{is_cluster_node}->{$cxn}; + + my $sql = "SHOW VARIABLES LIKE 'wsrep_on'"; + PTDEBUG && _d($sql); + my $row = $cxn->dbh->selectrow_arrayref($sql); + PTDEBUG && _d(defined $row ? @$row : 'undef'); + $self->{is_cluster_node}->{$cxn} = $row && $row->[1] + ? ($row->[1] eq 'ON' || $row->[1] eq '1') + : 0; + + return $self->{is_cluster_node}->{$cxn}; +} + +sub same_cluster { + my ($self, $cxn1, $cxn2) = @_; + return unless $self->is_cluster_node($cxn1) && $self->is_cluster_node($cxn2); + return if $self->is_master_of($cxn1, $cxn2) || $self->is_master_of($cxn2, $cxn1); + + my $sql = q{SHOW VARIABLES LIKE 'wsrep_cluster_name'}; + PTDEBUG && _d($sql); + my (undef, $row) = $cxn1->dbh->selectrow_array($sql); + my (undef, $cxn2_row) = $cxn2->dbh->selectrow_array($sql); + + return unless $row eq $cxn2_row; + + $sql = q{SHOW VARIABLES LIKE 'wsrep_cluster_address'}; + PTDEBUG && _d($sql); + my (undef, $addr) = $cxn1->dbh->selectrow_array($sql); + my (undef, $cxn2_addr) = $cxn2->dbh->selectrow_array($sql); + + return if $addr eq 'gcomm://' && $cxn2_addr eq 'gcomm://'; + + if ( $addr eq 'gcomm://' ) { + $addr = $self->_find_full_gcomm_addr($cxn1->dbh); + } + elsif ( $cxn2_addr eq 'gcomm://' ) { + $cxn2_addr = $self->_find_full_gcomm_addr($cxn2->dbh); + } + + return 1 if lc($addr) eq lc($cxn2_addr); + + return 1; +} + +sub is_master_of { + my ($self, $cxn1, $cxn2) = @_; + + my $cxn2_dbh = $cxn2->dbh; + my $sql = q{SHOW SLAVE STATUS}; + PTDEBUG && _d($sql); + local $cxn2_dbh->{FetchHashKeyName} = 'NAME_lc'; + my $slave_status = $cxn2_dbh->selectrow_hashref($sql); + return unless ref($slave_status) eq 'HASH'; + + my $port = $cxn1->dsn->{P}; + return unless $slave_status->{master_port} eq $port; + return 1 if $cxn1->dsn->{h} eq $slave_status->{master_host}; + + my $host = scalar gethostbyname($cxn1->dsn->{h}); + my $master_host = scalar gethostbyname($slave_status->{master_host}); + return 1 if $master_host eq $host; + return; +} + +sub _find_full_gcomm_addr { + my ($self, $dbh) = @_; + + my $sql = q{SHOW VARIABLES LIKE 'wsrep_provider_options'}; + PTDEBUG && _d($sql); + my (undef, $provider_opts) = $dbh->selectrow_array($sql); + my ($prov_addr) = $provider_opts =~ m{\Qgmcast.listen_addr\E\s*=\s*tcp://([^:]+:[0-9]+)\s*;}i; + my $full_gcomm = "gcomm://$prov_addr"; + PTDEBUG && _d("gcomm address: ", $full_gcomm); + return $full_gcomm; +} + +1; +} +# ########################################################################### +# End Percona::XtraDB::Cluster package +# ########################################################################### + # ########################################################################### # Quoter package # This package is a copy without comments from the original. The original @@ -5281,7 +5366,7 @@ sub _get_hash_func { } my ($dbh) = @args{@required_args}; my $o = $self->{OptionParser}; - my @funcs = qw(CRC32 FNV1A_64 FNV_64 MD5 SHA1); + my @funcs = qw(CRC32 FNV1A_64 FNV_64 MURMUR_HASH MD5 SHA1); if ( my $func = $o->get('function') ) { unshift @funcs, $func; @@ -5302,7 +5387,7 @@ sub _get_hash_func { PTDEBUG && _d('Chosen hash func:', $result); return $func; } - die $error || 'No hash functions (CRC32, MD5, etc.) are available'; + die($error || 'No hash functions (CRC32, MD5, etc.) are available'); } sub _get_crc_width { @@ -8535,6 +8620,9 @@ sub main { $have_time = sub { return 1; }; } + # PXC helper class + my $cluster = Percona::XtraDB::Cluster->new(); + # ######################################################################## # If this is not a dry run (--explain was not specified), then we're # going to checksum the tables, so do the necessary preparations and @@ -8604,12 +8692,42 @@ sub main { die $err if $err; } - if ( $master_cxn->is_cluster_node() && !@$slaves ) { - die $master_cxn->name() . " is a cluster node but no other nodes " - . "or regular replicas were found. Use --recursion-method=dsn " - . "to specify the other nodes in the cluster.\n"; + if ( $cluster->is_cluster_node($master_cxn) ) { + if ( !@$slaves ) { + die $master_cxn->name() . " is a cluster node but no other nodes " + . "or regular replicas were found. Use --recursion-method=dsn " + . "to specify the other nodes in the cluster.\n"; + } + else { + my $err = ''; + for my $slave (@$slaves) { + if ( $cluster->is_cluster_node($slave) + && !$cluster->same_cluster($slave, $master_cxn) ) { + $err .= $slave->name() . " is a cluster node, but doesn't " + . "belong to the same cluster as " . $master_cxn->name() + . ". This is not currently supported; You can try " + . "using --recursion-method=dsn to specify all nodes " + . "in the slave cluster.\n" + } + } + warn $err if $err; + } } - + elsif ( @$slaves ) { + my $err = ''; + for my $slave (@$slaves) { + if ( $cluster->is_cluster_node($slave) ) { + $err .= $slave->name() . " is a cluster node, but " + . $master_cxn->name() . " is not. This is not currently " + . "supported; You can try to specify " + . "all nodes in the cluster with " + . "--recursion-method=dsn if you want them checksummed.\n" + } + } + warn $err if $err; + } + + if ( $o->get('check-slave-lag') ) { PTDEBUG && _d('Will use --check-slave-lag to check for slave lag'); my $cxn = $make_cxn->( @@ -8629,7 +8747,8 @@ sub main { # to appear should be sufficient. @$slave_lag_cxns = grep { my $slave_cxn = $_; - if ( $slave_cxn->is_cluster_node() ) { + if ( $cluster->is_cluster_node($slave_cxn) + && $cluster->same_cluster($master_cxn, $slave_cxn) ) { warn "Not checking replica lag on " . $slave_cxn->name() . " because it is a cluster node.\n"; 0; @@ -8844,7 +8963,7 @@ sub main { . "(db, tbl, chunk, chunk_index," . " lower_boundary, upper_boundary, this_cnt, this_crc) " . "SELECT" - . ($master_cxn->is_cluster_node() ? ' /*!99997*/' : '') + . ($cluster->is_cluster_node($master_cxn) ? ' /*!99997*/' : '') . " ?, ?, ?, ?, ?, ?,"; my $past_cols = " COUNT(*), '0'"; diff --git a/lib/Cxn.pm b/lib/Cxn.pm index 5efca626..8b54e726 100644 --- a/lib/Cxn.pm +++ b/lib/Cxn.pm @@ -36,6 +36,7 @@ use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); use Scalar::Util qw(blessed); + use constant { PTDEBUG => $ENV{PTDEBUG} || 0, # Hostnames make testing less accurate. Tests need to see @@ -195,21 +196,6 @@ sub name { return $self->{hostname} || $self->{dsn_name} || 'unknown host'; } -sub is_cluster_node { - my ($self) = @_; - return $self->{is_cluster_node} if defined $self->{is_cluster_node}; - - my $sql = "SHOW VARIABLES LIKE 'wsrep_on'"; - PTDEBUG && _d($sql); - my $row = $self->{dbh}->selectrow_arrayref($sql); - PTDEBUG && _d(defined $row ? @$row : 'undef'); - $self->{is_cluster_node} = $row && $row->[1] - ? ($row->[1] eq 'ON' || $row->[1] eq '1') - : 0; - - return $self->{is_cluster_node}; -} - sub DESTROY { my ($self) = @_; if ( $self->{dbh} diff --git a/lib/Percona/XtraDB/Cluster.pm b/lib/Percona/XtraDB/Cluster.pm new file mode 100644 index 00000000..7b85ed44 --- /dev/null +++ b/lib/Percona/XtraDB/Cluster.pm @@ -0,0 +1,123 @@ +# This program is copyright 2011 Percona Inc. +# Feedback and improvements are welcome. +# +# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, version 2; OR the Perl Artistic License. On UNIX and similar +# systems, you can issue `man perlgpl' or `man perlartistic' to read these +# licenses. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 59 Temple +# Place, Suite 330, Boston, MA 02111-1307 USA. +# ########################################################################### +# Percona::XtraDB::Cluster package +# ########################################################################### +{ +# Package: Percona::XtraDB::Cluster +# Percona::XtraDB::Cluster has helper methods to deal with Percona XtraDB Cluster +# based servers + +package Percona::XtraDB::Cluster; +use Mo; +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +sub is_cluster_node { + my ($self, $cxn) = @_; + return $self->{is_cluster_node}->{$cxn} if defined $self->{is_cluster_node}->{$cxn}; + + my $sql = "SHOW VARIABLES LIKE 'wsrep_on'"; + PTDEBUG && _d($sql); + my $row = $cxn->dbh->selectrow_arrayref($sql); + PTDEBUG && _d(defined $row ? @$row : 'undef'); + $self->{is_cluster_node}->{$cxn} = $row && $row->[1] + ? ($row->[1] eq 'ON' || $row->[1] eq '1') + : 0; + + return $self->{is_cluster_node}->{$cxn}; +} + +sub same_cluster { + my ($self, $cxn1, $cxn2) = @_; + return unless $self->is_cluster_node($cxn1) && $self->is_cluster_node($cxn2); + return if $self->is_master_of($cxn1, $cxn2) || $self->is_master_of($cxn2, $cxn1); + + my $sql = q{SHOW VARIABLES LIKE 'wsrep_cluster_name'}; + PTDEBUG && _d($sql); + my (undef, $row) = $cxn1->dbh->selectrow_array($sql); + my (undef, $cxn2_row) = $cxn2->dbh->selectrow_array($sql); + + return unless $row eq $cxn2_row; + + # Now it becomes tricky. Ostensibly clusters shouldn't have the + # same name, but tell that to the world. + $sql = q{SHOW VARIABLES LIKE 'wsrep_cluster_address'}; + PTDEBUG && _d($sql); + my (undef, $addr) = $cxn1->dbh->selectrow_array($sql); + my (undef, $cxn2_addr) = $cxn2->dbh->selectrow_array($sql); + + # If they both have gcomm://, then they are both the first + # node of a cluster, so they can't be in the same one. + return if $addr eq 'gcomm://' && $cxn2_addr eq 'gcomm://'; + + if ( $addr eq 'gcomm://' ) { + $addr = $self->_find_full_gcomm_addr($cxn1->dbh); + } + elsif ( $cxn2_addr eq 'gcomm://' ) { + $cxn2_addr = $self->_find_full_gcomm_addr($cxn2->dbh); + } + + # Meanwhile, if they have the same address, then + # they are definitely part of the same cluster + return 1 if lc($addr) eq lc($cxn2_addr); + + # However, this still leaves us with the issue that + # the cluster addresses could look like this: + # node1 -> node2, node2 -> node1, + # or + # node1 -> node2 addr, + # node2 -> node3 addr, + # node3 -> node1 addr, + # TODO No clue what to do here + return 1; +} + +sub is_master_of { + my ($self, $cxn1, $cxn2) = @_; + + my $cxn2_dbh = $cxn2->dbh; + my $sql = q{SHOW SLAVE STATUS}; + PTDEBUG && _d($sql); + local $cxn2_dbh->{FetchHashKeyName} = 'NAME_lc'; + my $slave_status = $cxn2_dbh->selectrow_hashref($sql); + return unless ref($slave_status) eq 'HASH'; + + my $port = $cxn1->dsn->{P}; + return unless $slave_status->{master_port} eq $port; + return 1 if $cxn1->dsn->{h} eq $slave_status->{master_host}; + + # They might be the same but in different format + my $host = scalar gethostbyname($cxn1->dsn->{h}); + my $master_host = scalar gethostbyname($slave_status->{master_host}); + return 1 if $master_host eq $host; + return; +} + +sub _find_full_gcomm_addr { + my ($self, $dbh) = @_; + + my $sql = q{SHOW VARIABLES LIKE 'wsrep_provider_options'}; + PTDEBUG && _d($sql); + my (undef, $provider_opts) = $dbh->selectrow_array($sql); + my ($prov_addr) = $provider_opts =~ m{\Qgmcast.listen_addr\E\s*=\s*tcp://([^:]+:[0-9]+)\s*;}i; + my $full_gcomm = "gcomm://$prov_addr"; + PTDEBUG && _d("gcomm address: ", $full_gcomm); + return $full_gcomm; +} + +1; +} diff --git a/lib/Sandbox.pm b/lib/Sandbox.pm index e0764fc6..a2f9a5f0 100644 --- a/lib/Sandbox.pm +++ b/lib/Sandbox.pm @@ -39,6 +39,8 @@ $Data::Dumper::Quotekeys = 0; use constant PTDEBUG => $ENV{PTDEBUG} || 0; use constant PTDEVDEBUG => $ENV{PTDEVDEBUG} || 0; +use IO::Socket::INET; + my $trunk = $ENV{PERCONA_TOOLKIT_BRANCH}; my %port_for = ( @@ -74,7 +76,7 @@ sub use { return if !defined $cmd || !$cmd; my $use = $self->_use_for($server) . " $cmd"; PTDEBUG && _d('"Executing', $use, 'on', $server); - my $out = `$use 2>&1`; + my $out = `$use`; if ( $? >> 8 ) { die "Failed to execute $cmd on $server: $out"; } @@ -130,7 +132,7 @@ sub get_dbh_for { } sub load_file { - my ( $self, $server, $file, $use_db ) = @_; + my ( $self, $server, $file, $use_db, %args ) = @_; _check_server($server); $file = "$trunk/$file"; if ( !-f $file ) { @@ -141,11 +143,11 @@ sub load_file { my $use = $self->_use_for($server) . " $d < $file"; PTDEBUG && _d('Loading', $file, 'on', $server, ':', $use); - my $out = `$use 2>&1`; + my $out = `$use`; if ( $? >> 8 ) { die "Failed to execute $file on $server: $out"; } - $self->wait_for_slaves(); + $self->wait_for_slaves() unless $args{no_wait}; } sub _use_for { @@ -420,6 +422,85 @@ sub can_load_data { return ($output || '') =~ /1/; } +sub set_as_slave { + my ($self, $server, $master_server, @extras) = @_; + PTDEBUG && _d("Setting $server as slave of $master_server"); + my $master_port = $port_for{$master_server}; + my $sql = join ", ", qq{change master to master_host='127.0.0.1'}, + qq{master_user='msandbox'}, + qq{master_password='msandbox'}, + qq{master_port=$master_port}, + @extras; + for my $sql_to_run ($sql, "start slave") { + my $out = $self->use($server, qq{-e "$sql_to_run"}); + PTDEBUG && _d($out); + } +} + +sub start_sandbox { + my ($self, $mode, $server, $master_server) = @_; + my $port = $port_for{$server}; + my $master_port = $master_server ? $port_for{$master_server} : ''; + my $out = `$trunk/sandbox/start-sandbox $mode $port $master_port`; + die $out if $CHILD_ERROR; + return $out; +} + +sub stop_sandbox { + my ($self, @sandboxes) = @_; + my @ports = @port_for{@sandboxes}; + my $out = `$trunk/sandbox/stop-sandbox @ports`; + die $out if $CHILD_ERROR; + return $out; +} + +sub start_cluster { + my ($self, %args) = @_; + my $cluster_size = $args{cluster_size} || 3; + + my $out = ''; + + my ($node1, @nodes) = map { + my $node_name = "node$_"; + $node_name = "_$node_name" while exists $port_for{$node_name}; + $port_for{$node_name} = $self->_get_unused_port(); + $node_name + } 1..$cluster_size; + + local $ENV{CLUSTER_NAME} = $args{cluster_name} if $args{cluster_name}; + $self->start_sandbox("cluster", $node1); + for my $node ( @nodes ) { + $self->start_sandbox("cluster", $node, $node1); + } + + return ($node1, @nodes); +} + +# Lifted from Nginx::Test on CPAN +sub _get_unused_port { + my $port = 50000 + int (rand() * 5000); + + while ($port++ < 64000) { + my $sock = IO::Socket::INET->new ( + Listen => 5, + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'tcp', + ReuseAddr => 1 + ) or next; + + $sock->close; + return $port; + } + + die "Cannot find an open port"; +} + +sub port_for { + my ($self, $server) = @_; + return $port_for{$server}; +} + 1; } # ########################################################################### diff --git a/t/lib/Cxn.t b/t/lib/Cxn.t index 75355b76..42933ae7 100644 --- a/t/lib/Cxn.t +++ b/t/lib/Cxn.t @@ -9,7 +9,8 @@ BEGIN { use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); -use Test::More tests => 19; +use Test::More; +use Data::Dumper; use Sandbox; use OptionParser; @@ -17,8 +18,7 @@ use DSNParser; use Quoter; use PerconaTest; use Cxn; - -use Data::Dumper; +use VersionParser; my $q = new Quoter(); my $dp = new DSNParser(opts=>$dsn_opts); @@ -248,12 +248,10 @@ is_deeply( "Default cxn inherits default connection options" ); -@ARGV = (); -$o->get_opts(); - # ############################################################################# # Done. # ############################################################################# $master_dbh->disconnect() if $master_dbh; ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); +done_testing; exit; diff --git a/t/lib/Percona/XtraDB/Cluster.t b/t/lib/Percona/XtraDB/Cluster.t new file mode 100644 index 00000000..005085fc --- /dev/null +++ b/t/lib/Percona/XtraDB/Cluster.t @@ -0,0 +1,216 @@ +#!/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; +use Data::Dumper; + +# Hostnames make testing less accurate. Tests need to see +# that such-and-such happened on specific slave hosts, but +# the sandbox servers are all on one host so all slaves have +# the same hostname. +$ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} = 1; + +use Sandbox; +use OptionParser; +use DSNParser; +use Quoter; +use PerconaTest; +use Cxn; +use VersionParser; + +use Percona::XtraDB::Cluster; + +my $q = new Quoter(); +my $dp = new DSNParser(opts=>$dsn_opts); +my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp); +my $master_dbh = $sb->get_dbh_for('master'); + +my $cluster = Percona::XtraDB::Cluster->new(); + +if ( !$master_dbh ) { + plan skip_all => 'Cannot connect to sandbox master'; +} + +my $o = new OptionParser(description => 'Cxn'); +$o->get_specs("$trunk/bin/pt-table-checksum"); +$o->get_opts(); +$dp->prop('set-vars', $o->get('set-vars')); + +sub make_cxn { + my (%args) = @_; + $o->get_opts(); + return new Cxn( + OptionParser => $o, + DSNParser => $dp, + %args, + ); +} + +local @ARGV = (); +$o->get_opts(); + +diag("Starting master1"); +$sb->start_sandbox("master", "master1"); + +my ($master_cxn, $slave1_cxn, $master1_cxn) + = map { + my $cxn = make_cxn( dsn_string => $sb->dsn_for($_) ); + $cxn->connect(); + $cxn; + } qw( master slave1 master1 ); + +for my $cxn ( $master_cxn, $slave1_cxn, $master1_cxn ) { + ok( + !$cluster->is_cluster_node($cxn), + "is_cluster_node works correctly for non-nodes " . $cxn->name + ); +} + +ok($cluster->is_master_of($master_cxn, $slave1_cxn), "is_master_of(master, slave1) is true"); +ok(!$cluster->is_master_of($slave1_cxn, $master_cxn), "is_master_of(slave1, master) is false"); + +my $db_flavor = VersionParser->new($master_dbh)->flavor(); +SKIP: { + skip "PXC-only test", 17 + unless $db_flavor =~ /XtraDB Cluster/; + + diag("Starting a 1-node PXC"); + my ($node) = $sb->start_cluster(cluster_size => 1); + + my $cxn1 = make_cxn( dsn_string => $sb->dsn_for($node) ); + $cxn1->connect(); + ok( + $cluster->is_cluster_node($cxn1), + "is_cluster_node works correctly for cluster nodes" + ); + + ok( + !$cluster->is_master_of($master1_cxn, $cxn1), + "->is_master_of works correctly for a server unrelated to a cluster" + ); + + diag("Setting node as a slave of master1"); + $sb->set_as_slave($node, "master1"); + ok( + $cluster->is_master_of($master1_cxn, $cxn1), + "->is_master_of works correctly for master -> cluster" + ); + ok( + !$cluster->is_master_of($cxn1, $master1_cxn), + "...and the inverse returns the expected result" + ); + ok( + !$cluster->same_cluster($master1_cxn, $cxn1), + "->same_cluster works for master -> cluster" + ); + diag("Restarting the cluster"); + diag($sb->stop_sandbox($node)); + ($node) = $sb->start_cluster(cluster_size => 1); + $cxn1 = make_cxn( dsn_string => $sb->dsn_for($node) ); + $cxn1->connect(); + + diag("Setting master1 as a slave of the node"); + $sb->set_as_slave("master1", $node); + ok( + $cluster->is_master_of($cxn1, $master1_cxn), + "->is_master_of works correctly for cluster -> master" + ); + ok( + !$cluster->is_master_of($master1_cxn, $cxn1), + "...and the inverse returns the expected result" + ); + + ok( + !$cluster->same_cluster($cxn1, $master1_cxn), + "->same_cluster works for cluster -> master" + ); + + diag("Starting a 2-node PXC"); + my ($node2, $node3) = $sb->start_cluster(cluster_size => 2); + + my $cxn2 = make_cxn( dsn_string => $sb->dsn_for($node2) ); + $cxn2->connect(); + my $cxn3 = make_cxn( dsn_string => $sb->dsn_for($node3) ); + $cxn3->connect(); + ok( + $cluster->is_cluster_node($cxn2), + "is_cluster_node correctly finds that this node is part of a cluster" + ); + + ok( + !$cluster->same_cluster($cxn1, $cxn2), + "and same_cluster correctly finds that they don't belong to the same cluster, even when they have the same cluster name" + ); + + ok( + $cluster->same_cluster($cxn2, $cxn3), + "...but does find that they are in the same cluster, even if one is node1" + ); + + TODO: { + local $::TODO = "Should detected that (cluster1.node1) (cluster2.node2) come from different clusters, but doesn't"; + ok( + !$cluster->same_cluster($cxn1, $cxn3), + "...same_cluster works correctly when they have the same cluster names" + ); + } + + diag("Making the second cluster a slave of the first"); + $sb->set_as_slave($node2, $node); + ok($cluster->is_master_of($cxn1, $cxn2), "is_master_of(cluster1, cluster2) works"); + + ok( + !$cluster->same_cluster($cxn1, $cxn2), + "...same_cluster works correctly when they are cluster1.node1.master -> cluster2.node1.slave" + ); + + diag($sb->stop_sandbox($node2, $node3)); + diag("Starting a 3-node cluster"); + my $node4; + ($node2, $node3, $node4) + = $sb->start_cluster( + cluster_size => 3, + cluster_name => "pt_cxn_test", + ); + $cxn2 = make_cxn( dsn_string => $sb->dsn_for($node2) ); + $cxn2->connect(); + $cxn3 = make_cxn( dsn_string => $sb->dsn_for($node3) ); + $cxn3->connect(); + my $cxn4 = make_cxn( dsn_string => $sb->dsn_for($node4) ); + $cxn4->connect(); + + ok( + !$cluster->same_cluster($cxn1, $cxn2), + "...same_cluster works correctly when they have different cluster names & the are both gcomm" + ); + + ok( + !$cluster->same_cluster($cxn1, $cxn3), + "same_cluster detects that (cluster1.node1) (cluster2.node2) come from different clusters if they have different cluster_names" + ); + + ok( + $cluster->same_cluster($cxn2, $cxn3), + "sanity check: but still finds that nodes in the same cluster belong together" + ); + + diag($sb->stop_sandbox($node, $node2, $node3, $node4)); +} + +diag($sb->stop_sandbox("master1")); + +# ############################################################################# +# Done. +# ############################################################################# +$master_dbh->disconnect() if $master_dbh; +ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); +done_testing; +exit; diff --git a/t/pt-table-checksum/pxc.t b/t/pt-table-checksum/pxc.t new file mode 100644 index 00000000..50dad28a --- /dev/null +++ b/t/pt-table-checksum/pxc.t @@ -0,0 +1,242 @@ +#!/usr/bin/env 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; +use Data::Dumper; +use File::Spec::Functions; + +# Hostnames make testing less accurate. Tests need to see +# that such-and-such happened on specific slave hosts, but +# the sandbox servers are all on one host so all slaves have +# the same hostname. +$ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} = 1; + +use PerconaTest; +use Sandbox; + +require "$trunk/bin/pt-table-checksum"; +# Do this after requiring ptc, since it uses Mo +require VersionParser; + +my $dp = new DSNParser(opts=>$dsn_opts); +my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp); +my $master_dbh = $sb->get_dbh_for('master'); + +my $db_flavor = VersionParser->new($master_dbh)->flavor(); + +if ( !$master_dbh ) { + plan skip_all => 'Cannot connect to sandbox master'; +} +elsif ( $db_flavor !~ /XtraDB Cluster/ ) { + plan skip_all => "PXC tests"; +} + +# The sandbox servers run with lock_wait_timeout=3 and it's not dynamic +# so we need to specify --lock-wait-timeout=3 else the tool will die. +my $master_dsn = 'h=127.1,P=12345,u=msandbox,p=msandbox'; +my @args = ($master_dsn, qw(--lock-wait-timeout 3)); +my $output; +my $exit_status; +my $sample = "t/pt-table-checksum/samples/"; + +# ############################################################################# +# pt-table-checksum v2.1.4 doesn't detect diffs on Percona XtraDB Cluster nodes +# https://bugs.launchpad.net/percona-toolkit/+bug/1062563 +# ############################################################################# + +sub make_dbh_differ { + my ($dbh, @vals) = @_; + @vals = (@vals ? @vals : 1); + # Make them differ... + $dbh->do("DROP DATABASE IF EXISTS bug_1062563"); + $dbh->do("CREATE DATABASE bug_1062563"); + $dbh->do("CREATE TABLE bug_1062563.ptc_pxc (i int)"); + + # Now make this node different from the rest + $dbh->do("set sql_log_bin=0"); + $dbh->do("INSERT INTO bug_1062563.ptc_pxc (i) VALUES ($_)") for @vals; + $dbh->do("set sql_log_bin=1"); +} + +diag("Creating a 5-node PXC cluster..."); +my @nodes = $sb->start_cluster(cluster_size => 5); +diag("Nodes: ", Dumper( { map { $_ => $sb->port_for($_) } @nodes } )); + +my $node2 = $nodes[1]; +my $node2_dbh = $sb->get_dbh_for($node2); + +my $node2_slave = "master3"; + +diag("Creating a slave for $node2..."); +{ + local $ENV{BINLOG_FORMAT} = 'ROW'; + diag($sb->start_sandbox("slave", $node2_slave, $node2)); +} +my $node_slave_dbh = $sb->get_dbh_for($node2_slave); + +make_dbh_differ($node2_dbh); + +# And make its slave differ as well +PerconaTest::wait_for_table($sb->get_dbh_for($nodes[-1]), "bug_1062563.ptc_pxc"); +PerconaTest::wait_for_table($node_slave_dbh, "bug_1062563.ptc_pxc"); +$node_slave_dbh->do("INSERT INTO bug_1062563.ptc_pxc (i) VALUES ($_)") for 3, 4; + +my $dsns_table_sql = catfile(qw(t lib samples MasterSlave dsn_table.sql)); +$sb->load_file($node2, $dsns_table_sql, undef, no_wait => 1); +$node2_dbh->do("DELETE FROM dsn_t.dsns"); # Delete 12346 +my $sth = $node2_dbh->prepare("INSERT INTO dsn_t.dsns VALUES (null, null, ?)"); +for my $dsn ( map { $sb->dsn_for($_) } @nodes[0,2..$#nodes], $node2_slave ) { + $sth->execute($dsn); +} + +my $node2_dsn = $sb->dsn_for($node2); +$output = output( + sub { pt_table_checksum::main( + $node2_dsn, qw(--lock-wait-timeout 3), + qw(-d bug_1062563), + '--recursion-method', "dsn=D=dsn_t,t=dsns" + ) }, + stderr => 1, +); + +is( + PerconaTest::count_checksum_results($output, 'diffs'), + 1, + "Bug 1062563: Detects diffs between PXC nodes" +) or diag($output); + +my @cluster_nodes = $output =~ /(because it is a cluster node)/g; +is( + scalar(@cluster_nodes), + 4, + "Skips all the cluster nodes in the dsns table" +) or diag($output); + +# Now try with just the slave + +$node2_dbh->do("DELETE FROM dsn_t.dsns"); +$sth->execute($sb->dsn_for($node2_slave)); + +$output = output( + sub { pt_table_checksum::main( + $node2_dsn, qw(--lock-wait-timeout 3), + qw(--chunk-size 1), + qw(-d bug_1062563), + '--recursion-method', "dsn=D=dsn_t,t=dsns" + ) }, + stderr => 1, +); + +is( + PerconaTest::count_checksum_results($output, 'diffs'), + 1, + "Bug 1062563: Detects diffs on slaves where the master is a PXC node" +) or diag($output); + +$sth->finish(); +diag("Stopping the PXC cluster and the slave..."); +$sb->stop_sandbox($node2_slave, @nodes); + +# Now checking that cluster -> cluster works + +diag("Creating two 3-node clusters..."); +my @cluster1 = $sb->start_cluster(cluster_size => 3, cluster_name => "pt_test_cluster_1"); +my @cluster2 = $sb->start_cluster(cluster_size => 3, cluster_name => "pt_test_cluster_2"); +diag("Cluster 1: ", Dumper( { map { $_ => $sb->port_for($_) } @cluster1 } )); +diag("Cluster 2: ", Dumper( { map { $_ => $sb->port_for($_) } @cluster2 } )); + +$sb->set_as_slave($cluster2[0], $cluster1[0]); + +my $cluster1_dbh = $sb->get_dbh_for($cluster1[0]); +my $cluster2_dbh = $sb->get_dbh_for($cluster2[0]); +make_dbh_differ($cluster1_dbh); + +# And make its slave differ as well +PerconaTest::wait_for_table($sb->get_dbh_for($cluster2[-1]), "bug_1062563.ptc_pxc"); +PerconaTest::wait_for_table($sb->get_dbh_for($cluster1[-1]), "bug_1062563.ptc_pxc"); +PerconaTest::wait_for_table($cluster2_dbh, "bug_1062563.ptc_pxc"); +$cluster2_dbh->do("INSERT INTO bug_1062563.ptc_pxc (i) VALUES ($_)") for 3, 4; + +$dsns_table_sql = catfile(qw(t lib samples MasterSlave dsn_table.sql)); +$sb->load_file($cluster1[0], $dsns_table_sql, undef, no_wait => 1); +$cluster1_dbh->do("DELETE FROM dsn_t.dsns"); # Delete 12346 +$sth = $cluster1_dbh->prepare("INSERT INTO dsn_t.dsns VALUES (null, null, ?)"); +for my $dsn ( map { $sb->dsn_for($_) } @cluster1[1..$#cluster1], $cluster2[0] ) { + $sth->execute($dsn); +} +$sth->finish(); + +my $cluster1_dsn = $sb->dsn_for($cluster1[0]); +$output = output( + sub { pt_table_checksum::main( + $cluster1_dsn, qw(--lock-wait-timeout 3), + qw(-d bug_1062563), + '--recursion-method', "dsn=D=dsn_t,t=dsns" + ) }, + stderr => 1, +); + +is( + PerconaTest::count_checksum_results($output, 'diffs'), + 1, + "Bug 1062563: Detects diffs between PXC nodes when cluster -> cluster" +) or diag($output); + +like( + $output, + qr/is a cluster node, but doesn't belong to the same cluster as/, #' + "Shows a warning when cluster -> cluster" +) or diag($output); + +diag("Starting master1..."); +$sb->start_sandbox("master", "master1"); +diag("Setting it as master of a node in the first cluster"); +$sb->set_as_slave($cluster1[0], "master1"); + +my $master1_dbh = $sb->get_dbh_for("master1"); +make_dbh_differ($master1_dbh, 10..50); + +my $master1_dsn = $sb->dsn_for("master1"); +$output = output( + sub { pt_table_checksum::main( + $master1_dsn, qw(--lock-wait-timeout 3), + qw(-d bug_1062563), + ) }, + stderr => 1, +); + +is( + PerconaTest::count_checksum_results($output, 'diffs'), + 1, + "Bug 1062563: Detects diffs when master -> cluster" +) or diag($output); + +is( + PerconaTest::count_checksum_results($output, 'rows'), + 41, + "Bug 1062563: Correct number of rows for master -> cluster" +) or diag($output); + +like( + $output, + qr/is a cluster node, but .*? is not. This is not currently supported/, + "Shows a warning when master -> cluster" +) or diag($output); + +diag("Stopping both clusters and master1..."); +$sb->stop_sandbox(@cluster1, @cluster2, "master1"); + +# ############################################################################# +# Done. +# ############################################################################# +$sb->wipe_clean($master_dbh); +ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); +done_testing;