diff --git a/bin/pt-archiver b/bin/pt-archiver index e9ee9378..2431362a 100755 --- a/bin/pt-archiver +++ b/bin/pt-archiver @@ -6958,10 +6958,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process ID of -the daemonized instance. The PID file is removed when the daemonized instance -exits. The program checks for the existence of the PID file when starting; if -it exists and the process with the matching PID exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --plugin diff --git a/bin/pt-config-diff b/bin/pt-config-diff index e2ea45a3..7b219586 100755 --- a/bin/pt-config-diff +++ b/bin/pt-config-diff @@ -2138,23 +2138,24 @@ sub new { } my $self = { - dsn => $dsn, - dbh => $args{dbh}, - dsn_name => $dp->as_string($dsn, [qw(h P S)]), - hostname => '', - set => $args{set}, - NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, - dbh_set => 0, - OptionParser => $o, - DSNParser => $dp, + dsn => $dsn, + dbh => $args{dbh}, + dsn_name => $dp->as_string($dsn, [qw(h P S)]), + hostname => '', + set => $args{set}, + NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, + dbh_set => 0, + OptionParser => $o, + DSNParser => $dp, is_cluster_node => undef, + parent => $args{parent}, }; return bless $self, $class; } sub connect { - my ( $self ) = @_; + my ( $self, %opts ) = @_; my $dsn = $self->{dsn}; my $dp = $self->{DSNParser}; my $o = $self->{OptionParser}; @@ -2165,11 +2166,18 @@ sub connect { $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); $self->{asked_for_pass} = 1; } - $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit => 1 }); + $dbh = $dp->get_dbh( + $dp->get_cxn_params($dsn), + { + AutoCommit => 1, + %opts, + }, + ); } - PTDEBUG && _d($dbh, 'Connected dbh to', $self->{name}); - return $self->set_dbh($dbh); + $dbh = $self->set_dbh($dbh); + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name}); + return $dbh; } sub set_dbh { @@ -2192,6 +2200,11 @@ sub set_dbh { $self->{hostname} = $hostname; } + if ( $self->{parent} ) { + PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent'); + $dbh->{InactiveDestroy} = 1; + } + if ( my $set = $self->{set}) { $set->($dbh); } @@ -2201,6 +2214,13 @@ sub set_dbh { return $dbh; } +sub lost_connection { + my ($self, $e) = @_; + return 0 unless $e; + return $e =~ m/MySQL server has gone away/ + || $e =~ m/Lost connection to MySQL server/; +} + sub dbh { my ($self) = @_; return $self->{dbh}; @@ -2219,12 +2239,21 @@ sub name { sub DESTROY { my ($self) = @_; - if ( $self->{dbh} - && blessed($self->{dbh}) - && $self->{dbh}->can("disconnect") ) { - PTDEBUG && _d('Disconnecting dbh', $self->{dbh}, $self->{name}); + + PTDEBUG && _d('Destroying cxn'); + + if ( $self->{parent} ) { + PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent'); + } + elsif ( $self->{dbh} + && blessed($self->{dbh}) + && $self->{dbh}->can("disconnect") ) + { + PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname}, + $self->{dsn_name}); $self->{dbh}->disconnect(); } + return; } @@ -5022,12 +5051,7 @@ sub main { # Daemonize now that everything is setup and ready to work. # ######################################################################## my $daemon; - if ( $o->get('daemonize') ) { - $daemon = new Daemon(o=>$o); - $daemon->daemonize(); - PTDEBUG && _d('I am a daemon now'); - } - elsif ( $o->get('pid') ) { + if ( $o->get('pid') ) { # We're not daemoninzing, it just handles PID stuff. $daemon = new Daemon(o=>$o); $daemon->make_PID_file(); @@ -5118,7 +5142,7 @@ pt-config-diff - Diff MySQL configuration files and server variables. =head1 SYNOPSIS -Usage: pt-config-diff [OPTION...] CONFIG CONFIG [CONFIG...] +Usage: pt-config-diff [OPTIONS] CONFIG CONFIG [CONFIG...] pt-config-diff diffs MySQL configuration files and server variables. CONFIG can be a filename or a DSN. At least two CONFIG sources must be given. @@ -5238,11 +5262,6 @@ Read this comma-separated list of config files; if specified, this must be the first option on the command line. (This option does not specify a CONFIG; it's equivalent to C<--defaults-file>.) -=item --daemonize - -Fork to the background and detach from the shell. POSIX -operating systems only. - =item --defaults-file short form: -F; type: string @@ -5276,11 +5295,11 @@ Password to use for connection. type: string -Create the given PID file when daemonized. The file contains the process -ID of the daemonized instance. The PID file is removed when the -daemonized instance exits. The program checks for the existence of the -PID file when starting; if it exists and the process with the matching PID -exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-deadlock-logger b/bin/pt-deadlock-logger index 2768ac87..ebc8ab1a 100755 --- a/bin/pt-deadlock-logger +++ b/bin/pt-deadlock-logger @@ -23,9 +23,11 @@ BEGIN { VersionParser Quoter DSNParser + Cxn Daemon HTTPMicro VersionCheck + Runtime )); } @@ -2438,6 +2440,181 @@ sub _d { # End DSNParser package # ########################################################################### +# ########################################################################### +# Cxn 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/Cxn.pm +# t/lib/Cxn.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Cxn; + +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, +}; + +sub new { + my ( $class, %args ) = @_; + my @required_args = qw(DSNParser OptionParser); + foreach my $arg ( @required_args ) { + die "I need a $arg argument" unless $args{$arg}; + }; + my ($dp, $o) = @args{@required_args}; + + my $dsn_defaults = $dp->parse_options($o); + my $prev_dsn = $args{prev_dsn}; + my $dsn = $args{dsn}; + if ( !$dsn ) { + $args{dsn_string} ||= 'h=' . ($dsn_defaults->{h} || 'localhost'); + + $dsn = $dp->parse( + $args{dsn_string}, $prev_dsn, $dsn_defaults); + } + elsif ( $prev_dsn ) { + $dsn = $dp->copy($prev_dsn, $dsn); + } + + my $self = { + dsn => $dsn, + dbh => $args{dbh}, + dsn_name => $dp->as_string($dsn, [qw(h P S)]), + hostname => '', + set => $args{set}, + NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, + dbh_set => 0, + OptionParser => $o, + DSNParser => $dp, + is_cluster_node => undef, + parent => $args{parent}, + }; + + return bless $self, $class; +} + +sub connect { + my ( $self, %opts ) = @_; + my $dsn = $self->{dsn}; + my $dp = $self->{DSNParser}; + my $o = $self->{OptionParser}; + + my $dbh = $self->{dbh}; + if ( !$dbh || !$dbh->ping() ) { + if ( $o->get('ask-pass') && !$self->{asked_for_pass} ) { + $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); + $self->{asked_for_pass} = 1; + } + $dbh = $dp->get_dbh( + $dp->get_cxn_params($dsn), + { + AutoCommit => 1, + %opts, + }, + ); + } + + $dbh = $self->set_dbh($dbh); + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name}); + return $dbh; +} + +sub set_dbh { + my ($self, $dbh) = @_; + + if ( $self->{dbh} && $self->{dbh} == $dbh && $self->{dbh_set} ) { + PTDEBUG && _d($dbh, 'Already set dbh'); + return $dbh; + } + + PTDEBUG && _d($dbh, 'Setting dbh'); + + $dbh->{FetchHashKeyName} = 'NAME_lc' if $self->{NAME_lc}; + + my $sql = 'SELECT @@hostname, @@server_id'; + PTDEBUG && _d($dbh, $sql); + my ($hostname, $server_id) = $dbh->selectrow_array($sql); + PTDEBUG && _d($dbh, 'hostname:', $hostname, $server_id); + if ( $hostname ) { + $self->{hostname} = $hostname; + } + + if ( $self->{parent} ) { + PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent'); + $dbh->{InactiveDestroy} = 1; + } + + if ( my $set = $self->{set}) { + $set->($dbh); + } + + $self->{dbh} = $dbh; + $self->{dbh_set} = 1; + return $dbh; +} + +sub lost_connection { + my ($self, $e) = @_; + return 0 unless $e; + return $e =~ m/MySQL server has gone away/ + || $e =~ m/Lost connection to MySQL server/; +} + +sub dbh { + my ($self) = @_; + return $self->{dbh}; +} + +sub dsn { + my ($self) = @_; + return $self->{dsn}; +} + +sub name { + my ($self) = @_; + return $self->{dsn_name} if PERCONA_TOOLKIT_TEST_USE_DSN_NAMES; + return $self->{hostname} || $self->{dsn_name} || 'unknown host'; +} + +sub DESTROY { + my ($self) = @_; + + PTDEBUG && _d('Destroying cxn'); + + if ( $self->{parent} ) { + PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent'); + } + elsif ( $self->{dbh} + && blessed($self->{dbh}) + && $self->{dbh}->can("disconnect") ) + { + PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname}, + $self->{dsn_name}); + $self->{dbh}->disconnect(); + } + + return; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Cxn package +# ########################################################################### + # ########################################################################### # Daemon package # This package is a copy without comments from the original. The original @@ -3864,6 +4041,139 @@ sub _d { # End VersionCheck package # ########################################################################### +# ########################################################################### +# Runtime 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/Runtime.pm +# t/lib/Runtime.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Runtime; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +sub new { + my ( $class, %args ) = @_; + my @required_args = qw(run_time now); + foreach my $arg ( @required_args ) { + die "I need a $arg argument" unless exists $args{$arg}; + } + + my $run_time = $args{run_time}; + if ( defined $run_time ) { + die "run_time must be > 0" if $run_time <= 0; + } + + my $now = $args{now}; + die "now must be a callback" unless ref $now eq 'CODE'; + + my $self = { + run_time => $run_time, + now => $now, + start_time => undef, + end_time => undef, + time_left => undef, + stop => 0, + }; + + return bless $self, $class; +} + +sub time_left { + my ( $self, %args ) = @_; + + if ( $self->{stop} ) { + PTDEBUG && _d("No time left because stop was called"); + return 0; + } + + my $now = $self->{now}->(%args); + PTDEBUG && _d("Current time:", $now); + + if ( !defined $self->{start_time} ) { + $self->{start_time} = $now; + } + + return unless defined $now; + + my $run_time = $self->{run_time}; + return unless defined $run_time; + + if ( !$self->{end_time} ) { + $self->{end_time} = $now + $run_time; + PTDEBUG && _d("End time:", $self->{end_time}); + } + + $self->{time_left} = $self->{end_time} - $now; + PTDEBUG && _d("Time left:", $self->{time_left}); + return $self->{time_left}; +} + +sub have_time { + my ( $self, %args ) = @_; + my $time_left = $self->time_left(%args); + return 1 if !defined $time_left; # run forever + return $time_left <= 0 ? 0 : 1; # <=0s means run time has elapsed +} + +sub time_elapsed { + my ( $self, %args ) = @_; + + my $start_time = $self->{start_time}; + return 0 unless $start_time; + + my $now = $self->{now}->(%args); + PTDEBUG && _d("Current time:", $now); + + my $time_elapsed = $now - $start_time; + PTDEBUG && _d("Time elapsed:", $time_elapsed); + if ( $time_elapsed < 0 ) { + warn "Current time $now is earlier than start time $start_time"; + } + return $time_elapsed; +} + +sub reset { + my ( $self ) = @_; + $self->{start_time} = undef; + $self->{end_time} = undef; + $self->{time_left} = undef; + $self->{stop} = 0; + PTDEBUG && _d("Reset run time"); + return; +} + +sub stop { + my ( $self ) = @_; + $self->{stop} = 1; + return; +} + +sub start { + my ( $self ) = @_; + $self->{stop} = 0; + return; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Runtime package +# ########################################################################### + # ########################################################################### # This is a combination of modules and programs in one -- a runnable module. # http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last @@ -3877,21 +4187,15 @@ package pt_deadlock_logger; use English qw(-no_match_vars); use List::Util qw(max); use Socket qw(inet_aton); -use sigtrap qw(handler finish untrapped normal-signals); +use Time::HiRes qw(sleep); +use File::Temp qw(tempfile); +use File::Spec; + +use sigtrap 'handler', \&sig_int, 'normal-signals'; use Percona::Toolkit; use constant PTDEBUG => $ENV{PTDEBUG} || 0; -my $o; -my $oktorun; -my $dp; - -# ######################################################################## -# Configuration info. -# ######################################################################## -my $source_dsn; -my $dest_dsn; - # Some common patterns and variables my $d = qr/(\d+)/; # Digit my $t = qr/((?:\d+ \d+)|(?:[A-Fa-f0-9]+))/; # Transaction ID @@ -3934,48 +4238,54 @@ my %is_proc_info = ( 'update' => 1, ); -sub main { - local @ARGV = @_; # set global ARGV for this package +my $oktorun = 1; +my $exit_status = 0; - my $q = new Quoter(); +sub main { + local @ARGV = @_; # set global ARGV for this package + $oktorun = 1; + $exit_status = 0; # ######################################################################## # Get configuration information. # ######################################################################## - $o = new OptionParser(); + my $o = new OptionParser(); $o->get_specs(); $o->get_opts(); - $o->set('collapse', $o->get('print')) unless $o->got('collapse'); + my $dp = $o->DSNParser(); + $dp->prop('set-vars', $o->get('set-vars')); - $dp = $o->DSNParser(); - my $dsn_defaults = $dp->parse_options($o); - $source_dsn = @ARGV ? $dp->parse(shift @ARGV,$dsn_defaults) : $dsn_defaults; - $dest_dsn = $o->get('dest'); + my $src; + if ( my $src_dsn_string = shift @ARGV ) { + $src = Cxn->new( + dsn_string => $src_dsn_string, + parent => $o->get('daemonize'), + DSNParser => $dp, + OptionParser => $o, + ); + } - # The source dsn is not an option so --dest cannot use OptionParser - # to inherit values from it. Thus, we do it manually. --dest will - # inherit from --user, --port, etc. - if ( $source_dsn && $dest_dsn ) { - # If dest DSN only has D and t, this will copy h, P, S, etc. - # from the source DSN. - $dest_dsn = $dp->copy($source_dsn, $dest_dsn); + my $dst; + if ( my $dst_dsn = $o->get('dest') ) { + $dst = Cxn->new( + dsn => $dst_dsn, + prev_dsn => ($src ? $src->dsn : undef), + parent => $o->get('daemonize'), + DSNParser => $dp, + OptionParser => $o, + ); } if ( !$o->get('help') ) { - if ( !$source_dsn ) { - $o->save_error('Missing or invalid source host'); + if ( !$src ) { + $o->save_error('No DSN was specified.'); } - if ( $dest_dsn && !$dest_dsn->{D} ) { - $o->save_error("--dest requires a 'D' (database) part"); + if ( $dst && !$dst->dsn->{D} ) { + $o->save_error("--dest requires a 'D' (database) part."); } - if ( $dest_dsn && !$dest_dsn->{t} ) { - $o->save_error("--dest requires a 't' (table) part"); - } - - # Avoid running forever with zero second interval. - if ( $o->get('run-time') && !$o->get('interval') ) { - $o->set('interval', 1); + if ( $dst && !$dst->dsn->{t} ) { + $o->save_error("--dest requires a 't' (table) part."); } } @@ -3984,43 +4294,29 @@ sub main { # ######################################################################## # Connect to MySQL and set up the --dest, if any. # ######################################################################## - my $dbh = get_cxn($source_dsn, 1); - my $dest_dbh; - my $sth; + my $q = new Quoter(); + + $src->connect(); + + my @cols = @{ $o->get('columns') }; my $ins_sth; + my $ins_sql; + if ( $dst ) { + $dst->connect(AutoCommit => 0); - # Since the user might not have specified a hostname for the connection, - # try to extract it from the $dbh - if ( !$source_dsn->{h} ) { - ($source_dsn->{h}) = $dbh->{mysql_hostinfo} =~ m/(\w+) via/; - PTDEBUG && _d('Got source host from dbh:', $source_dsn->{h}); - } - - my @cols = qw( server ts thread txn_id txn_time user hostname ip db tbl idx - lock_type lock_mode wait_hold victim query ); - if ( $o->got('columns') ) { - @cols = grep { $o->get('columns')->{$_} } @cols; - } - - if ( $dest_dsn ) { - my $db_tbl = - join('.', - map { $q->quote($_) } - grep { $_ } - ( $dest_dsn->{D}, $dest_dsn->{t} )); - $dest_dbh = get_cxn($dest_dsn, 0); + my $db_tbl = $q->join_quote($dst->dsn->{D}, $dst->dsn->{t}); my $cols = join(',', map { $q->quote($_) } @cols); - my $parms = join(',', map { '?' } @cols); - my $sql = "INSERT IGNORE INTO $db_tbl($cols) VALUES($parms)"; - PTDEBUG && _d($sql); - $ins_sth = $dest_dbh->prepare($sql); + my $parms = join(',', map { '?' } @cols); + $ins_sql = "INSERT IGNORE INTO $db_tbl ($cols) VALUES ($parms) " + . "/* pt-deadlock-logger */"; + PTDEBUG && _d($ins_sql); + $ins_sth = $dst->dbh->prepare($ins_sql); if ( $o->get('create-dest-table') ) { - my $db_tbl = $q->quote($dest_dsn->{D}, $dest_dsn->{t}); - $sql = $o->read_para_after(__FILE__, qr/MAGIC_dest_table/); - $sql =~ s/deadlocks/IF NOT EXISTS $db_tbl/; + my $sql = $o->read_para_after(__FILE__, qr/MAGIC_dest_table/); + $sql =~ s/deadlocks/IF NOT EXISTS $db_tbl/; PTDEBUG && _d($sql); - $dest_dbh->do($sql); + $dst->dbh->do($sql); } } @@ -4039,6 +4335,16 @@ sub main { $daemon->make_PID_file(); } + # If we daemonized, the parent has already exited and we're the child. + # We shared a copy of every Cxn with the parent, and the parent's copies + # were destroyed but the dbhs were not disconnected because the parent + # attrib was true. Now, as the child, set it false so the dbhs will be + # disconnected when our Cxn copies are destroyed. If we didn't daemonize, + # then we're not really a parent (since we have no children), so set it + # false to auto-disconnect the dbhs when our Cxns are destroyed. + $src->{parent} = 0; + $dst->{parent} = 0 if $dst; + # ######################################################################## # Do the version-check # ######################################################################## @@ -4046,117 +4352,155 @@ sub main { VersionCheck::version_check( force => $o->got('version-check'), instances => [ - { dbh => $dbh, dsn => $source_dsn }, - ($dest_dsn ? { dbh => $dest_dsn, dsn => $dest_dsn } : ()), + { dbh => $src->dbh, dsn => $src->dsn }, + ($dst ? { dbh => $dst->dbh, dsn => $dst->dsn } : ()) ], ); } + # ######################################################################## + # Set upt the --clear-deadlocks table. + # ######################################################################## + my $clear_deadlocks_table_def; + my $clear_deadlocks_table = $o->get('clear-deadlocks'); + if ( $clear_deadlocks_table ) { + $clear_deadlocks_table_def + = $o->read_para_after(__FILE__, qr/MAGIC_clear_deadlocks/); + if ( VersionParser->new($src->dbh) < '4.1.2') { + $clear_deadlocks_table_def =~ s/ENGINE=/TYPE=/; + } + $clear_deadlocks_table_def + =~ s/percona_schema.clear_deadlocks/$clear_deadlocks_table/; + PTDEBUG && _d('--clear-deadlocks table:', $clear_deadlocks_table_def); + } + # ######################################################################## # Start looking for and logging deadlocks. # ######################################################################## - my $last_fingerprint = ''; + my $sep = $o->get('tab') ? "\t" : ' '; + my $last_fingerprint = ''; + my $parse_deadlocks_options = { + 'server' => $src->dsn->{h} || $src->{hostname}, + 'numeric-ip' => $o->got('numeric-ip'), + }; - $oktorun = 1; - my $start = time(); - my $end = $start + ($o->get('run-time') || 0); # When we should exit - my $now = $start; - while ( # Quit if: - ($start == $end || $now < $end) # time is exceeded - && $oktorun # or instructed to quit - ) - { - my $text = $dbh->selectrow_hashref("SHOW /*!40100 ENGINE*/ INNODB STATUS")->{Status}; - my $parse_deadlocks_options = { - 'numeric-ip' => $o->got('numeric-ip'), - 'collapse' => $o->got('collapse'), + my $run_time = Runtime->new( + run_time => $o->get('run-time'), + now => sub { return time }, + ); + + my $interval = $o->get('interval'); + my $iters = $o->get('iterations'); + PTDEBUG && _d('iterations:', $iters, 'interval:', $interval); + + ITERATION: + while ( + $oktorun + && $run_time->have_time() + && (!defined $iters || $iters--) + ) { + + my %txns; + my $fingerprint; + eval { + my $sql = "SHOW /*!40100 ENGINE*/ INNODB STATUS " + . "/* pt-deadlock-logger */"; + my $text = $src->dbh->selectrow_hashref($sql)->{status}; + + %txns = %{parse_deadlocks($text, $parse_deadlocks_options)}; + $fingerprint = fingerprint(\%txns); }; - my %txns = %{parse_deadlocks($text, $parse_deadlocks_options)}; - my $fingerprint = fingerprint(\%txns); - - if ( $ins_sth ) { - foreach my $txn (sort { $a->{thread} <=> $b->{thread} } values %txns) { - $ins_sth->execute(@{$txn}{@cols}); - } - $dest_dbh->commit(); - } - - if ( $fingerprint ne $last_fingerprint ) { - PTDEBUG && _d('New deadlock'); - if ( $o->got('print') || !$o->got('dest') ) { - my $sep = $o->get('tab') ? "\t" : ' '; - print join($sep, @cols), "\n"; - foreach my $txn (sort {$a->{thread}<=>$b->{thread}} values %txns) { - # If 'collapse' is on, it's already been taken care of, - # but if it's unset, by default strip whitespace. - if ( !$o->got('collapse') ) { - $txn->{query} =~ s/\s+/ /g; - } - print join($sep, map { $txn->{$_} } @cols), "\n"; + if ( my $e = $EVAL_ERROR ) { + PTDEBUG && _d('Error getting InnoDB status:', $e); + if ( $src->lost_connection($e) ) { + eval { $src->connect() }; + if ( $EVAL_ERROR ) { + warn "Lost connection to " . $src->name . ". Will try " + . "to reconnect in the next iteration.\n"; + } + else { + PTDEBUG && _d('Reconnected to MySQL'); + redo ITERATION; } } + else { + warn "Error getting SHOW ENGINE INNODB STATUS: $EVAL_ERROR"; + $exit_status |= 1; + } } else { - PTDEBUG && _d('Same deadlock, not printing'); - } - # Save deadlock's fingerprint for next interval. - $last_fingerprint = $fingerprint; - - # If specified, clear the deadlock... - if ( my $db_tbl = $o->get('clear-deadlocks') ) { - PTDEBUG && _d('Creating --clear-deadlocks table', $db_tbl); - $dbh->{AutoCommit} = 0; - my $sql = $o->read_para_after(__FILE__, qr/MAGIC_clear_deadlocks/); - - if ( VersionParser->new($dbh) < '4.1.2') { - $sql =~ s/ENGINE=/TYPE=/; + if ( $ins_sth ) { + eval { + PTDEBUG && _d('Saving deadlock to --dest'); + foreach my $txn ( + sort { $a->{thread} <=> $b->{thread} } values %txns + ) { + $ins_sth->execute(@{$txn}{@cols}); + } + $dst->dbh->commit(); + }; + if ( my $e = $EVAL_ERROR ) { + PTDEBUG && _d('Error saving to --dest:', $e); + if ( $dst->lost_connection($e) ) { + eval { + $ins_sth->finish() if $ins_sth; + $dst->dbh->disconnect() if $dst->dbh; + $dst->connect(AutoCommit => 0); + $ins_sth = $dst->dbh->prepare($ins_sql); + }; + if ( $EVAL_ERROR ) { + warn "Lost connection to " . $dst->name . ". Will try " + . "to reconnect in the next iteration.\n"; + } + else { + PTDEBUG && _d('Reconnected to MySQL (--dest)'); + redo ITERATION; + } + } + else { + warn "Error saving to --dest: $EVAL_ERROR"; + $exit_status |= 1; + } + } } - $sql =~ s/test.deadlock_maker/$db_tbl/; - PTDEBUG && _d($sql); - $dbh->do($sql); - $sql = "INSERT INTO $db_tbl(a) VALUES(1)"; - PTDEBUG && _d($sql); - $dbh->do($sql); # I'm holding locks on the table now. - # Fork off a child to try to take a lock on the table. - my $pid = fork(); - if ( defined($pid) && $pid == 0 ) { # I am a child - my $dbh_child = get_cxn($source_dsn, 0); - $sql = "SELECT * FROM $db_tbl FOR UPDATE"; - PTDEBUG && _d($sql); - eval { $dbh_child->do($sql); }; # Should block against parent. - PTDEBUG && _d($EVAL_ERROR); # Parent inserted value 0. - $sql = "COMMIT"; - PTDEBUG && _d($sql); - $dbh_child->do($sql); - exit; + if ( $fingerprint ne $last_fingerprint ) { + PTDEBUG && _d('New deadlock'); + if ( !$o->get('quiet') ) { + print join($sep, @cols), "\n"; + foreach my $txn ( + sort { $a->{thread} <=> $b->{thread} } values %txns + ) { + $txn->{query} =~ s/\s+/ /g; + print join($sep, map { $txn->{$_} } @cols), "\n"; + } + } } - elsif ( !defined($pid) ) { - die("Unable to fork for clearing deadlocks!\n"); + else { + PTDEBUG && _d('Same deadlock, not printing'); + } + + $last_fingerprint = $fingerprint; + + if ( $clear_deadlocks_table ) { + clear_deadlocks( + dsn => $src->dsn, + table => $clear_deadlocks_table, + table_def => $clear_deadlocks_table_def, + DSNParser => $dp, + ); } - sleep 1; - $sql = "INSERT INTO $db_tbl(a) VALUES(0)";# Will make child deadlock - PTDEBUG && _d($sql); - eval { $dbh->do($sql); }; - PTDEBUG && _d($EVAL_ERROR); - waitpid($pid, 0); - $sql = "DROP TABLE $db_tbl"; - PTDEBUG && _d($sql); - $dbh->do($sql); } - - # If there's an --interval argument, run forever or till specified. - # Otherwise just run once. - if ( $o->get('interval') ) { - sleep($o->get('interval')); - $now = time(); - } - else { - $oktorun = 0; + + # Sleep if there's an --iteration left. + if ( !defined $iters || $iters ) { + PTDEBUG && _d('Sleeping', $interval, 'seconds'); + sleep $interval; } } - return 0; + PTDEBUG && _d('Done running, exiting', $exit_status); + return $exit_status; } # ############################################################################ @@ -4244,7 +4588,7 @@ sub parse_deadlocks { my ( $query_text ) = $body =~ m/\nMySQL thread id .*\n((?s).*)/; $query_text =~ s/\s+$//; - $query_text =~ s/\s+/ /g if $args->{'collapse'}; + $query_text =~ s/\s+/ /g; @{$hash}{qw(thread hostname ip user query)} = ($mysql_thread_id, $hostname, $ip, $user, $query_text); @@ -4290,13 +4634,100 @@ sub parse_deadlocks { foreach my $txn ( values %txns ) { $txn->{victim} = $txn->{id} == $victim ? 1 : 0; $txn->{ts} = $ts; - $txn->{server} = $source_dsn->{h} || ''; + $txn->{server} = $args->{server} || ''; $txn->{ip} = inet_aton($txn->{ip}) if $args->{'numeric-ip'}; } return \%txns; } +sub clear_deadlocks { + my (%args) = @_; + my @required_args = qw(dsn table table_def DSNParser); + foreach my $arg ( @required_args ) { + die "I need a $arg argument" unless $args{$arg}; + } + my $dsn = $args{dsn}; + my $table = $args{table}; + my $table_def = $args{table_def}; + my $dp = $args{DSNParser}; + PTDEBUG && _d('Clearing deadlocks with table', $table, $table_def); + + my $parent_dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit=>0 }); + $parent_dbh->{InactiveDestroy} = 1; # because of forking + + # Create the deadlocks table. + PTDEBUG && _d($table_def); + $parent_dbh->do($table_def); + + # Get a lock on it. + my $sql = "INSERT INTO $table (a) VALUES (1) " + . "/* pt-deadlock-logger clear deadlocks parent */"; + PTDEBUG && _d($sql); + $parent_dbh->do($sql); + + my ($sync_fh, $sync_file) = tempfile( + 'pt-deadlock-logger-clear-deadlocks.XXXXXXX', + DIR => File::Spec->tmpdir(), + ); + PTDEBUG && _d('Sync file:', $sync_file); + close $sync_fh; + unlink $sync_file; + + # Fork a child to try to take a lock on the table. + my $pid = fork(); + if ( defined($pid) && $pid == 0 ) { + # I am the child + PTDEBUG && _d('Clear deadlocks child', $PID); + my $child_dbh = $dp->get_dbh($dp->get_cxn_params($dsn), {AutoCommit=>0}); + my $sql = "SELECT * FROM $table FOR UPDATE " + . "/* pt-deadlock-logger clear deadlocks child */"; + PTDEBUG && _d($sql); + open my $fh, '>', $sync_file + or die "Error creating $sync_file: $OS_ERROR"; + close $fh; + PTDEBUG && _d('Clear deadlocks child ready (child)'); + eval { $child_dbh->do($sql); }; # Should block against parent. + PTDEBUG && _d($EVAL_ERROR); # Parent inserted value 0. + $child_dbh->commit(); + $child_dbh->disconnect(); + exit; + } + elsif ( !defined($pid) ) { + die "Failed to fork for --clear-deadlocks: " . ($OS_ERROR || ''); + } + + # Wait up to 10s for the child to connect and become ready. + for ( 1..40 ) { + last if -f $sync_file; + PTDEBUG && _d('Waiting for the clear deadlocks child'); + sleep 0.25; + } + PTDEBUG && _d('Clear deadlocks child ready (parent)'); + sleep 0.25; # wait for child to exec its SELECT statement + + # Make the child deadlock. + $sql = "INSERT INTO $table (a) VALUES (0) " + . "/* pt-deadlock-logger clear deadlocks parent */"; + PTDEBUG && _d($sql); + eval { $parent_dbh->do($sql); }; + PTDEBUG && _d($EVAL_ERROR); + + # Reap the child. + waitpid($pid, 0); + + # Drop the table. + $sql = "DROP TABLE IF EXISTS $table"; + PTDEBUG && _d($sql); + $parent_dbh->do($sql); + + $parent_dbh->disconnect(); + + unlink $sync_file; + + return; +} + sub fingerprint { my ( $txns ) = @_; my $fingerprint = ''; @@ -4308,21 +4739,11 @@ sub fingerprint { return $fingerprint; } -# Catches signals so the program can exit gracefully. -sub finish { - my ($signal) = @_; - print STDERR "Exiting on SIG$signal.\n"; +sub sig_int { + my ( $signal ) = @_; $oktorun = 0; -} - -sub get_cxn { - my ( $dsn, $ac ) = @_; - if ( $o->get('ask-pass') ) { - $dsn->{p} = OptionParser::prompt_noecho("Enter password: "); - } - my $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), {AutoCommit => $ac}); - $dbh->{InactiveDestroy} = 1; # Because of forking. - return $dbh; + print STDERR "# Caught SIG$signal. Use 'kill -ABRT $PID' if " + . "the tool does not exit normally in a few seconds.\n"; } sub _d { @@ -4347,32 +4768,28 @@ if ( !caller ) { exit main(@ARGV); } =head1 NAME -pt-deadlock-logger - Extract and log MySQL deadlock information. +pt-deadlock-logger - Log MySQL deadlocks. =head1 SYNOPSIS -Usage: pt-deadlock-logger [OPTION...] SOURCE_DSN +Usage: pt-deadlock-logger [OPTIONS] DSN -pt-deadlock-logger extracts and saves information about the most recent deadlock -in a MySQL server. +pt-deadlock-logger logs information about MySQL deadlocks on the given +DSN. Information is printed to C, and it can also be saved to a +table by specifying L<"--dest">. The tool runs for forever unless +L<"--run-time"> or L<"--iterations"> is specified. -Print deadlocks on SOURCE_DSN: +Print deadlocks on host1: - pt-deadlock-logger SOURCE_DSN + pt-fk-error-logger h=host1 -Store deadlock information from SOURCE_DSN in test.deadlocks table on SOURCE_DSN -(source and destination are the same host): +Print deadlocks on host1 once then exit: - pt-deadlock-logger SOURCE_DSN --dest D=test,t=deadlocks + pt-fk-error-logger h=host1 --iterations 1 -Store deadlock information from SOURCE_DSN in test.deadlocks table on DEST_DSN -(source and destination are different hosts): +Save deadlocks on host1 to percona_schema.fke on host2: - pt-deadlock-logger SOURCE_DSN --dest DEST_DSN,D=test,t=deadlocks - -Daemonize and check for deadlocks on SOURCE_DSN every 30 seconds for 4 hours: - - pt-deadlock-logger SOURCE_DSN --dest D=test,t=deadlocks --daemonize --run-time 4h --interval 30s + pt-fk-error-logger h=host1 --dest h=host2,D=percona_schema,t=deadlocks =head1 RISKS @@ -4398,43 +4815,93 @@ See also L<"BUGS"> for more information on filing bugs and getting help. =head1 DESCRIPTION -pt-deadlock-logger extracts deadlock data from a MySQL server. Currently only -InnoDB deadlock information is available. You can print the information to -standard output, store it in a database table, or both. If neither -L<"--print"> nor L<"--dest"> are given, then the deadlock information is -printed by default. If only L<"--dest"> is given, then the deadlock -information is only stored. If both options are given, then the deadlock -information is printed and stored. +pt-deadlock-logger prints information about MySQL deadlocks by polling +and parsing C. When a new deadlock occurs, +it's printed to C and, if specified, saved to L<"--dest">. -The source host can be specified using one of two methods. The first method is -to use at least one of the standard connection-related command line options: -L<"--defaults-file">, L<"--password">, L<"--host">, L<"--port">, L<"--socket"> -or L<"--user">. These options only apply to the source host; they cannot be -used to specify the destination host. +Only new deadlocks are printed. A fingerprint for each deadlock is created +using the deadlock's server, ts, and thread values (even if these +columns are not specified by L<"--columns">). A deadlock is printed if +its fingerprint is different than the last deadlock's fingerprint. -The second method to specify the source host, or the optional destination host -using L<"--dest">, is a DSN. A DSN is a special syntax that can be either just -a hostname (like C or C<1.2.3.4>), or a -C string. Keys are a single letter: - - KEY MEANING - === ======= - h Connect to host - P Port number to use for connection - S Socket file to use for connection - u User for login if not current user - p Password to use when connecting - F Only read default options from the given file - -If you omit any values from the destination host DSN, they are filled in with -values from the source host, so you don't need to specify them in both places. -C reads all normal MySQL option files, such as ~/.my.cnf, so -you may not need to specify username, password and other common options at all. +The L<"--dest"> statement uses C to eliminate duplicate +deadlocks, so every deadlock is saved for every L<"--iterations">. =head1 OUTPUT -You can choose which columns are output and/or saved to L<"--dest"> with the -L<"--columns"> argument. The default columns are as follows: +New deadlocks are printed to C, unless L<"--quiet"> is specified. +Errors and warnings are printed to C. + +See also L<"--columns"> and L<"--tab">. + +=head1 INNODB CAVEATS AND DETAILS + +InnoDB's output is hard to parse and sometimes there's no way to do it right. + +Sometimes not all information (for example, username or IP address) is included +in the deadlock information. In this case there's nothing for the script to put +in those columns. It may also be the case that the deadlock output is so long +(because there were a lot of locks) that the whole thing is truncated. + +Though there are usually two transactions involved in a deadlock, there are more +locks than that; at a minimum, one more lock than transactions is necessary to +create a cycle in the waits-for graph. pt-deadlock-logger prints the +transactions (always two in the InnoDB output, even when there are more +transactions in the waits-for graph than that) and fills in locks. It prefers +waited-for over held when choosing lock information to output, but you can +figure out the rest with a moment's thought. If you see one wait-for and one +held lock, you're looking at the same lock, so of course you'd prefer to see +both wait-for locks and get more information. If the two waited-for locks are +not on the same table, more than two transactions were involved in the deadlock. + +Finally, keep in mind that, because usernames with spaces are not quoted by +InnoDB, the tool will generally misreport the second word of these usernames +as the hostname. + +=head1 OPTIONS + +This tool accepts additional command-line arguments. Refer to the +L<"SYNOPSIS"> and usage information for details. + +=over + +=item --ask-pass + +Prompt for a password when connecting to MySQL. + +=item --charset + +short form: -A; type: string + +Default character set. If the value is utf8, sets Perl's binmode on +STDOUT to utf8, passes the mysql_enable_utf8 option to DBD::mysql, and runs SET +NAMES UTF8 after connecting to MySQL. Any other value sets binmode on STDOUT +without the utf8 layer, and runs SET NAMES after connecting to MySQL. + +=item --clear-deadlocks + +type: string + +Use this table to create a small deadlock. This usually has the effect of +clearing out a huge deadlock, which otherwise consumes the entire output of +C. The table must not exist. pt-deadlock-logger will +create it with the following structure: + +=for comment ignore-pt-internal-value +MAGIC_clear_deadlocks + + CREATE TABLE percona_schema.clear_deadlocks ( + a INT PRIMARY KEY + ) ENGINE=InnoDB + +After creating the table and causing a small deadlock, the tool will drop the +table again. + +=item --columns + +type: Array; default: server, ts, thread, txn_id, txn_time, user, hostname, ip, db, tbl, idx, lock_type, lock_mode, wait_hold, victim, query + +The columns are: =over @@ -4509,78 +4976,6 @@ The query that caused the deadlock. =back -=head1 INNODB CAVEATS AND DETAILS - -InnoDB's output is hard to parse and sometimes there's no way to do it right. - -Sometimes not all information (for example, username or IP address) is included -in the deadlock information. In this case there's nothing for the script to put -in those columns. It may also be the case that the deadlock output is so long -(because there were a lot of locks) that the whole thing is truncated. - -Though there are usually two transactions involved in a deadlock, there are more -locks than that; at a minimum, one more lock than transactions is necessary to -create a cycle in the waits-for graph. pt-deadlock-logger prints the -transactions (always two in the InnoDB output, even when there are more -transactions in the waits-for graph than that) and fills in locks. It prefers -waited-for over held when choosing lock information to output, but you can -figure out the rest with a moment's thought. If you see one wait-for and one -held lock, you're looking at the same lock, so of course you'd prefer to see -both wait-for locks and get more information. If the two waited-for locks are -not on the same table, more than two transactions were involved in the deadlock. - -Finally, keep in mind that, because usernames with spaces are not quoted by -InnoDB, the tool will generally misreport the second word of these usernames -as the hostname. - -=head1 OPTIONS - -This tool accepts additional command-line arguments. Refer to the -L<"SYNOPSIS"> and usage information for details. - -=over - -=item --ask-pass - -Prompt for a password when connecting to MySQL. - -=item --charset - -short form: -A; type: string - -Default character set. If the value is utf8, sets Perl's binmode on -STDOUT to utf8, passes the mysql_enable_utf8 option to DBD::mysql, and runs SET -NAMES UTF8 after connecting to MySQL. Any other value sets binmode on STDOUT -without the utf8 layer, and runs SET NAMES after connecting to MySQL. - -=item --clear-deadlocks - -type: string - -Use this table to create a small deadlock. This usually has the effect of -clearing out a huge deadlock, which otherwise consumes the entire output of -C. The table must not exist. pt-deadlock-logger will -create it with the following MAGIC_clear_deadlocks structure: - - CREATE TABLE test.deadlock_maker(a INT PRIMARY KEY) ENGINE=InnoDB; - -After creating the table and causing a small deadlock, the tool will drop the -table again. - -=item --[no]collapse - -Collapse whitespace in queries to a single space. This might make it easier to -inspect on the command line or in a query. By default, whitespace is collapsed -when printing with L<"--print">, but not modified when storing to L<"--dest">. -(That is, the default is different for each action). - -=item --columns - -type: hash - -Output only this comma-separated list of columns. See L<"OUTPUT"> for more -details on columns. - =item --config type: Array @@ -4617,12 +5012,12 @@ Missing values are filled in with the same values from the source host, so you can usually omit most parts of this argument if you're storing deadlocks on the same server on which they happen. -By default, whitespace in the query column is left intact; -use L<"--[no]collapse"> if you want whitespace collapsed. - -The following MAGIC_dest_table is suggested if you want to store all the +The following table structure is suggested if you want to store all the information pt-deadlock-logger can extract about deadlocks: +=for comment ignore-pt-internal-value +MAGIC_dest_table + CREATE TABLE deadlocks ( server char(20) NOT NULL, ts datetime NOT NULL, @@ -4658,12 +5053,23 @@ Connect to host. =item --interval -type: time +type: time; default: 30 How often to check for deadlocks. If no L<"--run-time"> is specified, pt-deadlock-logger runs forever, checking for deadlocks at every interval. See also L<"--run-time">. +=item --iterations + +type: int + +How many times to check for deadlocks. By default, this option +is undefined which means an infinite number of iterations. The tool always +exits for L<"--run-time">, regardless of the value specified for this option. +For example, the tool will exit after 1 minute with +C<--run-time 1m --iterations 4 --interval 30> because 4 iterations at 30 +second intervals would take 2 minutes, longer than the 1 mintue run-time. + =item --log type: string @@ -4684,10 +5090,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process ID of -the daemonized instance. The PID file is removed when the daemonized instance -exits. The program checks for the existence of the PID file when starting; if -it exists and the process with the matching PID exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port @@ -4695,16 +5102,9 @@ short form: -P; type: int Port number to use for connection. -=item --print +=item --quiet -Print results on standard output. See L<"OUTPUT"> for more. By default, -enables L<"--[no]collapse"> unless you explicitly disable it. - -If L<"--interval"> or L<"--run-time"> is specified, only new deadlocks are -printed at each interval. A fingerprint for each deadlock is created using -L<"--columns"> server, ts and thread (even if those columns were not specified -by L<"--columns">) and if the current deadlock's fingerprint is different from -the last deadlock's fingerprint, then it is printed. +Do not deadlocks; only print errors and warnings to C. =item --run-time @@ -4729,7 +5129,7 @@ Socket file to use for connection. =item --tab -Print tab-separated columns, instead of aligned. +Use tabs to separate columns instead of spaces. =item --user @@ -4886,7 +5286,7 @@ Replace C with the name of any tool. =head1 AUTHORS -Baron Schwartz +Baron Schwartz and Daniel Nichter =head1 ABOUT PERCONA TOOLKIT diff --git a/bin/pt-duplicate-key-checker b/bin/pt-duplicate-key-checker index 97cd56ec..b4ce8d50 100755 --- a/bin/pt-duplicate-key-checker +++ b/bin/pt-duplicate-key-checker @@ -5130,14 +5130,11 @@ Password to use when connecting. type: string -Create the given PID file. The file contains the process ID of the script. -The PID file is removed when the script exits. Before starting, the script -checks if the PID file already exists. If it does not, then the script creates -and writes its own PID to it. If it does, then the script checks the following: -if the file contains a PID and a process is running with that PID, then -the script dies; or, if there is no process running with that PID, then the -script overwrites the file with its own PID and starts; else, if the file -contains no PID, then the script dies. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-fifo-split b/bin/pt-fifo-split index 06303be6..a3c7e81d 100755 --- a/bin/pt-fifo-split +++ b/bin/pt-fifo-split @@ -1456,14 +1456,11 @@ printed to the fifo. type: string -Create the given PID file. The file contains the process ID of the script. -The PID file is removed when the script exits. Before starting, the script -checks if the PID file already exists. If it does not, then the script creates -and writes its own PID to it. If it does, then the script checks the following: -if the file contains a PID and a process is running with that PID, then -the script dies; or, if there is no process running with that PID, then the -script overwrites the file with its own PID and starts; else, if the file -contains no PID, then the script dies. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --statistics diff --git a/bin/pt-find b/bin/pt-find index 67ba6084..100af3ef 100755 --- a/bin/pt-find +++ b/bin/pt-find @@ -4241,14 +4241,11 @@ Password to use when connecting. type: string -Create the given PID file. The file contains the process ID of the script. -The PID file is removed when the script exits. Before starting, the script -checks if the PID file already exists. If it does not, then the script creates -and writes its own PID to it. If it does, then the script checks the following: -if the file contains a PID and a process is running with that PID, then -the script dies; or, if there is no process running with that PID, then the -script overwrites the file with its own PID and starts; else, if the file -contains no PID, then the script dies. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-fk-error-logger b/bin/pt-fk-error-logger index 8dda698e..4de8b6c2 100755 --- a/bin/pt-fk-error-logger +++ b/bin/pt-fk-error-logger @@ -17,10 +17,12 @@ BEGIN { OptionParser Quoter DSNParser + Cxn Daemon Transformers HTTPMicro VersionCheck + Runtime )); } @@ -1595,6 +1597,181 @@ sub _d { # End DSNParser package # ########################################################################### +# ########################################################################### +# Cxn 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/Cxn.pm +# t/lib/Cxn.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Cxn; + +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, +}; + +sub new { + my ( $class, %args ) = @_; + my @required_args = qw(DSNParser OptionParser); + foreach my $arg ( @required_args ) { + die "I need a $arg argument" unless $args{$arg}; + }; + my ($dp, $o) = @args{@required_args}; + + my $dsn_defaults = $dp->parse_options($o); + my $prev_dsn = $args{prev_dsn}; + my $dsn = $args{dsn}; + if ( !$dsn ) { + $args{dsn_string} ||= 'h=' . ($dsn_defaults->{h} || 'localhost'); + + $dsn = $dp->parse( + $args{dsn_string}, $prev_dsn, $dsn_defaults); + } + elsif ( $prev_dsn ) { + $dsn = $dp->copy($prev_dsn, $dsn); + } + + my $self = { + dsn => $dsn, + dbh => $args{dbh}, + dsn_name => $dp->as_string($dsn, [qw(h P S)]), + hostname => '', + set => $args{set}, + NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, + dbh_set => 0, + OptionParser => $o, + DSNParser => $dp, + is_cluster_node => undef, + parent => $args{parent}, + }; + + return bless $self, $class; +} + +sub connect { + my ( $self, %opts ) = @_; + my $dsn = $self->{dsn}; + my $dp = $self->{DSNParser}; + my $o = $self->{OptionParser}; + + my $dbh = $self->{dbh}; + if ( !$dbh || !$dbh->ping() ) { + if ( $o->get('ask-pass') && !$self->{asked_for_pass} ) { + $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); + $self->{asked_for_pass} = 1; + } + $dbh = $dp->get_dbh( + $dp->get_cxn_params($dsn), + { + AutoCommit => 1, + %opts, + }, + ); + } + + $dbh = $self->set_dbh($dbh); + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name}); + return $dbh; +} + +sub set_dbh { + my ($self, $dbh) = @_; + + if ( $self->{dbh} && $self->{dbh} == $dbh && $self->{dbh_set} ) { + PTDEBUG && _d($dbh, 'Already set dbh'); + return $dbh; + } + + PTDEBUG && _d($dbh, 'Setting dbh'); + + $dbh->{FetchHashKeyName} = 'NAME_lc' if $self->{NAME_lc}; + + my $sql = 'SELECT @@hostname, @@server_id'; + PTDEBUG && _d($dbh, $sql); + my ($hostname, $server_id) = $dbh->selectrow_array($sql); + PTDEBUG && _d($dbh, 'hostname:', $hostname, $server_id); + if ( $hostname ) { + $self->{hostname} = $hostname; + } + + if ( $self->{parent} ) { + PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent'); + $dbh->{InactiveDestroy} = 1; + } + + if ( my $set = $self->{set}) { + $set->($dbh); + } + + $self->{dbh} = $dbh; + $self->{dbh_set} = 1; + return $dbh; +} + +sub lost_connection { + my ($self, $e) = @_; + return 0 unless $e; + return $e =~ m/MySQL server has gone away/ + || $e =~ m/Lost connection to MySQL server/; +} + +sub dbh { + my ($self) = @_; + return $self->{dbh}; +} + +sub dsn { + my ($self) = @_; + return $self->{dsn}; +} + +sub name { + my ($self) = @_; + return $self->{dsn_name} if PERCONA_TOOLKIT_TEST_USE_DSN_NAMES; + return $self->{hostname} || $self->{dsn_name} || 'unknown host'; +} + +sub DESTROY { + my ($self) = @_; + + PTDEBUG && _d('Destroying cxn'); + + if ( $self->{parent} ) { + PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent'); + } + elsif ( $self->{dbh} + && blessed($self->{dbh}) + && $self->{dbh}->can("disconnect") ) + { + PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname}, + $self->{dsn_name}); + $self->{dbh}->disconnect(); + } + + return; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Cxn package +# ########################################################################### + # ########################################################################### # Daemon package # This package is a copy without comments from the original. The original @@ -3374,6 +3551,139 @@ sub _d { # End VersionCheck package # ########################################################################### +# ########################################################################### +# Runtime 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/Runtime.pm +# t/lib/Runtime.t +# See https://launchpad.net/percona-toolkit for more information. +# ########################################################################### +{ +package Runtime; + +use strict; +use warnings FATAL => 'all'; +use English qw(-no_match_vars); +use constant PTDEBUG => $ENV{PTDEBUG} || 0; + +sub new { + my ( $class, %args ) = @_; + my @required_args = qw(run_time now); + foreach my $arg ( @required_args ) { + die "I need a $arg argument" unless exists $args{$arg}; + } + + my $run_time = $args{run_time}; + if ( defined $run_time ) { + die "run_time must be > 0" if $run_time <= 0; + } + + my $now = $args{now}; + die "now must be a callback" unless ref $now eq 'CODE'; + + my $self = { + run_time => $run_time, + now => $now, + start_time => undef, + end_time => undef, + time_left => undef, + stop => 0, + }; + + return bless $self, $class; +} + +sub time_left { + my ( $self, %args ) = @_; + + if ( $self->{stop} ) { + PTDEBUG && _d("No time left because stop was called"); + return 0; + } + + my $now = $self->{now}->(%args); + PTDEBUG && _d("Current time:", $now); + + if ( !defined $self->{start_time} ) { + $self->{start_time} = $now; + } + + return unless defined $now; + + my $run_time = $self->{run_time}; + return unless defined $run_time; + + if ( !$self->{end_time} ) { + $self->{end_time} = $now + $run_time; + PTDEBUG && _d("End time:", $self->{end_time}); + } + + $self->{time_left} = $self->{end_time} - $now; + PTDEBUG && _d("Time left:", $self->{time_left}); + return $self->{time_left}; +} + +sub have_time { + my ( $self, %args ) = @_; + my $time_left = $self->time_left(%args); + return 1 if !defined $time_left; # run forever + return $time_left <= 0 ? 0 : 1; # <=0s means run time has elapsed +} + +sub time_elapsed { + my ( $self, %args ) = @_; + + my $start_time = $self->{start_time}; + return 0 unless $start_time; + + my $now = $self->{now}->(%args); + PTDEBUG && _d("Current time:", $now); + + my $time_elapsed = $now - $start_time; + PTDEBUG && _d("Time elapsed:", $time_elapsed); + if ( $time_elapsed < 0 ) { + warn "Current time $now is earlier than start time $start_time"; + } + return $time_elapsed; +} + +sub reset { + my ( $self ) = @_; + $self->{start_time} = undef; + $self->{end_time} = undef; + $self->{time_left} = undef; + $self->{stop} = 0; + PTDEBUG && _d("Reset run time"); + return; +} + +sub stop { + my ( $self ) = @_; + $self->{stop} = 1; + return; +} + +sub start { + my ( $self ) = @_; + $self->{stop} = 0; + return; +} + +sub _d { + my ($package, undef, $line) = caller 0; + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } + map { defined $_ ? $_ : 'undef' } + @_; + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; +} + +1; +} +# ########################################################################### +# End Runtime package +# ########################################################################### + # ########################################################################### # This is a combination of modules and programs in one -- a runnable module. # http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last @@ -3387,18 +3697,21 @@ package pt_fk_error_logger; use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); -use sigtrap qw(handler finish untrapped normal-signals); + +use sigtrap 'handler', \&sig_int, 'normal-signals'; use Percona::Toolkit; use constant PTDEBUG => $ENV{PTDEBUG} || 0; Transformers->import(qw(parse_timestamp)); -my $oktorun; +my $oktorun = 1; +my $exit_status = 0; sub main { - local @ARGV = @_; # set global ARGV for this package - $oktorun = 1; + local @ARGV = @_; # set global ARGV for this package + $oktorun = 1; + $exit_status = 0; # ######################################################################## # Get configuration information. @@ -3409,69 +3722,56 @@ sub main { my $dp = $o->DSNParser(); $dp->prop('set-vars', $o->get('set-vars')); - my $dsn_defaults = $dp->parse_options($o); - my $src_dsn = @ARGV ? $dp->parse(shift @ARGV, $dsn_defaults) : $dsn_defaults; - my $dst_dsn = $o->get('dest'); - # The source dsn is not an option so --dest cannot use OptionParser - # to inherit values from it. Thus, we do it manually. --dest will - # inherit from --user, --port, etc. - if ( $src_dsn && $dst_dsn ) { - # If dest DSN only has D and t, this will copy h, P, S, etc. - # from the source DSN. - $dst_dsn = $dp->copy($src_dsn, $dst_dsn); + my $src; + if ( my $src_dsn_string = shift @ARGV ) { + $src = Cxn->new( + dsn_string => $src_dsn_string, + parent => $o->get('daemonize'), + DSNParser => $dp, + OptionParser => $o, + ); + } + + my $dst; + if ( my $dst_dsn = $o->get('dest') ) { + $dst = Cxn->new( + dsn => $dst_dsn, + prev_dsn => ($src ? $src->dsn : undef), + parent => $o->get('daemonize'), + DSNParser => $dp, + OptionParser => $o, + ); } if ( !$o->get('help') ) { - if ( !$src_dsn ) { - $o->save_error('Missing or invalid source host'); + if ( !$src ) { + $o->save_error('No DSN was specified.'); } - if ( $dst_dsn && !$dst_dsn->{D} ) { - $o->save_error("--dest requires a 'D' (database) part"); + if ( $dst && !$dst->dsn->{D} ) { + $o->save_error("--dest requires a 'D' (database) part."); } - if ( $dst_dsn && !$dst_dsn->{t} ) { - $o->save_error("--dest requires a 't' (table) part"); + if ( $dst && !$dst->dsn->{t} ) { + $o->save_error("--dest requires a 't' (table) part."); } } $o->usage_or_errors(); # ######################################################################## - # Make common modules. + # Connect to MySQL. # ######################################################################## - my $q = new Quoter(); - my %modules = ( - o => $o, - dp => $dp, - q => $q, - ); + my $q = Quoter->new(); + + $src->connect(); - # ######################################################################## - # Start working. - # ######################################################################## - my $dbh = get_cxn($src_dsn, 1, %modules); - my $start = time(); - my $end = $start + ($o->get('run-time') || 0); # When we should exit - my $now = $start; - my $dst_dbh; my $ins_sth; - - # Since the user might not have specified a hostname for the connection, - # try to extract it from the $dbh - if ( !$src_dsn->{h} ) { - ($src_dsn->{h}) = $dbh->{mysql_hostinfo} =~ m/(\w+) via/; - PTDEBUG && _d('Got source host from dbh:', $src_dsn->{h}); - } - - if ( $dst_dsn ) { - my $db_tbl = join('.', - map { $q->quote($_) } - grep { $_ } - ( $dst_dsn->{D}, $dst_dsn->{t} )); - $dst_dbh = get_cxn($dst_dsn, 1, %modules); - my $sql = "INSERT IGNORE INTO $db_tbl VALUES (?, ?)"; - PTDEBUG && _d('insert sql:', $sql); - $ins_sth = $dst_dbh->prepare($sql); + if ( $dst ) { + $dst->connect(); + my $db_tbl = $q->join_quote($dst->dsn->{D}, $dst->dsn->{t}); + my $sql = "INSERT IGNORE INTO $db_tbl VALUES (?, ?)"; + PTDEBUG && _d('--dest INSERT SQL:', $sql); + $ins_sth = $dst->dbh->prepare($sql); } # ######################################################################## @@ -3489,6 +3789,16 @@ sub main { $daemon->make_PID_file(); } + # If we daemonized, the parent has already exited and we're the child. + # We shared a copy of every Cxn with the parent, and the parent's copies + # were destroyed but the dbhs were not disconnected because the parent + # attrib was true. Now, as the child, set it false so the dbhs will be + # disconnected when our Cxn copies are destroyed. If we didn't daemonize, + # then we're not really a parent (since we have no children), so set it + # false to auto-disconnect the dbhs when our Cxns are destroyed. + $src->{parent} = 0; + $dst->{parent} = 0 if $dst; + # ######################################################################## # Do the version-check # ######################################################################## @@ -3496,8 +3806,8 @@ sub main { VersionCheck::version_check( force => $o->got('version-check'), instances => [ - { dbh => $dbh, dsn => $src_dsn }, - ($dst_dbh ? { dbh => $dst_dbh, dsn => $dst_dsn } : ()) + { dbh => $src->dbh, dsn => $src->dsn }, + ($dst ? { dbh => $dst->dbh, dsn => $dst->dsn } : ()) ], ); } @@ -3505,43 +3815,77 @@ sub main { # ######################################################################## # Start finding and logging foreign key errors. # ######################################################################## - while ( # Quit if: - ($start == $end || $now < $end) # time is exceeded - && $oktorun # or instructed to quit - ) - { - my $text = $dbh->selectrow_hashref("SHOW /*!40100 ENGINE*/ INNODB STATUS")->{Status}; - my ($ts, $fk_error) = get_fk_error($text); - PTDEBUG && _d('ts:', $ts, 'fk error:', $fk_error); + my $run_time = Runtime->new( + run_time => $o->get('run-time'), + now => sub { return time }, + ); - if ( $ts && $fk_error ) { - # Save and/or print the foreign key error. - if ( $ins_sth ) { - my $fk_ts = parse_timestamp($ts); - PTDEBUG && _d('Saving fk error', $ts, $fk_error); - eval { - $ins_sth->execute($fk_ts, $fk_error); - }; + my $interval = $o->get('interval'); + my $iters = $o->get('iterations'); + PTDEBUG && _d('iterations:', $iters, 'interval:', $interval); + + ITERATION: + while ( + $oktorun + && $run_time->have_time() + && (!defined $iters || $iters--) + ) { + my ($ts, $fk_error); + eval { + my $sql = "SHOW /*!40100 ENGINE*/ INNODB STATUS " + . "/* pt-fk-error-logger */"; + PTDEBUG && _d($sql); + my $text = $src->dbh->selectrow_hashref($sql)->{status}; + ($ts, $fk_error) = get_fk_error($text); + }; + if ( my $e = $EVAL_ERROR ) { + PTDEBUG && _d('Error getting InnoDB status:', $e); + if ( $src->lost_connection($e) ) { + eval { $src->connect() }; if ( $EVAL_ERROR ) { - warn $EVAL_ERROR; - PTDEBUG && _d($EVAL_ERROR); + warn "Lost connection to MySQL. Will try to reconnect " + . "in the next iteration.\n"; + } + else { + PTDEBUG && _d('Reconnected to MySQL'); + redo ITERATION; } } - print "$ts $fk_error\n\n" if $o->get('print') || !$o->got('dest'); - } - - # If there's an --interval argument, run forever or till specified. - # Otherwise just run once. - if ( $o->get('interval') ) { - sleep($o->get('interval')); - $now = time(); + else { + warn "Error parsing SHOW ENGINE INNODB STATUS: $EVAL_ERROR"; + $exit_status |= 1; + } } else { - $oktorun = 0; + if ( $ts && $fk_error ) { + # Save and/or print the foreign key error. + if ( $ins_sth ) { + my $fk_ts = parse_timestamp($ts); + PTDEBUG && _d('Saving fk error', $ts, $fk_error); + eval { + $ins_sth->execute($fk_ts, $fk_error); + }; + if ( $EVAL_ERROR ) { + warn $EVAL_ERROR; + PTDEBUG && _d($EVAL_ERROR); + } + } + + if ( !$o->get('quiet') ) { + print "$ts $fk_error\n\n"; + } + } + } + + # Sleep if there's an --iteration left. + if ( !defined $iters || $iters ) { + PTDEBUG && _d('Sleeping', $interval, 'seconds'); + sleep $interval; } } - return 0; + PTDEBUG && _d('Done running, exiting', $exit_status); + return $exit_status; } # ############################################################################ @@ -3550,9 +3894,13 @@ sub main { sub get_fk_error { my ( $text ) = @_; + PTDEBUG && _d($text); # Quick check if text even has a foreign key error. - return unless $text =~ m/LATEST FOREIGN KEY ERROR/; + if ( $text !~ m/LATEST FOREIGN KEY ERROR/ ) { + PTDEBUG && _d('No fk error'); + return; + } # InnoDB timestamp my $idb_ts = qr/((?:\d{6}|\d{4}-\d\d-\d\d) .\d:\d\d:\d\d)/; @@ -3564,24 +3912,11 @@ sub get_fk_error { return $ts, $fke; } -# Catches signals so the program can exit gracefully. -sub finish { - my ($signal) = @_; - print STDERR "Exiting on SIG$signal.\n"; +sub sig_int { + my ( $signal ) = @_; $oktorun = 0; -} - -sub get_cxn { - my ( $dsn, $ac, %args ) = @_; - my $o = $args{o}; - my $dp = $args{dp}; - - if ( $o->get('ask-pass') ) { - $dsn->{p} = OptionParser::prompt_noecho("Enter password: "); - } - my $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), {AutoCommit => $ac}); - $dbh->{InactiveDestroy} = 1; # Because of forking. - return $dbh; + print STDERR "# Caught SIG$signal. Use 'kill -ABRT $PID' if " + . "the tool does not exit normally in a few seconds.\n"; } sub _d { @@ -3606,22 +3941,28 @@ if ( !caller ) { exit main(@ARGV); } =head1 NAME -pt-fk-error-logger - Extract and log MySQL foreign key errors. +pt-fk-error-logger - Log MySQL foreign key errors. =head1 SYNOPSIS -Usage: pt-fk-error-logger [OPTION...] SOURCE_DSN +Usage: pt-fk-error-logger [OPTIONS] DSN -pt-fk-error-logger extracts and saves information about the most recent foreign -key errors in a MySQL server. +pt-fk-error-logger logs information about foreign key errors on the given +DSN. Information is printed to C, and it can also be saved to a +table by specifying L<"--dest">. The tool runs for forever unless +L<"--run-time"> or L<"--iterations"> is specified. Print foreign key errors on host1: pt-fk-error-logger h=host1 -Save foreign key errors on host1 to db.foreign_key_errors table on host2: +Print foreign key errors on host1 once then exit: - pt-fk-error-logger h=host1 --dest h=host1,D=db,t=foreign_key_errors + pt-fk-error-logger h=host1 --iterations 1 + +Save foreign key errors on host1 to percona_schema.fke on host2: + + pt-fk-error-logger h=host1 --dest h=host2,D=percona_schema,t=fke =head1 RISKS @@ -3650,11 +3991,15 @@ C. The errors are not parsed or interpreted in any way. Foreign key errors are uniquely identified by their timestamp. Only new (more recent) errors are printed or saved. +By default the tool runs forever, checking every L<"--interval"> seconds +for new foreign key errors. Specify L<"--run-time"> and/or L<"--iterations"> +to limit how long the tool runs. + =head1 OUTPUT -If L<"--print"> is given or no L<"--dest"> is given, then pt-fk-error-logger -prints the foreign key error text to STDOUT exactly as it appeared in -C. +The foreign key error text from C is printed +to C, unless L<"--quiet"> is specified. Errors and warnings +are printed to C. =head1 OPTIONS @@ -3698,11 +4043,12 @@ pathname. type: DSN -DSN for where to store foreign key errors; specify at least a database (D) and table (t). +Save foreign key errors in this table. The DSN must specify a database (D) +and table (t). -Missing values are filled in with the same values from the source host, so you -can usually omit most parts of this argument if you're storing foreign key -errors on the same server on which they happen. +Missing DSN values are inherited from the DSN being monitored, so you +can omit most values if you're saving foreign key errors on the same +host. The following table is suggested: @@ -3726,10 +4072,21 @@ Connect to host. =item --interval -type: time; default: 0 +type: time; default: 30 How often to check for foreign key errors. +=item --iterations + +type: int + +How many times to check for foreign key errors. By default, this option +is undefined which means an infinite number of iterations. The tool always +exits for L<"--run-time">, regardless of the value specified for this option. +For example, the tool will exit after 1 minute with +C<--run-time 1m --iterations 4 --interval 30> because 4 iterations at 30 +second intervals would take 2 minutes, longer than the 1 mintue run-time. + =item --log type: string @@ -3746,10 +4103,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process ID of -the daemonized instance. The PID file is removed when the daemonized instance -exits. The program checks for the existence of the PID file when starting; if -it exists and the process with the matching PID exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port @@ -3757,15 +4115,15 @@ short form: -P; type: int Port number to use for connection. -=item --print +=item --quiet -Print results on standard output. See L<"OUTPUT"> for more. +Do not print foreign key errors; only print errors and warnings to C. =item --run-time type: time -How long to run before exiting. +How long to run before exiting. By default, the tool runs forever. =item --set-vars @@ -3948,7 +4306,7 @@ L for more software developed by Percona. =head1 COPYRIGHT, LICENSE, AND WARRANTY -This program is copyright 2011-2012 Percona Ireland Ltd. +This program is copyright 2011-2013 Percona Ireland Ltd. THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF diff --git a/bin/pt-heartbeat b/bin/pt-heartbeat index 8ba42405..4a8ccbae 100755 --- a/bin/pt-heartbeat +++ b/bin/pt-heartbeat @@ -5664,10 +5664,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process ID of -the daemonized instance. The PID file is removed when the daemonized instance -exits. The program checks for the existence of the PID file when starting; if -it exists and the process with the matching PID exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-kill b/bin/pt-kill index c3a7331d..a88586c7 100755 --- a/bin/pt-kill +++ b/bin/pt-kill @@ -4969,23 +4969,24 @@ sub new { } my $self = { - dsn => $dsn, - dbh => $args{dbh}, - dsn_name => $dp->as_string($dsn, [qw(h P S)]), - hostname => '', - set => $args{set}, - NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, - dbh_set => 0, - OptionParser => $o, - DSNParser => $dp, + dsn => $dsn, + dbh => $args{dbh}, + dsn_name => $dp->as_string($dsn, [qw(h P S)]), + hostname => '', + set => $args{set}, + NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, + dbh_set => 0, + OptionParser => $o, + DSNParser => $dp, is_cluster_node => undef, + parent => $args{parent}, }; return bless $self, $class; } sub connect { - my ( $self ) = @_; + my ( $self, %opts ) = @_; my $dsn = $self->{dsn}; my $dp = $self->{DSNParser}; my $o = $self->{OptionParser}; @@ -4996,11 +4997,18 @@ sub connect { $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); $self->{asked_for_pass} = 1; } - $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit => 1 }); + $dbh = $dp->get_dbh( + $dp->get_cxn_params($dsn), + { + AutoCommit => 1, + %opts, + }, + ); } - PTDEBUG && _d($dbh, 'Connected dbh to', $self->{name}); - return $self->set_dbh($dbh); + $dbh = $self->set_dbh($dbh); + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name}); + return $dbh; } sub set_dbh { @@ -5023,6 +5031,11 @@ sub set_dbh { $self->{hostname} = $hostname; } + if ( $self->{parent} ) { + PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent'); + $dbh->{InactiveDestroy} = 1; + } + if ( my $set = $self->{set}) { $set->($dbh); } @@ -5032,6 +5045,13 @@ sub set_dbh { return $dbh; } +sub lost_connection { + my ($self, $e) = @_; + return 0 unless $e; + return $e =~ m/MySQL server has gone away/ + || $e =~ m/Lost connection to MySQL server/; +} + sub dbh { my ($self) = @_; return $self->{dbh}; @@ -5050,12 +5070,21 @@ sub name { sub DESTROY { my ($self) = @_; - if ( $self->{dbh} - && blessed($self->{dbh}) - && $self->{dbh}->can("disconnect") ) { - PTDEBUG && _d('Disconnecting dbh', $self->{dbh}, $self->{name}); + + PTDEBUG && _d('Destroying cxn'); + + if ( $self->{parent} ) { + PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent'); + } + elsif ( $self->{dbh} + && blessed($self->{dbh}) + && $self->{dbh}->can("disconnect") ) + { + PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname}, + $self->{dsn_name}); $self->{dbh}->disconnect(); } + return; } @@ -6464,6 +6493,7 @@ sub main { $cxn = Cxn->new( dsn_string => shift @ARGV, NAME_lc => 0, + parent => $o->get('daemonize'), DSNParser => $dp, OptionParser => $o, ); @@ -6643,6 +6673,15 @@ sub main { $daemon->make_PID_file(); } + # If we daemonized, the parent has already exited and we're the child. + # We shared a copy of every Cxn with the parent, and the parent's copies + # were destroyed but the dbhs were not disconnected because the parent + # attrib was true. Now, as the child, set it false so the dbhs will be + # disconnected when our Cxn copies are destroyed. If we didn't daemonize, + # then we're not really a parent (since we have no children), so set it + # false to auto-disconnect the dbhs when our Cxns are destroyed. + $cxn->{parent} = 0 if $cxn; + # ######################################################################## # Do the version-check # ######################################################################## @@ -7311,10 +7350,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process ID of -the daemonized instance. The PID file is removed when the daemonized instance -exits. The program checks for the existence of the PID file when starting; if -it exists and the process with the matching PID exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-online-schema-change b/bin/pt-online-schema-change index 353ed2b9..e8884e6a 100755 --- a/bin/pt-online-schema-change +++ b/bin/pt-online-schema-change @@ -4018,23 +4018,24 @@ sub new { } my $self = { - dsn => $dsn, - dbh => $args{dbh}, - dsn_name => $dp->as_string($dsn, [qw(h P S)]), - hostname => '', - set => $args{set}, - NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, - dbh_set => 0, - OptionParser => $o, - DSNParser => $dp, + dsn => $dsn, + dbh => $args{dbh}, + dsn_name => $dp->as_string($dsn, [qw(h P S)]), + hostname => '', + set => $args{set}, + NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, + dbh_set => 0, + OptionParser => $o, + DSNParser => $dp, is_cluster_node => undef, + parent => $args{parent}, }; return bless $self, $class; } sub connect { - my ( $self ) = @_; + my ( $self, %opts ) = @_; my $dsn = $self->{dsn}; my $dp = $self->{DSNParser}; my $o = $self->{OptionParser}; @@ -4045,11 +4046,18 @@ sub connect { $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); $self->{asked_for_pass} = 1; } - $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit => 1 }); + $dbh = $dp->get_dbh( + $dp->get_cxn_params($dsn), + { + AutoCommit => 1, + %opts, + }, + ); } - PTDEBUG && _d($dbh, 'Connected dbh to', $self->{name}); - return $self->set_dbh($dbh); + $dbh = $self->set_dbh($dbh); + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name}); + return $dbh; } sub set_dbh { @@ -4072,6 +4080,11 @@ sub set_dbh { $self->{hostname} = $hostname; } + if ( $self->{parent} ) { + PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent'); + $dbh->{InactiveDestroy} = 1; + } + if ( my $set = $self->{set}) { $set->($dbh); } @@ -4081,6 +4094,13 @@ sub set_dbh { return $dbh; } +sub lost_connection { + my ($self, $e) = @_; + return 0 unless $e; + return $e =~ m/MySQL server has gone away/ + || $e =~ m/Lost connection to MySQL server/; +} + sub dbh { my ($self) = @_; return $self->{dbh}; @@ -4099,12 +4119,21 @@ sub name { sub DESTROY { my ($self) = @_; - if ( $self->{dbh} - && blessed($self->{dbh}) - && $self->{dbh}->can("disconnect") ) { - PTDEBUG && _d('Disconnecting dbh', $self->{dbh}, $self->{name}); + + PTDEBUG && _d('Destroying cxn'); + + if ( $self->{parent} ) { + PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent'); + } + elsif ( $self->{dbh} + && blessed($self->{dbh}) + && $self->{dbh}->can("disconnect") ) + { + PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname}, + $self->{dsn_name}); $self->{dbh}->disconnect(); } + return; } @@ -10839,10 +10868,11 @@ Password to use when connecting. type: string -Create the given PID file. The file contains the process ID of the tool's -instance. The PID file is removed when the tool exits. The tool checks for -the existence of the PID file when starting; if it exists and the process with -the matching PID exists, the tool exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-query-advisor b/bin/pt-query-advisor index 78d8c2fd..010f6cc1 100755 --- a/bin/pt-query-advisor +++ b/bin/pt-query-advisor @@ -9092,11 +9092,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process -ID of the daemonized instance. The PID file is removed when the -daemonized instance exits. The program checks for the existence of the -PID file when starting; if it exists and the process with the matching PID -exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-query-digest b/bin/pt-query-digest index 518d1891..6b671d26 100755 --- a/bin/pt-query-digest +++ b/bin/pt-query-digest @@ -14967,11 +14967,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process -ID of the daemonized instance. The PID file is removed when the -daemonized instance exits. The program checks for the existence of the -PID file when starting; if it exists and the process with the matching PID -exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-show-grants b/bin/pt-show-grants index 3f318ecd..9df78c0e 100755 --- a/bin/pt-show-grants +++ b/bin/pt-show-grants @@ -2093,14 +2093,11 @@ Password to use when connecting. type: string -Create the given PID file. The file contains the process ID of the script. -The PID file is removed when the script exits. Before starting, the script -checks if the PID file already exists. If it does not, then the script creates -and writes its own PID to it. If it does, then the script checks the following: -if the file contains a PID and a process is running with that PID, then -the script dies; or, if there is no process running with that PID, then the -script overwrites the file with its own PID and starts; else, if the file -contains no PID, then the script dies. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-slave-delay b/bin/pt-slave-delay index 75c9db13..b511c4a7 100755 --- a/bin/pt-slave-delay +++ b/bin/pt-slave-delay @@ -4478,11 +4478,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process -ID of the daemonized instance. The PID file is removed when the -daemonized instance exits. The program checks for the existence of the -PID file when starting; if it exists and the process with the matching PID -exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-slave-find b/bin/pt-slave-find index d104a4d6..730a18af 100755 --- a/bin/pt-slave-find +++ b/bin/pt-slave-find @@ -3960,14 +3960,11 @@ Password to use when connecting. type: string -Create the given PID file. The file contains the process ID of the script. -The PID file is removed when the script exits. Before starting, the script -checks if the PID file already exists. If it does not, then the script creates -and writes its own PID to it. If it does, then the script checks the following: -if the file contains a PID and a process is running with that PID, then -the script dies; or, if there is no process running with that PID, then the -script overwrites the file with its own PID and starts; else, if the file -contains no PID, then the script dies. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-slave-restart b/bin/pt-slave-restart index 71cad5b7..43efd494 100755 --- a/bin/pt-slave-restart +++ b/bin/pt-slave-restart @@ -5312,11 +5312,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process -ID of the daemonized instance. The PID file is removed when the -daemonized instance exits. The program checks for the existence of the -PID file when starting; if it exists and the process with the matching PID -exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-stalk b/bin/pt-stalk index 8adbd5bc..e2d5a518 100755 --- a/bin/pt-stalk +++ b/bin/pt-stalk @@ -1723,7 +1723,11 @@ Send mail to this list of addresses when data is collected. type: string; default: /var/run/pt-stalk.pid -Create a PID file when daemonized. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --plugin diff --git a/bin/pt-table-checksum b/bin/pt-table-checksum index e52a4df0..558b62c2 100755 --- a/bin/pt-table-checksum +++ b/bin/pt-table-checksum @@ -3376,23 +3376,24 @@ sub new { } my $self = { - dsn => $dsn, - dbh => $args{dbh}, - dsn_name => $dp->as_string($dsn, [qw(h P S)]), - hostname => '', - set => $args{set}, - NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, - dbh_set => 0, - OptionParser => $o, - DSNParser => $dp, + dsn => $dsn, + dbh => $args{dbh}, + dsn_name => $dp->as_string($dsn, [qw(h P S)]), + hostname => '', + set => $args{set}, + NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, + dbh_set => 0, + OptionParser => $o, + DSNParser => $dp, is_cluster_node => undef, + parent => $args{parent}, }; return bless $self, $class; } sub connect { - my ( $self ) = @_; + my ( $self, %opts ) = @_; my $dsn = $self->{dsn}; my $dp = $self->{DSNParser}; my $o = $self->{OptionParser}; @@ -3403,11 +3404,18 @@ sub connect { $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); $self->{asked_for_pass} = 1; } - $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit => 1 }); + $dbh = $dp->get_dbh( + $dp->get_cxn_params($dsn), + { + AutoCommit => 1, + %opts, + }, + ); } - PTDEBUG && _d($dbh, 'Connected dbh to', $self->{name}); - return $self->set_dbh($dbh); + $dbh = $self->set_dbh($dbh); + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name}); + return $dbh; } sub set_dbh { @@ -3430,6 +3438,11 @@ sub set_dbh { $self->{hostname} = $hostname; } + if ( $self->{parent} ) { + PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent'); + $dbh->{InactiveDestroy} = 1; + } + if ( my $set = $self->{set}) { $set->($dbh); } @@ -3439,6 +3452,13 @@ sub set_dbh { return $dbh; } +sub lost_connection { + my ($self, $e) = @_; + return 0 unless $e; + return $e =~ m/MySQL server has gone away/ + || $e =~ m/Lost connection to MySQL server/; +} + sub dbh { my ($self) = @_; return $self->{dbh}; @@ -3457,12 +3477,21 @@ sub name { sub DESTROY { my ($self) = @_; - if ( $self->{dbh} - && blessed($self->{dbh}) - && $self->{dbh}->can("disconnect") ) { - PTDEBUG && _d('Disconnecting dbh', $self->{dbh}, $self->{name}); + + PTDEBUG && _d('Destroying cxn'); + + if ( $self->{parent} ) { + PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent'); + } + elsif ( $self->{dbh} + && blessed($self->{dbh}) + && $self->{dbh}->can("disconnect") ) + { + PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname}, + $self->{dsn_name}); $self->{dbh}->disconnect(); } + return; } @@ -11590,14 +11619,11 @@ Password to use when connecting. type: string -Create the given PID file. The file contains the process ID of the script. -The PID file is removed when the script exits. Before starting, the script -checks if the PID file already exists. If it does not, then the script creates -and writes its own PID to it. If it does, then the script checks the following: -if the file contains a PID and a process is running with that PID, then -the script dies; or, if there is no process running with that PID, then the -script overwrites the file with its own PID and starts; else, if the file -contains no PID, then the script dies. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-table-sync b/bin/pt-table-sync index 1d4ff3f7..86bf9406 100755 --- a/bin/pt-table-sync +++ b/bin/pt-table-sync @@ -12051,14 +12051,11 @@ Password to use when connecting. type: string -Create the given PID file. The file contains the process ID of the script. -The PID file is removed when the script exits. Before starting, the script -checks if the PID file already exists. If it does not, then the script creates -and writes its own PID to it. If it does, then the script checks the following: -if the file contains a PID and a process is running with that PID, then -the script dies; or, if there is no process running with that PID, then the -script overwrites the file with its own PID and starts; else, if the file -contains no PID, then the script dies. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-table-usage b/bin/pt-table-usage index 2452194f..a32eebcc 100755 --- a/bin/pt-table-usage +++ b/bin/pt-table-usage @@ -7192,11 +7192,11 @@ Password to use when connecting. type: string -Create the given PID file when running. The file contains the process -ID of the daemonized instance. The PID file is removed when the -daemonized instance exits. The program checks for the existence of the -PID file when starting; if it exists and the process with the matching PID -exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-upgrade b/bin/pt-upgrade index 347733d8..6f1537d3 100755 --- a/bin/pt-upgrade +++ b/bin/pt-upgrade @@ -13483,11 +13483,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process -ID of the daemonized instance. The PID file is removed when the -daemonized instance exits. The program checks for the existence of the -PID file when starting; if it exists and the process with the matching PID -exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-variable-advisor b/bin/pt-variable-advisor index 6aa51591..b497892c 100755 --- a/bin/pt-variable-advisor +++ b/bin/pt-variable-advisor @@ -5752,11 +5752,11 @@ Password to use when connecting. type: string -Create the given PID file when daemonized. The file contains the process -ID of the daemonized instance. The PID file is removed when the -daemonized instance exits. The program checks for the existence of the -PID file when starting; if it exists and the process with the matching PID -exists, the program exits. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/bin/pt-visual-explain b/bin/pt-visual-explain index 78809597..16d370c8 100755 --- a/bin/pt-visual-explain +++ b/bin/pt-visual-explain @@ -2952,14 +2952,11 @@ Password to use when connecting. type: string -Create the given PID file. The file contains the process ID of the script. -The PID file is removed when the script exits. Before starting, the script -checks if the PID file already exists. If it does not, then the script creates -and writes its own PID to it. If it does, then the script checks the following: -if the file contains a PID and a process is running with that PID, then -the script dies; or, if there is no process running with that PID, then the -script overwrites the file with its own PID and starts; else, if the file -contains no PID, then the script dies. +Create the given PID file. The tool won't start if the PID file already +exists and the PID it contains is different than the current PID. However, +if the PID file exists and the PID it contains is no longer running, the +tool will overwrite the PID file with the current PID. The PID file is +removed automatically when the tool exits. =item --port diff --git a/docs/percona-toolkit.pod b/docs/percona-toolkit.pod index 0dbd6f03..0c63ee98 100644 --- a/docs/percona-toolkit.pod +++ b/docs/percona-toolkit.pod @@ -175,6 +175,34 @@ Format EXPLAIN output as a tree. For more free, open-source software developed Percona, visit L. +=head1 SPECIAL OPTION TYPES + +=over + +=item time + +Time values are seconds by default. For example, C<--run-time 60> means +60 seconds. Time values support an optional suffix: s (seconds), +m (minutes), h (hours), d (days). C<--run-time 1m> means 1 minute +(the same as 60 seconds). + +=item size + +Size values are bytes by default. For example, C<--disk-space-free 1024> +means 1 Kibibyte. Size values support an optional suffix: k (Kibibyte), +M (Mebibyte), G (Gibibyte). + +=item DSN + +See L<"DSN (DATA SOURCE NAME) SPECIFICATIONS">. + +=item Hash, hash, Array, array + +Hash, hash, Array, and array values are comma-separated lists of values. +For example, C<--ignore-tables foo,bar> ignores tables C and C. + +=back + =head1 CONFIGURATION FILES Percona Toolkit tools can read options from configuration files. The diff --git a/lib/Cxn.pm b/lib/Cxn.pm index 864abb3f..441f49be 100644 --- a/lib/Cxn.pm +++ b/lib/Cxn.pm @@ -98,23 +98,24 @@ sub new { } my $self = { - dsn => $dsn, - dbh => $args{dbh}, - dsn_name => $dp->as_string($dsn, [qw(h P S)]), - hostname => '', - set => $args{set}, - NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, - dbh_set => 0, - OptionParser => $o, - DSNParser => $dp, + dsn => $dsn, + dbh => $args{dbh}, + dsn_name => $dp->as_string($dsn, [qw(h P S)]), + hostname => '', + set => $args{set}, + NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1, + dbh_set => 0, + OptionParser => $o, + DSNParser => $dp, is_cluster_node => undef, + parent => $args{parent}, }; return bless $self, $class; } sub connect { - my ( $self ) = @_; + my ( $self, %opts ) = @_; my $dsn = $self->{dsn}; my $dp = $self->{DSNParser}; my $o = $self->{OptionParser}; @@ -126,11 +127,18 @@ sub connect { $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); $self->{asked_for_pass} = 1; } - $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit => 1 }); + $dbh = $dp->get_dbh( + $dp->get_cxn_params($dsn), + { + AutoCommit => 1, + %opts, + }, + ); } - PTDEBUG && _d($dbh, 'Connected dbh to', $self->{name}); - return $self->set_dbh($dbh); + $dbh = $self->set_dbh($dbh); + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name}); + return $dbh; } sub set_dbh { @@ -163,6 +171,11 @@ sub set_dbh { $self->{hostname} = $hostname; } + if ( $self->{parent} ) { + PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent'); + $dbh->{InactiveDestroy} = 1; + } + # Call the set callback to let the caller SET any MySQL variables. if ( my $set = $self->{set}) { $set->($dbh); @@ -173,6 +186,15 @@ sub set_dbh { return $dbh; } +sub lost_connection { + my ($self, $e) = @_; + return 0 unless $e; + return $e =~ m/MySQL server has gone away/ + || $e =~ m/Lost connection to MySQL server/; + # The 1st pattern means that MySQL itself died or was stopped. + # The 2nd pattern means that our cxn was killed (KILL ). +} + # Sub: dbh # Return the cxn's dbh. sub dbh { @@ -197,12 +219,21 @@ sub name { sub DESTROY { my ($self) = @_; - if ( $self->{dbh} - && blessed($self->{dbh}) - && $self->{dbh}->can("disconnect") ) { - PTDEBUG && _d('Disconnecting dbh', $self->{dbh}, $self->{name}); + + PTDEBUG && _d('Destroying cxn'); + + if ( $self->{parent} ) { + PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent'); + } + elsif ( $self->{dbh} + && blessed($self->{dbh}) + && $self->{dbh}->can("disconnect") ) + { + PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname}, + $self->{dsn_name}); $self->{dbh}->disconnect(); } + return; } diff --git a/lib/Runtime.pm b/lib/Runtime.pm index 18e266a8..64019682 100644 --- a/lib/Runtime.pm +++ b/lib/Runtime.pm @@ -18,13 +18,6 @@ # Runtime package # ########################################################################### { -# Package: Runtime -# Runtime keeps track of time to control how long a tool's main loop runs. -# This package was created to handle mk-query-digest --run-time-mode event. -# In essence, we abstract time so that the tool doesn't know/care whether -# now() comes from a clock, a log timestamp, or wherever. The creator of -# Runtime object determines how, or from where, time is gotten so that the -# caller of the object can simply ask, "What time is it?". package Runtime; use strict; @@ -32,30 +25,24 @@ use warnings FATAL => 'all'; use English qw(-no_match_vars); use constant PTDEBUG => $ENV{PTDEBUG} || 0; -# Sub: new -# -# Parameters: -# %args - Arguments -# -# Required Arguments: -# now - Callback that sets current time. -# runtime - Amount of time to run in seconds, or undef for forever. -# -# Returns: -# Runtime object sub new { my ( $class, %args ) = @_; - my @required_args = qw(now); + my @required_args = qw(run_time now); foreach my $arg ( @required_args ) { - die "I need a $arg argument" unless $args{$arg}; + die "I need a $arg argument" unless exists $args{$arg}; } - if ( ($args{runtime} || 0) < 0 ) { - die "runtime argument must be greater than zero" + my $run_time = $args{run_time}; + if ( defined $run_time ) { + die "run_time must be > 0" if $run_time <= 0; } + my $now = $args{now}; + die "now must be a callback" unless ref $now eq 'CODE'; + my $self = { - %args, + run_time => $run_time, + now => $now, start_time => undef, end_time => undef, time_left => undef, @@ -66,8 +53,8 @@ sub new { } # Sub: time_left -# Return the number of runtime seconds left or undef for forever. -# The return may be less than zero if the runtime has been exceeded. +# Return the number of run time seconds left or undef for forever. +# The return may be less than zero if the run time has been exceeded. # The first call to this subroutine "starts the clock", so to speak, # if the now callbackup returns a defined value. # @@ -75,7 +62,7 @@ sub new { # %args - Arguments passed to now callback. # # Returns: -# Number of runtime seconds left, possibly less than zero, or undef +# Number of run time seconds left, possibly less than zero, or undef # if running forever. sub time_left { my ( $self, %args ) = @_; @@ -99,14 +86,14 @@ sub time_left { # we know the current time. return unless defined $now; - # If runtime is also defined, then we can determine time left. + # If run_time is also defined, then we can determine time left. # If it's not defined, then we're running forever. - my $runtime = $self->{runtime}; - return unless defined $runtime; + my $run_time = $self->{run_time}; + return unless defined $run_time; # Set the end time once. if ( !$self->{end_time} ) { - $self->{end_time} = $now + $runtime; + $self->{end_time} = $now + $run_time; PTDEBUG && _d("End time:", $self->{end_time}); } @@ -118,7 +105,7 @@ sub time_left { } # Sub: have_time -# Return true or false if there's runtime left. This sub is a simpler +# Return true or false if there's run time left. This sub is a simpler # wrapper around which returns true (1) if time left is # defined and greater than zero or undef, else returns false. # @@ -131,7 +118,7 @@ sub have_time { my ( $self, %args ) = @_; my $time_left = $self->time_left(%args); return 1 if !defined $time_left; # run forever - return $time_left <= 0 ? 0 : 1; # <=0s means runtime has elapsed + return $time_left <= 0 ? 0 : 1; # <=0s means run time has elapsed } # Sub: time_elapsed @@ -173,7 +160,7 @@ sub reset { $self->{end_time} = undef; $self->{time_left} = undef; $self->{stop} = 0; - PTDEBUG && _d("Reset runtime"); + PTDEBUG && _d("Reset run time"); return; } diff --git a/t/lib/Cxn.t b/t/lib/Cxn.t index 75355b76..3a7a6a75 100644 --- a/t/lib/Cxn.t +++ b/t/lib/Cxn.t @@ -9,7 +9,7 @@ BEGIN { use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); -use Test::More tests => 19; +use Test::More; use Sandbox; use OptionParser; @@ -251,9 +251,65 @@ is_deeply( @ARGV = (); $o->get_opts(); +# ############################################################################# +# The parent of a forked Cxn should not disconnect the dbh in DESTORY +# because the child still has access to it. +# ############################################################################# + +my $sync_file = "/tmp/pt-cxn-sync.$PID"; +my $outfile = "/tmp/pt-cxn-outfile.$PID"; + +my $pid; +{ + my $parent_cxn = make_cxn( + dsn_string => 'h=127.1,P=12345,u=msandbox,p=msandbox', + parent => 1, + ); + $parent_cxn->connect(); + + $pid = fork(); + if ( defined($pid) && $pid == 0 ) { + # I am the child. + # Wait for the parent to leave this code block which will cause + # the $parent_cxn to be destroyed. + PerconaTest::wait_for_files($sync_file); + $parent_cxn->{parent} = 0; + eval { + $parent_cxn->dbh->do("SELECT 123 INTO OUTFILE '$outfile'"); + $parent_cxn->dbh->disconnect(); + }; + warn $EVAL_ERROR if $EVAL_ERROR; + exit; + } +} + +# Let the child know that we (the parent) have left that ^ code block, +# so our copy of $parent_cxn has been destroyed, but hopefully the child's +# copy is still alive, i.e. has an active/not-disconnected dbh. +diag(`touch $sync_file`); + +# Wait for the child. +waitpid($pid, 0); + +ok( + -f $outfile, + "Child created outfile" +); + +my $output = `cat $outfile 2>/dev/null`; + +is( + $output, + "123\n", + "Child executed query" +); + +unlink $sync_file if -f $sync_file; +unlink $outfile if -f $outfile; + # ############################################################################# # Done. # ############################################################################# $master_dbh->disconnect() if $master_dbh; ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); -exit; +done_testing; diff --git a/t/pt-deadlock-logger/basics.t b/t/pt-deadlock-logger/basics.t index 0ff5a980..1ac1edf1 100644 --- a/t/pt-deadlock-logger/basics.t +++ b/t/pt-deadlock-logger/basics.t @@ -11,6 +11,8 @@ use warnings FATAL => 'all'; use English qw(-no_match_vars); use Test::More; +$ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} = 1; + use PerconaTest; use Sandbox; require "$trunk/bin/pt-deadlock-logger"; @@ -25,8 +27,8 @@ if ( !$dbh1 || !$dbh2 ) { } my $output; -my $cnf = "/tmp/12345/my.sandbox.cnf"; -my $cmd = "$trunk/bin/pt-deadlock-logger -F $cnf h=127.1"; +my $dsn = $sb->dsn_for('master'); +my @args = ($dsn, qw(--iterations 1)); $dbh1->commit; $dbh2->commit; @@ -90,20 +92,29 @@ make_deadlock(); $output = $dbh1->selectrow_hashref('show /*!40101 engine*/ innodb status')->{status}; like($output, qr/WE ROLL BACK/, 'There was a deadlock'); -$output = `$cmd --print`; +$output = output( + sub { + pt_deadlock_logger::main(@args); + } +); + like( $output, qr/127\.1.+msandbox.+GEN_CLUST_INDEX/, 'Deadlock logger prints the output' ); -$output = `$cmd`; -like( - $output, - qr/127\.1.+msandbox.+GEN_CLUST_INDEX/, - '--print is implicit' +$output = output( + sub { + pt_deadlock_logger::main(@args, qw(--quiet)); + } ); +is( + $output, + "", + "No output with --quiet" +); # ############################################################################# # Issue 943: mk-deadlock-logger reports the same deadlock with --interval @@ -112,55 +123,59 @@ like( # The deadlock from above won't be re-printed so even after running for # 3 seconds and checking multiple times only the single, 3 line deadlock # should be reported. -chomp($output = `$cmd --run-time 3 | wc -l`); + +$output = output( + sub { + pt_deadlock_logger::main(@args, qw(--run-time 3)); + } +); $output =~ s/^\s+//; +my @lines = split("\n", $output); is( - $output, + scalar @lines, 3, "Doesn't re-print same deadlock (issue 943)" -); +) or diag($output); # ############################################################################# # Check that deadlocks from previous test were stored in table. # ############################################################################# -`$cmd --dest D=test,t=deadlocks --create-dest-table`; +$output = output( + sub { + pt_deadlock_logger::main(@args, '--dest', 'D=test,t=deadlocks', + qw(--create-dest-table)) + } +); + my $res = $dbh1->selectall_arrayref('SELECT * FROM test.deadlocks'); ok( scalar @$res, - 'Deadlocks recorded in --dest table' -); + 'Deadlock saved in --dest table' +) or diag($output); # ############################################################################# -# Check that --dest suppress --print output unless --print is explicit. +# In 2.1, --dest suppressed output (--print). In 2.2, output is only +# suppressed by --quiet. # ############################################################################# -$output = 'foo'; -$dbh1->do('TRUNCATE TABLE test.deadlocks'); -$output = `$cmd --dest D=test,t=deadlocks`; -is( - $output, - '', - 'No output with --dest' -); - -$res = $dbh1->selectall_arrayref('SELECT * FROM test.deadlocks'); -ok( - scalar @$res, - 'Deadlocks still recorded in table' -); - $output = ''; $dbh1->do('TRUNCATE TABLE test.deadlocks'); -$output = `$trunk/bin/pt-deadlock-logger --print --dest D=test,t=deadlocks --host 127.1 --port 12345 --user msandbox --password msandbox`; -like( +$output = output( + sub { + pt_deadlock_logger::main(@args, '--dest', 'D=test,t=deadlocks', + qw(--quiet)) + } +); + +is( $output, - qr/127\.1.+msandbox.+GEN_CLUST_INDEX/, - 'Prints output with --dest and explicit --print' + "", + "No output with --dest and --quiet" ); $res = $dbh1->selectall_arrayref('SELECT * FROM test.deadlocks'); ok( scalar @$res, - 'Deadlocks recorded in table again' + "... deadlock still saved in the table" ); # ############################################################################# @@ -180,9 +195,7 @@ SKIP: { make_deadlock(); $output = output( - sub { pt_deadlock_logger::main("F=/tmp/12345/my.sandbox.cnf", - qw(--print) ); - } + sub { pt_deadlock_logger::main(@args) } ); like( @@ -200,4 +213,3 @@ $dbh2->commit; $sb->wipe_clean($dbh1); ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); done_testing; -exit; diff --git a/t/pt-deadlock-logger/clear_deadlocks.t b/t/pt-deadlock-logger/clear_deadlocks.t index 5141e910..07e7ab32 100644 --- a/t/pt-deadlock-logger/clear_deadlocks.t +++ b/t/pt-deadlock-logger/clear_deadlocks.t @@ -22,9 +22,6 @@ my $dbh1 = $sb->get_dbh_for('master'); if ( !$dbh1 ) { plan skip_all => 'Cannot connect to sandbox master'; } -else { - plan tests => 4; -} my $output; my $cnf = "/tmp/12345/my.sandbox.cnf"; @@ -39,7 +36,7 @@ $sb->create_dbs($dbh1, ['test']); # The clear-deadlocks table comes and goes quickly so we can really # only search the debug output for evidence that it was created. -$output = `PTDEBUG=1 $trunk/bin/pt-deadlock-logger F=$cnf,D=test --clear-deadlocks test.make_deadlock 2>&1`; +$output = `PTDEBUG=1 $trunk/bin/pt-deadlock-logger F=$cnf,D=test --clear-deadlocks test.make_deadlock --iterations 1 2>&1`; like( $output, qr/INSERT INTO test.make_deadlock/, @@ -67,4 +64,4 @@ like( # ############################################################################# $sb->wipe_clean($dbh1); ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); -exit; +done_testing; diff --git a/t/pt-deadlock-logger/create_dest_table.t b/t/pt-deadlock-logger/create_dest_table.t index e285590a..5280d8f8 100644 --- a/t/pt-deadlock-logger/create_dest_table.t +++ b/t/pt-deadlock-logger/create_dest_table.t @@ -22,15 +22,10 @@ my $dbh1 = $sb->get_dbh_for('master'); if ( !$dbh1 ) { plan skip_all => 'Cannot connect to sandbox master'; } -else { - plan tests => 3; -} my $output; -my $cnf = "/tmp/12345/my.sandbox.cnf"; -my $cmd = "$trunk/bin/pt-deadlock-logger -F $cnf h=127.1"; +my $dsn = $sb->dsn_for('master'); -$sb->wipe_clean($dbh1); $sb->create_dbs($dbh1, ['test']); # ############################################################################# @@ -42,17 +37,25 @@ is_deeply( 'Deadlocks table does not exit (issue 386)' ); -`$cmd --dest D=test,t=issue_386 --run-time 1s --interval 1s --create-dest-table`; +$output = output( + sub { + pt_deadlock_logger::main($dsn, + '--dest', 'D=test,t=issue_386', + qw(--iterations 1 --create-dest-table) + ) + }, + stderr => 1, +); is_deeply( $dbh1->selectall_arrayref(q{show tables from `test` like 'issue_386'}), [['issue_386']], 'Deadlocks table created with --create-dest-table (issue 386)' -); +) or diag($output); # ############################################################################# # Done. # ############################################################################# $sb->wipe_clean($dbh1); ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); -exit; +done_testing; diff --git a/t/pt-deadlock-logger/option_sanity.t b/t/pt-deadlock-logger/option_sanity.t index ce75689c..f8376d26 100644 --- a/t/pt-deadlock-logger/option_sanity.t +++ b/t/pt-deadlock-logger/option_sanity.t @@ -21,7 +21,7 @@ my $output; $output = `$trunk/bin/pt-deadlock-logger --dest D=test,t=deadlocks 2>&1`; like( $output, - qr/Missing or invalid source host/, + qr/No DSN was specified/, 'Requires source host' ); diff --git a/t/pt-deadlock-logger/deadlocks_tbl.sql b/t/pt-deadlock-logger/samples/deadlocks_tbl.sql similarity index 100% rename from t/pt-deadlock-logger/deadlocks_tbl.sql rename to t/pt-deadlock-logger/samples/deadlocks_tbl.sql diff --git a/t/pt-deadlock-logger/standard_options.t b/t/pt-deadlock-logger/standard_options.t index 7b770008..4c6b10ec 100644 --- a/t/pt-deadlock-logger/standard_options.t +++ b/t/pt-deadlock-logger/standard_options.t @@ -17,69 +17,96 @@ require "$trunk/bin/pt-deadlock-logger"; my $dp = new DSNParser(opts=>$dsn_opts); my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp); -my $dbh1 = $sb->get_dbh_for('master'); +my $dbh = $sb->get_dbh_for('master'); -if ( !$dbh1 ) { +if ( !$dbh ) { plan skip_all => 'Cannot connect to sandbox master'; } -else { - plan tests => 10; -} my $output; -my $cnf = "/tmp/12345/my.sandbox.cnf"; -my $cmd = "$trunk/bin/pt-deadlock-logger -F $cnf h=127.1"; +my $dsn = $sb->dsn_for('master'); +my @args = ($dsn, qw(--iterations 1)); -$sb->wipe_clean($dbh1); -$sb->create_dbs($dbh1, ['test']); +$sb->wipe_clean($dbh); +$sb->create_dbs($dbh, ['test']); # ############################################################################# # Issue 248: Add --user, --pass, --host, etc to all tools # ############################################################################# # Test that source DSN inherits from --user, etc. -$output = `$trunk/bin/pt-deadlock-logger h=127.1,D=test,u=msandbox,p=msandbox --clear-deadlocks test.make_deadlock --port 12345 2>&1`; +$output = output( + sub { + pt_deadlock_logger::main( + "h=127.1,D=test,u=msandbox,p=msandbox", + qw(--clear-deadlocks test.make_deadlock --port 12345), + qw(--iterations 1) + ) + } +); + unlike( $output, qr/failed/, 'Source DSN inherits from standard connection options (issue 248)' ); -# ######################################################################### +# ############################################################################# # Issue 391: Add --pid option to all scripts -# ######################################################################### -`touch /tmp/mk-script.pid`; -$output = `$cmd --clear-deadlocks test.make_deadlock --port 12345 --pid /tmp/mk-script.pid 2>&1`; +# ############################################################################# + +my $pid_file = "/tmp/pt-deadlock-logger-test.pid.$PID"; +diag(`touch $pid_file`); + +$output = output( + sub { + pt_deadlock_logger::main(@args, '--pid', $pid_file) + }, + stderr => 1, +); + like( $output, - qr{PID file /tmp/mk-script.pid already exists}, + qr{PID file $pid_file already exists}, 'Dies if PID file already exists (--pid without --daemonize) (issue 391)' ); -`rm -rf /tmp/mk-script.pid`; + +unlink $pid_file if -f $pid_file; # ############################################################################# # Check daemonization # ############################################################################# -my $deadlocks_tbl = load_file('t/pt-deadlock-logger/deadlocks_tbl.sql'); -$dbh1->do('USE test'); -$dbh1->do('DROP TABLE IF EXISTS deadlocks'); -$dbh1->do("$deadlocks_tbl"); +$dbh->do('USE test'); +$dbh->do('DROP TABLE IF EXISTS deadlocks'); +$sb->load_file('master', 't/pt-deadlock-logger/samples/deadlocks_tbl.sql', 'test'); -my $pid_file = '/tmp/mk-deadlock-logger.pid'; -unlink $pid_file - and diag("Unlinked existing $pid_file"); - -`$cmd --dest D=test,t=deadlocks --daemonize --run-time 6s --interval 1s --pid $pid_file 1>/dev/null 2>/dev/null`; -$output = `ps -eaf | grep '$cmd \-\-dest '`; -like($output, qr/\Q$cmd/, 'It lives daemonized'); +$output = `$trunk/bin/pt-deadlock-logger $dsn --dest D=test,t=deadlocks --daemonize --run-time 10 --interval 1 --pid $pid_file 1>/dev/null 2>/dev/null`; PerconaTest::wait_for_files($pid_file); -ok(-f $pid_file, 'PID file created'); -my ($pid) = $output =~ /\s+(\d+)\s+/; + +$output = `ps x | grep 'pt-deadlock-logger $dsn' | grep -v grep`; +like( + $output, + qr/\Qpt-deadlock-logger $dsn/, + 'It lives daemonized' +) or diag($output); + +my ($pid) = $output =~ /(\d+)/; + +ok( + -f $pid_file, + 'PID file created' +) or diag($output); + chomp($output = slurp_file($pid_file)); -is($output, $pid, 'PID file has correct PID'); +is( + $output, + $pid, + 'PID file has correct PID' +); # Kill it +kill 2, $pid; PerconaTest::wait_until(sub { !kill 0, $pid }); ok(! -f $pid_file, 'PID file removed'); @@ -90,17 +117,25 @@ ok( 'PID file already exists' ); -$output = `$cmd --dest D=test,t=deadlocks --daemonize --run-time 1s --interval 1s --pid $pid_file 2>&1`; +$output = output( + sub { + pt_deadlock_logger::main(@args, '--pid', $pid_file, + qw(--daemonize)) + }, + stderr => 1, +); + like( $output, - qr/PID file .+ already exists/, + qr/PID file $pid_file already exists/, 'Does not run if PID file already exists' ); -$output = `ps -eaf | grep 'pt-deadlock-logger \-\-dest '`; -unlike( +$output = `ps x | grep 'pt-deadlock-logger $dsn' | grep -v grep`; + +is( $output, - qr/$cmd/, + "", 'It does not lived daemonized' ); @@ -109,6 +144,6 @@ unlink $pid_file; # ############################################################################# # Done. # ############################################################################# -$sb->wipe_clean($dbh1); +$sb->wipe_clean($dbh); ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); -exit; +done_testing; diff --git a/t/pt-fk-error-logger/basics.t b/t/pt-fk-error-logger/basics.t index 58cadbdc..f24e8818 100644 --- a/t/pt-fk-error-logger/basics.t +++ b/t/pt-fk-error-logger/basics.t @@ -27,8 +27,9 @@ if ( !$dbh ) { $sb->create_dbs($dbh, [qw(test)]); my $output; -my $cnf = '/tmp/12345/my.sandbox.cnf'; -my $cmd = "$trunk/bin/pt-fk-error-logger -F $cnf "; +my $cnf = '/tmp/12345/my.sandbox.cnf'; +my $cmd = "$trunk/bin/pt-fk-error-logger -F $cnf "; +my @args = qw(--iterations 1); $sb->load_file('master', 't/pt-fk-error-logger/samples/fke_tbl.sql', 'test'); @@ -39,8 +40,45 @@ $sb->load_file('master', 't/pt-fk-error-logger/samples/fke_tbl.sql', 'test'); # First, create a foreign key error. `/tmp/12345/use -D test < $trunk/t/pt-fk-error-logger/samples/fke.sql 1>/dev/null 2>/dev/null`; -# Then get and save that fke. -output(sub { pt_fk_error_logger::main('h=127.1,P=12345,u=msandbox,p=msandbox', '--dest', 'h=127.1,P=12345,D=test,t=foreign_key_errors'); } ); +$output = output( + sub { + pt_fk_error_logger::main(@args, 'h=127.1,P=12345,u=msandbox,p=msandbox'), + } +); + +like( + $output, + qr/Foreign key constraint fails/, + "Prints fk error by default" +); + +$output = output( + sub { + pt_fk_error_logger::main(@args, 'h=127.1,P=12345,u=msandbox,p=msandbox', + qw(--quiet)) + } +); + +is( + $output, + "", + "No output with --quiet" +); + + +# ############################################################################# +# --dest +# ############################################################################# + +$output = output( + sub { + pt_fk_error_logger::main(@args, + 'h=127.1,P=12345,u=msandbox,p=msandbox', + '--dest', 'h=127.1,P=12345,D=test,t=foreign_key_errors', + ) + } +); + sleep 0.1; # And then test that it was actually saved. @@ -61,7 +99,7 @@ like( # Check again to make sure that the same fke isn't saved twice. my $first_ts = $fke->[0]->[0]; -output(sub { pt_fk_error_logger::main('h=127.1,P=12345,u=msandbox,p=msandbox', '--dest', 'h=127.1,P=12345,D=test,t=foreign_key_errors'); } ); +output(sub { pt_fk_error_logger::main(@args, 'h=127.1,P=12345,u=msandbox,p=msandbox', '--dest', 'h=127.1,P=12345,D=test,t=foreign_key_errors'); } ); sleep 0.1; $fke = $dbh->selectall_arrayref('SELECT * FROM test.foreign_key_errors'); is( @@ -82,7 +120,7 @@ $dbh->do('INSERT INTO child VALUES (1, 2)'); eval { $dbh->do('DELETE FROM parent WHERE id = 2'); # Causes foreign key error. }; -output( sub { pt_fk_error_logger::main('h=127.1,P=12345,u=msandbox,p=msandbox', '--dest', 'h=127.1,P=12345,D=test,t=foreign_key_errors'); } ); +output( sub { pt_fk_error_logger::main(@args, 'h=127.1,P=12345,u=msandbox,p=msandbox', '--dest', 'h=127.1,P=12345,D=test,t=foreign_key_errors'); } ); sleep 0.1; $fke = $dbh->selectall_arrayref('SELECT * FROM test.foreign_key_errors'); like( @@ -99,11 +137,14 @@ is( # ########################################################################## # Test printing the errors. # ########################################################################## + $dbh->do('USE test'); eval { $dbh->do('DELETE FROM parent WHERE id = 2'); # Causes foreign key error. }; -$output = output(sub { pt_fk_error_logger::main('h=127.1,P=12345,u=msandbox,p=msandbox'); }); + +$output = output(sub { pt_fk_error_logger::main(@args, 'h=127.1,P=12345,u=msandbox,p=msandbox'); }); + like( $output, qr/DELETE FROM parent WHERE id = 2/, @@ -127,7 +168,7 @@ $sb->load_file('master1', 't/pt-fk-error-logger/samples/fke_tbl.sql', 'test'); $output = output( sub { - pt_fk_error_logger::main('h=127.1,P=12348,u=msandbox,p=msandbox', + pt_fk_error_logger::main(@args, 'h=127.1,P=12348,u=msandbox,p=msandbox', '--dest', 'h=127.1,P=12348,D=test,t=foreign_key_errors') }, stderr => 1, @@ -141,6 +182,23 @@ is( diag(`$trunk/sandbox/stop-sandbox 12348 >/dev/null`); +# ############################################################################# +# Test --pid +# ############################################################################# + +my $pid_file = "/tmp/pt-fk-error-log-test-$PID.pid"; +diag(`touch $pid_file`); + +$output = `$trunk/bin/pt-fk-error-logger h=127.1,P=12345,u=msandbox,p=msandbox --pid $pid_file --iterations 1 2>&1`; + +like( + $output, + qr{PID file $pid_file already exists}, + 'Dies if PID file already exists (--pid without --daemonize) (issue 391)' +); + +unlink $pid_file; + # ############################################################################# # Done. # ############################################################################# diff --git a/t/pt-fk-error-logger/get_fk_error.t b/t/pt-fk-error-logger/get_fk_error.t index df90626d..5d8b6ab5 100644 --- a/t/pt-fk-error-logger/get_fk_error.t +++ b/t/pt-fk-error-logger/get_fk_error.t @@ -9,7 +9,7 @@ BEGIN { use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); -use Test::More tests => 10; +use Test::More; use PerconaTest; require "$trunk/bin/pt-fk-error-logger"; @@ -70,4 +70,4 @@ test_get_fk_error( # ############################################################################# # Done. # ############################################################################# -exit; +done_testing; diff --git a/t/pt-fk-error-logger/standard_options.t b/t/pt-fk-error-logger/standard_options.t deleted file mode 100644 index 57bfd18d..00000000 --- a/t/pt-fk-error-logger/standard_options.t +++ /dev/null @@ -1,32 +0,0 @@ -#!/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 tests => 1; - -use PerconaTest; -require "$trunk/bin/pt-fk-error-logger"; - -# ######################################################################### -# Issue 391: Add --pid option to all scripts -# ######################################################################### -`touch /tmp/mk-script.pid`; -my $output = `$trunk/bin/pt-fk-error-logger h=127.1,P=12345,u=msandbox,p=msandbox --print --pid /tmp/mk-script.pid 2>&1`; -like( - $output, - qr{PID file /tmp/mk-script.pid already exists}, - 'Dies if PID file already exists (--pid without --daemonize) (issue 391)' -); -`rm -rf /tmp/mk-script.pid`; - -# ############################################################################# -# Done. -# ############################################################################# -exit;