Merge lp:~percona-toolkit-dev/percona-toolkit/pt-kill-log-dsn into lp:percona-toolkit/2.1

Proposed by Daniel Nichter
Status: Merged
Approved by: Daniel Nichter
Approved revision: 315
Merged at revision: 315
Proposed branch: lp:~percona-toolkit-dev/percona-toolkit/pt-kill-log-dsn
Merge into: lp:percona-toolkit/2.1
Diff against target: 1155 lines (+915/-33)
4 files modified
bin/pt-kill (+720/-7)
lib/Processlist.pm (+16/-3)
t/lib/Processlist.t (+23/-21)
t/pt-kill/kill.t (+156/-2)
To merge this branch: bzr merge lp:~percona-toolkit-dev/percona-toolkit/pt-kill-log-dsn
Reviewer Review Type Date Requested Status
Daniel Nichter Approve
Review via email: mp+114910@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Daniel Nichter (daniel-nichter) :
review: Needs Fixing
Revision history for this message
Daniel Nichter (daniel-nichter) wrote :

Changes for

bug 941469

branch https://code.launchpad.net/~percona-toolkit-dev/percona-toolkit/pt-kill-reconnect-bug-941469

merge https://code.launchpad.net/~percona-toolkit-dev/percona-toolkit/pt-kill-reconnect-bug-941469/+merge/114748

conflict with the --log-dsn code. The code needs to be updated like those ^ changes, i.e. use Retry to try doing the INSERT, if that fails, reconnect and try again. I would say: tries=20, wait 3s (i.e. 1 minute). MySQL shouldn't stay away for long if the code just observed it, and if an INSERT fails that many times, it's no big deal, but it's worth making a good effort.

Also, please standardize the tests:

* Use English and indention,

is(
   $EVAL_ERROR,
   "",
   "foo"
);

* Be more explicit, e.g.:

   my $result = shift @$results;
   $result->[7] =~ s/localhost:[0-9]+/localhost/;
   is_deeply(
      [ @{$result}[6..9, 11, 12] ],

That's cryptic. Rather:

my $row = $dbh->selectrow_hashref($sql);
is_deeply(
   $row,
   {
      Id => 123,
      user => 'foo',
      ...
   },
   "..."
) or diag(Dumper($row));

313. By Brian Fraser

t/pt-kill/kill.t: Make a test 5.0 compatible

314. By Daniel Nichter

Move certain vars to outer scope to avoid Perl 5.8 scoping bug.

315. By Brian Fraser

Really make a test 5.0 compatible

Revision history for this message
Daniel Nichter (daniel-nichter) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/pt-kill'
2--- bin/pt-kill 2012-07-15 02:58:17 +0000
3+++ bin/pt-kill 2012-07-19 16:42:35 +0000
4@@ -1280,7 +1280,7 @@
5 }
6
7 foreach my $key ( keys %given_props ) {
8- die "Unknown DSN option '$key' in '$dsn'. For more details, "
9+ die "DSN option '$key' in '$dsn'. For more details, "
10 . "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
11 . "for complete documentation."
12 unless exists $opts->{$key};
13@@ -2087,6 +2087,436 @@
14 # ###########################################################################
15
16 # ###########################################################################
17+# TableParser package
18+# This package is a copy without comments from the original. The original
19+# with comments and its test file can be found in the Bazaar repository at,
20+# lib/TableParser.pm
21+# t/lib/TableParser.t
22+# See https://launchpad.net/percona-toolkit for more information.
23+# ###########################################################################
24+{
25+package TableParser;
26+
27+use strict;
28+use warnings FATAL => 'all';
29+use English qw(-no_match_vars);
30+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
31+
32+use Data::Dumper;
33+$Data::Dumper::Indent = 1;
34+$Data::Dumper::Sortkeys = 1;
35+$Data::Dumper::Quotekeys = 0;
36+
37+sub new {
38+ my ( $class, %args ) = @_;
39+ my @required_args = qw(Quoter);
40+ foreach my $arg ( @required_args ) {
41+ die "I need a $arg argument" unless $args{$arg};
42+ }
43+ my $self = { %args };
44+ return bless $self, $class;
45+}
46+
47+sub get_create_table {
48+ my ( $self, $dbh, $db, $tbl ) = @_;
49+ die "I need a dbh parameter" unless $dbh;
50+ die "I need a db parameter" unless $db;
51+ die "I need a tbl parameter" unless $tbl;
52+ my $q = $self->{Quoter};
53+
54+ my $new_sql_mode
55+ = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, '
56+ . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), }
57+ . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, '
58+ . '@@SQL_QUOTE_SHOW_CREATE := 1 */';
59+
60+ my $old_sql_mode = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, '
61+ . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */';
62+
63+ PTDEBUG && _d($new_sql_mode);
64+ eval { $dbh->do($new_sql_mode); };
65+ PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
66+
67+ my $use_sql = 'USE ' . $q->quote($db);
68+ PTDEBUG && _d($dbh, $use_sql);
69+ $dbh->do($use_sql);
70+
71+ my $show_sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl);
72+ PTDEBUG && _d($show_sql);
73+ my $href;
74+ eval { $href = $dbh->selectrow_hashref($show_sql); };
75+ if ( $EVAL_ERROR ) {
76+ PTDEBUG && _d($EVAL_ERROR);
77+
78+ PTDEBUG && _d($old_sql_mode);
79+ $dbh->do($old_sql_mode);
80+
81+ return;
82+ }
83+
84+ PTDEBUG && _d($old_sql_mode);
85+ $dbh->do($old_sql_mode);
86+
87+ my ($key) = grep { m/create (?:table|view)/i } keys %$href;
88+ if ( !$key ) {
89+ die "Error: no 'Create Table' or 'Create View' in result set from "
90+ . "$show_sql: " . Dumper($href);
91+ }
92+
93+ return $href->{$key};
94+}
95+
96+sub parse {
97+ my ( $self, $ddl, $opts ) = @_;
98+ return unless $ddl;
99+
100+ if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {
101+ die "Cannot parse table definition; is ANSI quoting "
102+ . "enabled or SQL_QUOTE_SHOW_CREATE disabled?";
103+ }
104+
105+ my ($name) = $ddl =~ m/CREATE (?:TEMPORARY )?TABLE\s+(`.+?`)/;
106+ (undef, $name) = $self->{Quoter}->split_unquote($name) if $name;
107+
108+ $ddl =~ s/(`[^`]+`)/\L$1/g;
109+
110+ my $engine = $self->get_engine($ddl);
111+
112+ my @defs = $ddl =~ m/^(\s+`.*?),?$/gm;
113+ my @cols = map { $_ =~ m/`([^`]+)`/ } @defs;
114+ PTDEBUG && _d('Table cols:', join(', ', map { "`$_`" } @cols));
115+
116+ my %def_for;
117+ @def_for{@cols} = @defs;
118+
119+ my (@nums, @null);
120+ my (%type_for, %is_nullable, %is_numeric, %is_autoinc);
121+ foreach my $col ( @cols ) {
122+ my $def = $def_for{$col};
123+ my ( $type ) = $def =~ m/`[^`]+`\s([a-z]+)/;
124+ die "Can't determine column type for $def" unless $type;
125+ $type_for{$col} = $type;
126+ if ( $type =~ m/(?:(?:tiny|big|medium|small)?int|float|double|decimal|year)/ ) {
127+ push @nums, $col;
128+ $is_numeric{$col} = 1;
129+ }
130+ if ( $def !~ m/NOT NULL/ ) {
131+ push @null, $col;
132+ $is_nullable{$col} = 1;
133+ }
134+ $is_autoinc{$col} = $def =~ m/AUTO_INCREMENT/i ? 1 : 0;
135+ }
136+
137+ my ($keys, $clustered_key) = $self->get_keys($ddl, $opts, \%is_nullable);
138+
139+ my ($charset) = $ddl =~ m/DEFAULT CHARSET=(\w+)/;
140+
141+ return {
142+ name => $name,
143+ cols => \@cols,
144+ col_posn => { map { $cols[$_] => $_ } 0..$#cols },
145+ is_col => { map { $_ => 1 } @cols },
146+ null_cols => \@null,
147+ is_nullable => \%is_nullable,
148+ is_autoinc => \%is_autoinc,
149+ clustered_key => $clustered_key,
150+ keys => $keys,
151+ defs => \%def_for,
152+ numeric_cols => \@nums,
153+ is_numeric => \%is_numeric,
154+ engine => $engine,
155+ type_for => \%type_for,
156+ charset => $charset,
157+ };
158+}
159+
160+sub sort_indexes {
161+ my ( $self, $tbl ) = @_;
162+
163+ my @indexes
164+ = sort {
165+ (($a ne 'PRIMARY') <=> ($b ne 'PRIMARY'))
166+ || ( !$tbl->{keys}->{$a}->{is_unique} <=> !$tbl->{keys}->{$b}->{is_unique} )
167+ || ( $tbl->{keys}->{$a}->{is_nullable} <=> $tbl->{keys}->{$b}->{is_nullable} )
168+ || ( scalar(@{$tbl->{keys}->{$a}->{cols}}) <=> scalar(@{$tbl->{keys}->{$b}->{cols}}) )
169+ }
170+ grep {
171+ $tbl->{keys}->{$_}->{type} eq 'BTREE'
172+ }
173+ sort keys %{$tbl->{keys}};
174+
175+ PTDEBUG && _d('Indexes sorted best-first:', join(', ', @indexes));
176+ return @indexes;
177+}
178+
179+sub find_best_index {
180+ my ( $self, $tbl, $index ) = @_;
181+ my $best;
182+ if ( $index ) {
183+ ($best) = grep { uc $_ eq uc $index } keys %{$tbl->{keys}};
184+ }
185+ if ( !$best ) {
186+ if ( $index ) {
187+ die "Index '$index' does not exist in table";
188+ }
189+ else {
190+ ($best) = $self->sort_indexes($tbl);
191+ }
192+ }
193+ PTDEBUG && _d('Best index found is', $best);
194+ return $best;
195+}
196+
197+sub find_possible_keys {
198+ my ( $self, $dbh, $database, $table, $quoter, $where ) = @_;
199+ return () unless $where;
200+ my $sql = 'EXPLAIN SELECT * FROM ' . $quoter->quote($database, $table)
201+ . ' WHERE ' . $where;
202+ PTDEBUG && _d($sql);
203+ my $expl = $dbh->selectrow_hashref($sql);
204+ $expl = { map { lc($_) => $expl->{$_} } keys %$expl };
205+ if ( $expl->{possible_keys} ) {
206+ PTDEBUG && _d('possible_keys =', $expl->{possible_keys});
207+ my @candidates = split(',', $expl->{possible_keys});
208+ my %possible = map { $_ => 1 } @candidates;
209+ if ( $expl->{key} ) {
210+ PTDEBUG && _d('MySQL chose', $expl->{key});
211+ unshift @candidates, grep { $possible{$_} } split(',', $expl->{key});
212+ PTDEBUG && _d('Before deduping:', join(', ', @candidates));
213+ my %seen;
214+ @candidates = grep { !$seen{$_}++ } @candidates;
215+ }
216+ PTDEBUG && _d('Final list:', join(', ', @candidates));
217+ return @candidates;
218+ }
219+ else {
220+ PTDEBUG && _d('No keys in possible_keys');
221+ return ();
222+ }
223+}
224+
225+sub check_table {
226+ my ( $self, %args ) = @_;
227+ my @required_args = qw(dbh db tbl);
228+ foreach my $arg ( @required_args ) {
229+ die "I need a $arg argument" unless $args{$arg};
230+ }
231+ my ($dbh, $db, $tbl) = @args{@required_args};
232+ my $q = $self->{Quoter};
233+ my $db_tbl = $q->quote($db, $tbl);
234+ PTDEBUG && _d('Checking', $db_tbl);
235+
236+ my $sql = "SHOW TABLES FROM " . $q->quote($db)
237+ . ' LIKE ' . $q->literal_like($tbl);
238+ PTDEBUG && _d($sql);
239+ my $row;
240+ eval {
241+ $row = $dbh->selectrow_arrayref($sql);
242+ };
243+ if ( $EVAL_ERROR ) {
244+ PTDEBUG && _d($EVAL_ERROR);
245+ return 0;
246+ }
247+ if ( !$row->[0] || $row->[0] ne $tbl ) {
248+ PTDEBUG && _d('Table does not exist');
249+ return 0;
250+ }
251+
252+ PTDEBUG && _d('Table exists; no privs to check');
253+ return 1 unless $args{all_privs};
254+
255+ $sql = "SHOW FULL COLUMNS FROM $db_tbl";
256+ PTDEBUG && _d($sql);
257+ eval {
258+ $row = $dbh->selectrow_hashref($sql);
259+ };
260+ if ( $EVAL_ERROR ) {
261+ PTDEBUG && _d($EVAL_ERROR);
262+ return 0;
263+ }
264+ if ( !scalar keys %$row ) {
265+ PTDEBUG && _d('Table has no columns:', Dumper($row));
266+ return 0;
267+ }
268+ my $privs = $row->{privileges} || $row->{Privileges};
269+
270+ $sql = "DELETE FROM $db_tbl LIMIT 0";
271+ PTDEBUG && _d($sql);
272+ eval {
273+ $dbh->do($sql);
274+ };
275+ my $can_delete = $EVAL_ERROR ? 0 : 1;
276+
277+ PTDEBUG && _d('User privs on', $db_tbl, ':', $privs,
278+ ($can_delete ? 'delete' : ''));
279+
280+ if ( !($privs =~ m/select/ && $privs =~ m/insert/ && $privs =~ m/update/
281+ && $can_delete) ) {
282+ PTDEBUG && _d('User does not have all privs');
283+ return 0;
284+ }
285+
286+ PTDEBUG && _d('User has all privs');
287+ return 1;
288+}
289+
290+sub get_engine {
291+ my ( $self, $ddl, $opts ) = @_;
292+ my ( $engine ) = $ddl =~ m/\).*?(?:ENGINE|TYPE)=(\w+)/;
293+ PTDEBUG && _d('Storage engine:', $engine);
294+ return $engine || undef;
295+}
296+
297+sub get_keys {
298+ my ( $self, $ddl, $opts, $is_nullable ) = @_;
299+ my $engine = $self->get_engine($ddl);
300+ my $keys = {};
301+ my $clustered_key = undef;
302+
303+ KEY:
304+ foreach my $key ( $ddl =~ m/^ ((?:[A-Z]+ )?KEY .*)$/gm ) {
305+
306+ next KEY if $key =~ m/FOREIGN/;
307+
308+ my $key_ddl = $key;
309+ PTDEBUG && _d('Parsed key:', $key_ddl);
310+
311+ if ( $engine !~ m/MEMORY|HEAP/ ) {
312+ $key =~ s/USING HASH/USING BTREE/;
313+ }
314+
315+ my ( $type, $cols ) = $key =~ m/(?:USING (\w+))? \((.+)\)/;
316+ my ( $special ) = $key =~ m/(FULLTEXT|SPATIAL)/;
317+ $type = $type || $special || 'BTREE';
318+ if ( $opts->{mysql_version} && $opts->{mysql_version} lt '004001000'
319+ && $engine =~ m/HEAP|MEMORY/i )
320+ {
321+ $type = 'HASH'; # MySQL pre-4.1 supports only HASH indexes on HEAP
322+ }
323+
324+ my ($name) = $key =~ m/(PRIMARY|`[^`]*`)/;
325+ my $unique = $key =~ m/PRIMARY|UNIQUE/ ? 1 : 0;
326+ my @cols;
327+ my @col_prefixes;
328+ foreach my $col_def ( $cols =~ m/`[^`]+`(?:\(\d+\))?/g ) {
329+ my ($name, $prefix) = $col_def =~ m/`([^`]+)`(?:\((\d+)\))?/;
330+ push @cols, $name;
331+ push @col_prefixes, $prefix;
332+ }
333+ $name =~ s/`//g;
334+
335+ PTDEBUG && _d( $name, 'key cols:', join(', ', map { "`$_`" } @cols));
336+
337+ $keys->{$name} = {
338+ name => $name,
339+ type => $type,
340+ colnames => $cols,
341+ cols => \@cols,
342+ col_prefixes => \@col_prefixes,
343+ is_unique => $unique,
344+ is_nullable => scalar(grep { $is_nullable->{$_} } @cols),
345+ is_col => { map { $_ => 1 } @cols },
346+ ddl => $key_ddl,
347+ };
348+
349+ if ( $engine =~ m/InnoDB/i && !$clustered_key ) {
350+ my $this_key = $keys->{$name};
351+ if ( $this_key->{name} eq 'PRIMARY' ) {
352+ $clustered_key = 'PRIMARY';
353+ }
354+ elsif ( $this_key->{is_unique} && !$this_key->{is_nullable} ) {
355+ $clustered_key = $this_key->{name};
356+ }
357+ PTDEBUG && $clustered_key && _d('This key is the clustered key');
358+ }
359+ }
360+
361+ return $keys, $clustered_key;
362+}
363+
364+sub get_fks {
365+ my ( $self, $ddl, $opts ) = @_;
366+ my $q = $self->{Quoter};
367+ my $fks = {};
368+
369+ foreach my $fk (
370+ $ddl =~ m/CONSTRAINT .* FOREIGN KEY .* REFERENCES [^\)]*\)/mg )
371+ {
372+ my ( $name ) = $fk =~ m/CONSTRAINT `(.*?)`/;
373+ my ( $cols ) = $fk =~ m/FOREIGN KEY \(([^\)]+)\)/;
374+ my ( $parent, $parent_cols ) = $fk =~ m/REFERENCES (\S+) \(([^\)]+)\)/;
375+
376+ my ($db, $tbl) = $q->split_unquote($parent, $opts->{database});
377+ my %parent_tbl = (tbl => $tbl);
378+ $parent_tbl{db} = $db if $db;
379+
380+ if ( $parent !~ m/\./ && $opts->{database} ) {
381+ $parent = $q->quote($opts->{database}) . ".$parent";
382+ }
383+
384+ $fks->{$name} = {
385+ name => $name,
386+ colnames => $cols,
387+ cols => [ map { s/[ `]+//g; $_; } split(',', $cols) ],
388+ parent_tbl => \%parent_tbl,
389+ parent_tblname => $parent,
390+ parent_cols => [ map { s/[ `]+//g; $_; } split(',', $parent_cols) ],
391+ parent_colnames=> $parent_cols,
392+ ddl => $fk,
393+ };
394+ }
395+
396+ return $fks;
397+}
398+
399+sub remove_auto_increment {
400+ my ( $self, $ddl ) = @_;
401+ $ddl =~ s/(^\).*?) AUTO_INCREMENT=\d+\b/$1/m;
402+ return $ddl;
403+}
404+
405+sub get_table_status {
406+ my ( $self, $dbh, $db, $like ) = @_;
407+ my $q = $self->{Quoter};
408+ my $sql = "SHOW TABLE STATUS FROM " . $q->quote($db);
409+ my @params;
410+ if ( $like ) {
411+ $sql .= ' LIKE ?';
412+ push @params, $like;
413+ }
414+ PTDEBUG && _d($sql, @params);
415+ my $sth = $dbh->prepare($sql);
416+ eval { $sth->execute(@params); };
417+ if ($EVAL_ERROR) {
418+ PTDEBUG && _d($EVAL_ERROR);
419+ return;
420+ }
421+ my @tables = @{$sth->fetchall_arrayref({})};
422+ @tables = map {
423+ my %tbl; # Make a copy with lowercased keys
424+ @tbl{ map { lc $_ } keys %$_ } = values %$_;
425+ $tbl{engine} ||= $tbl{type} || $tbl{comment};
426+ delete $tbl{type};
427+ \%tbl;
428+ } @tables;
429+ return @tables;
430+}
431+
432+sub _d {
433+ my ($package, undef, $line) = caller 0;
434+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
435+ map { defined $_ ? $_ : 'undef' }
436+ @_;
437+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
438+}
439+
440+1;
441+}
442+# ###########################################################################
443+# End TableParser package
444+# ###########################################################################
445+
446+# ###########################################################################
447 # Processlist package
448 # This package is a copy without comments from the original. The original
449 # with comments and its test file can be found in the Bazaar repository at,
450@@ -2135,6 +2565,7 @@
451 last_poll => 0,
452 active_cxn => {}, # keyed off ID
453 event_cache => [],
454+ _reasons_for_matching => {},
455 };
456 return bless $self, $class;
457 }
458@@ -2345,7 +2776,9 @@
459 PTDEBUG && _d("Query isn't running long enough");
460 next QUERY;
461 }
462- PTDEBUG && _d('Exceeds busy time');
463+ my $reason = 'Exceeds busy time';
464+ PTDEBUG && _d($reason);
465+ push @{$self->{_reasons_for_matching}->{$query} ||= []}, $reason;
466 $matched++;
467 }
468
469@@ -2355,7 +2788,9 @@
470 PTDEBUG && _d("Query isn't idle long enough");
471 next QUERY;
472 }
473- PTDEBUG && _d('Exceeds idle time');
474+ my $reason = 'Exceeds idle time';
475+ PTDEBUG && _d($reason);
476+ push @{$self->{_reasons_for_matching}->{$query} ||= []}, $reason;
477 $matched++;
478 }
479
480@@ -2372,7 +2807,9 @@
481 PTDEBUG && _d('Query does not match', $property, 'spec');
482 next QUERY;
483 }
484- PTDEBUG && _d('Query matches', $property, 'spec');
485+ my $reason = 'Query matches ' . $property . ' spec';
486+ PTDEBUG && _d($reason);
487+ push @{$self->{_reasons_for_matching}->{$query} ||= []}, $reason;
488 $matched++;
489 }
490 }
491@@ -3295,6 +3732,125 @@
492 # ###########################################################################
493
494 # ###########################################################################
495+# Quoter package
496+# This package is a copy without comments from the original. The original
497+# with comments and its test file can be found in the Bazaar repository at,
498+# lib/Quoter.pm
499+# t/lib/Quoter.t
500+# See https://launchpad.net/percona-toolkit for more information.
501+# ###########################################################################
502+{
503+package Quoter;
504+
505+use strict;
506+use warnings FATAL => 'all';
507+use English qw(-no_match_vars);
508+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
509+
510+sub new {
511+ my ( $class, %args ) = @_;
512+ return bless {}, $class;
513+}
514+
515+sub quote {
516+ my ( $self, @vals ) = @_;
517+ foreach my $val ( @vals ) {
518+ $val =~ s/`/``/g;
519+ }
520+ return join('.', map { '`' . $_ . '`' } @vals);
521+}
522+
523+sub quote_val {
524+ my ( $self, $val ) = @_;
525+
526+ return 'NULL' unless defined $val; # undef = NULL
527+ return "''" if $val eq ''; # blank string = ''
528+ return $val if $val =~ m/^0x[0-9a-fA-F]+$/; # hex data
529+
530+ $val =~ s/(['\\])/\\$1/g;
531+ return "'$val'";
532+}
533+
534+sub split_unquote {
535+ my ( $self, $db_tbl, $default_db ) = @_;
536+ $db_tbl =~ s/`//g;
537+ my ( $db, $tbl ) = split(/[.]/, $db_tbl);
538+ if ( !$tbl ) {
539+ $tbl = $db;
540+ $db = $default_db;
541+ }
542+ return ($db, $tbl);
543+}
544+
545+sub literal_like {
546+ my ( $self, $like ) = @_;
547+ return unless $like;
548+ $like =~ s/([%_])/\\$1/g;
549+ return "'$like'";
550+}
551+
552+sub join_quote {
553+ my ( $self, $default_db, $db_tbl ) = @_;
554+ return unless $db_tbl;
555+ my ($db, $tbl) = split(/[.]/, $db_tbl);
556+ if ( !$tbl ) {
557+ $tbl = $db;
558+ $db = $default_db;
559+ }
560+ $db = "`$db`" if $db && $db !~ m/^`/;
561+ $tbl = "`$tbl`" if $tbl && $tbl !~ m/^`/;
562+ return $db ? "$db.$tbl" : $tbl;
563+}
564+
565+sub serialize_list {
566+ my ( $self, @args ) = @_;
567+ return unless @args;
568+
569+ return $args[0] if @args == 1 && !defined $args[0];
570+
571+ die "Cannot serialize multiple values with undef/NULL"
572+ if grep { !defined $_ } @args;
573+
574+ return join ',', map { quotemeta } @args;
575+}
576+
577+sub deserialize_list {
578+ my ( $self, $string ) = @_;
579+ return $string unless defined $string;
580+ my @escaped_parts = $string =~ /
581+ \G # Start of string, or end of previous match.
582+ ( # Each of these is an element in the original list.
583+ [^\\,]* # Anything not a backslash or a comma
584+ (?: # When we get here, we found one of the above.
585+ \\. # A backslash followed by something so we can continue
586+ [^\\,]* # Same as above.
587+ )* # Repeat zero of more times.
588+ )
589+ , # Comma dividing elements
590+ /sxgc;
591+
592+ push @escaped_parts, pos($string) ? substr( $string, pos($string) ) : $string;
593+
594+ my @unescaped_parts = map {
595+ my $part = $_;
596+
597+ my $char_class = utf8::is_utf8($part) # If it's a UTF-8 string,
598+ ? qr/(?=\p{ASCII})\W/ # We only care about non-word
599+ : qr/(?=\p{ASCII})\W|[\x{80}-\x{FF}]/; # Otherwise,
600+ $part =~ s/\\($char_class)/$1/g;
601+ $part;
602+ } @escaped_parts;
603+
604+ return @unescaped_parts;
605+}
606+
607+1;
608+}
609+# ###########################################################################
610+# End Quoter package
611+# ###########################################################################
612+
613+# ###########################################################################
614 # QueryRewriter package
615 # This package is a copy without comments from the original. The original
616 # with comments and its test file can be found in the Bazaar repository at,
617@@ -4027,7 +4583,10 @@
618 my $cxn;
619 my $dbh; # $cxn->dbh
620 my $get_proclist; # callback to SHOW PROCESSLIST
621+ my $proc_sth;
622 my $kill; # callback to KILL
623+ my $kill_sth;
624+ my $kill_sql = $o->get('kill-query') ? 'KILL QUERY ?' : 'KILL ?';
625 my $files;
626 if ( $files = $o->get('test-matching') ) {
627 PTDEBUG && _d('Getting processlist from files:', @$files);
628@@ -4079,7 +4638,7 @@
629 # will need to be re-initialized.
630 my $retry = Retry->new();
631
632- my $proc_sth = $dbh->prepare('SHOW FULL PROCESSLIST');
633+ $proc_sth = $dbh->prepare('SHOW FULL PROCESSLIST');
634 $get_proclist = sub {
635 return $retry->retry(
636 # Retry for an hour: 1,200 tries x 3 seconds = 3600s/1hr
637@@ -4112,8 +4671,8 @@
638 );
639 };
640
641- my $kill_sql = $o->get('kill-query') ? 'KILL QUERY ?' : 'KILL ?';
642- my $kill_sth = $dbh->prepare($kill_sql);
643+
644+ $kill_sth = $dbh->prepare($kill_sql);
645 $kill = sub {
646 my ($id) = @_;
647 PTDEBUG && _d('Killing process', $id);
648@@ -4146,6 +4705,93 @@
649 };
650 }
651
652+ # Set up --log-dsn if specified.
653+ my ($log, $log_sth);
654+ my @processlist_columns = qw(
655+ Id User Host db Command
656+ Time State Info Time_ms
657+ );
658+ if ( my $log_dsn = $o->get('log-dsn') ) {
659+ my $db = $log_dsn->{D};
660+ my $table = $log_dsn->{t};
661+ die "--log-dsn does not specify a database (D) "
662+ . "or a database-qualified table (t)"
663+ unless defined $table && defined $db;
664+ my $log_cxn = Cxn->new(
665+ dsn_string => ($dp->get_cxn_params($log_dsn))[0],
666+ NAME_lc => 0,
667+ DSNParser => $dp,
668+ OptionParser => $o,
669+ );
670+ my $log_dbh = $log_cxn->connect();
671+ my $log_table = Quoter->quote($db, $table);
672+
673+ # Create the log-table table if it doesn't exist and --create-log-table
674+ # was passed in
675+ my $tp = TableParser->new( Quoter => "Quoter" );
676+ if ( !$tp->check_table( dbh => $log_dbh, db => $db, tbl => $table ) ) {
677+ if ($o->get('create-log-table') ) {
678+ my $sql = $o->read_para_after(
679+ __FILE__, qr/MAGIC_create_log_table/);
680+ $sql =~ s/kill_log/IF NOT EXISTS $log_table/;
681+ PTDEBUG && _d($sql);
682+ $log_dbh->do($sql);
683+ }
684+ else {
685+ die "--log-dsn table does not exist. Please create it or specify "
686+ . "--create-log-table.";
687+ }
688+ }
689+
690+ # All the columns of the table that we care about
691+ my @all_log_columns = ( qw( server_id timestamp reason kill_error ),
692+ @processlist_columns );
693+
694+ my $sql = 'SELECT @@SERVER_ID';
695+ PTDEBUG && _d($sql);
696+ my ($server_id) = $dbh->selectrow_array($sql);
697+
698+ $sql = "INSERT INTO $log_table ("
699+ . join(", ", @all_log_columns)
700+ . ") VALUES("
701+ . join(", ", $server_id, ("?") x (@all_log_columns-1))
702+ . ")";
703+ PTDEBUG && _d($sql);
704+ $log_sth = $log_dbh->prepare($sql);
705+
706+ my $retry = Retry->new();
707+
708+ $log = sub {
709+ my (@params) = @_;
710+ PTDEBUG && _d('Logging values:', @params);
711+ return $retry->retry(
712+ tries => 20,
713+ wait => sub { sleep 3; },
714+ try => sub { return $log_sth->execute(@params); },
715+ fail => sub {
716+ my (%args) = @_;
717+ my $error = $args{error};
718+ # The 1st pattern means that MySQL itself died or was stopped.
719+ # The 2nd pattern means that our cxn was killed (KILL <id>).
720+ if ( $error =~ m/MySQL server has gone away/
721+ || $error =~ m/Lost connection to MySQL server/ ) {
722+ eval {
723+ $log_dbh = $log_cxn->connect();
724+ $log_sth = $log_dbh->prepare( $sql );
725+ msg('Reconnected to ' . $cxn->name());
726+ };
727+ return 1 unless $EVAL_ERROR; # try again
728+ }
729+ return 0; # call final_fail
730+ },
731+ final_fail => sub {
732+ my (%args) = @_;
733+ die $args{error};
734+ },
735+ );
736+ };
737+ }
738+
739 # ########################################################################
740 # Daemonize only after (potentially) asking for passwords for --ask-pass.
741 # ########################################################################
742@@ -4349,7 +4995,17 @@
743 . " seconds before kill");
744 sleep $o->get('wait-before-kill');
745 }
746+ local $@;
747 eval { $kill->($query->{Id}) };
748+ if ( $log ) {
749+ log_to_table(
750+ log => $log,
751+ query => $query,
752+ proclist => $pl,
753+ columns => \@processlist_columns,
754+ eval_error => $EVAL_ERROR,
755+ );
756+ }
757 if ( $EVAL_ERROR ) {
758 msg("Error killing $query->{Id}: $EVAL_ERROR");
759 }
760@@ -4417,6 +5073,21 @@
761 return;
762 }
763
764+sub log_to_table {
765+ my (%args) = @_;
766+ my ($log, $query, $pl, $processlist_columns)
767+ = @args{qw( log query proclist columns )};
768+
769+ my $ts = Transformers::ts(localtime);
770+ my $reasons = join "\n", map {
771+ defined($_) ? $_ : "Unkown reason"
772+ } @{ $pl->{_reasons_for_matching}->{$query} };
773+ $log->(
774+ $ts, $reasons, $args{eval_error},
775+ @{$query}{@$processlist_columns}
776+ );
777+}
778+
779 sub group_queries {
780 my ( %args ) = @_;
781 my ($proclist, $group_by, $qr) = @args{qw(proclist group_by QueryRewriter)};
782@@ -4642,6 +5313,13 @@
783
784 The database to use for the connection.
785
786+=item --create-log-table
787+
788+Create the L<"--log-dsn"> table if it does not exist.
789+
790+This option causes the table specified by L<"--log-dsn"> to be created with the
791+default structure shown in the documentation for that option.
792+
793 =item --daemonize
794
795 Fork to the background and detach from the shell. POSIX operating systems
796@@ -4746,6 +5424,37 @@
797
798 Print all output to this file when daemonized.
799
800+=item --log-dsn
801+
802+type: DSN
803+
804+Store each query killed in this DSN.
805+
806+The argument specifies a table to store all killed queries. The DSN
807+passed in must have the databse (D) and table (t) options. The
808+table must have at least the following columns. You can add more columns for
809+your own special purposes, but they won't be used by pt-kill. The
810+following CREATE TABLE definition is also used for L<"--create-log-table">.
811+MAGIC_create_log_table:
812+
813+ CREATE TABLE kill_log (
814+ kill_id int(10) unsigned NOT NULL AUTO_INCREMENT,
815+ server_id bigint(4) NOT NULL DEFAULT '0',
816+ timestamp DATETIME,
817+ reason TEXT,
818+ kill_error TEXT,
819+ Id bigint(4) NOT NULL DEFAULT '0',
820+ User varchar(16) NOT NULL DEFAULT '',
821+ Host varchar(64) NOT NULL DEFAULT '',
822+ db varchar(64) DEFAULT NULL,
823+ Command varchar(16) NOT NULL DEFAULT '',
824+ Time int(7) NOT NULL DEFAULT '0',
825+ State varchar(64) DEFAULT NULL,
826+ Info longtext,
827+ Time_ms bigint(21) DEFAULT '0', # NOTE, TODO: currently not used
828+ PRIMARY KEY (kill_id)
829+ ) DEFAULT CHARSET=utf8
830+
831 =item --password
832
833 short form: -p; type: string
834@@ -5251,6 +5960,10 @@
835
836 User for login if not current user.
837
838+=item * t
839+
840+Table to log actions in, if passed through --log-dsn.
841+
842 =back
843
844 =head1 ENVIRONMENT
845
846=== modified file 'lib/Processlist.pm'
847--- lib/Processlist.pm 2012-05-28 02:28:35 +0000
848+++ lib/Processlist.pm 2012-07-19 16:42:35 +0000
849@@ -75,6 +75,7 @@
850 last_poll => 0,
851 active_cxn => {}, # keyed off ID
852 event_cache => [],
853+ _reasons_for_matching => {},
854 };
855 return bless $self, $class;
856 }
857@@ -475,7 +476,15 @@
858 PTDEBUG && _d("Query isn't running long enough");
859 next QUERY;
860 }
861- PTDEBUG && _d('Exceeds busy time');
862+ my $reason = 'Exceeds busy time';
863+ PTDEBUG && _d($reason);
864+ # Saving the reasons for each query in the objct is a bit nasty,
865+ # but the alternatives are worse:
866+ # - Saving internal data in the query
867+ # - Instead of using the stringified hashref as a key, using
868+ # a checksum of the hashes' contents. Which could occasionally
869+ # fail miserably due to timing-related issues.
870+ push @{$self->{_reasons_for_matching}->{$query} ||= []}, $reason;
871 $matched++;
872 }
873
874@@ -486,7 +495,9 @@
875 PTDEBUG && _d("Query isn't idle long enough");
876 next QUERY;
877 }
878- PTDEBUG && _d('Exceeds idle time');
879+ my $reason = 'Exceeds idle time';
880+ PTDEBUG && _d($reason);
881+ push @{$self->{_reasons_for_matching}->{$query} ||= []}, $reason;
882 $matched++;
883 }
884
885@@ -507,7 +518,9 @@
886 PTDEBUG && _d('Query does not match', $property, 'spec');
887 next QUERY;
888 }
889- PTDEBUG && _d('Query matches', $property, 'spec');
890+ my $reason = 'Query matches ' . $property . ' spec';
891+ PTDEBUG && _d($reason);
892+ push @{$self->{_reasons_for_matching}->{$query} ||= []}, $reason;
893 $matched++;
894 }
895 }
896
897=== modified file 't/lib/Processlist.t'
898--- t/lib/Processlist.t 2012-05-30 14:36:44 +0000
899+++ t/lib/Processlist.t 2012-07-19 16:42:35 +0000
900@@ -9,7 +9,7 @@
901 use strict;
902 use warnings FATAL => 'all';
903 use English qw(-no_match_vars);
904-use Test::More tests => 34;
905+use Test::More tests => 35;
906
907 use Processlist;
908 use PerconaTest;
909@@ -600,6 +600,17 @@
910 },
911 );
912
913+my $matching_query =
914+ { 'Time' => '91',
915+ 'Command' => 'Query',
916+ 'db' => undef,
917+ 'Id' => '43',
918+ 'Info' => 'select * from foo',
919+ 'User' => 'msandbox',
920+ 'State' => 'executing',
921+ 'Host' => 'localhost'
922+ };
923+
924 my @queries = $pl->find(
925 [ { 'Time' => '488',
926 'Command' => 'Connect',
927@@ -675,33 +686,24 @@
928 'State' => 'Locked',
929 'Host' => 'localhost'
930 },
931- { 'Time' => '91',
932- 'Command' => 'Query',
933- 'db' => undef,
934- 'Id' => '43',
935- 'Info' => 'select * from foo',
936- 'User' => 'msandbox',
937- 'State' => 'executing',
938- 'Host' => 'localhost'
939- },
940+ $matching_query,
941 ],
942 %find_spec,
943 );
944
945-my $expected = [
946- { 'Time' => '91',
947- 'Command' => 'Query',
948- 'db' => undef,
949- 'Id' => '43',
950- 'Info' => 'select * from foo',
951- 'User' => 'msandbox',
952- 'State' => 'executing',
953- 'Host' => 'localhost'
954- },
955- ];
956+my $expected = [ $matching_query ];
957
958 is_deeply(\@queries, $expected, 'Basic find()');
959
960+{
961+ # Internal, fragile test!
962+ is_deeply(
963+ $pl->{_reasons_for_matching}->{$matching_query},
964+ [ 'Exceeds busy time', 'Query matches Command spec', 'Query matches Info spec', ],
965+ "_reasons_for_matching works"
966+ );
967+}
968+
969 %find_spec = (
970 busy_time => 1,
971 ignore => {
972
973=== modified file 't/pt-kill/kill.t'
974--- t/pt-kill/kill.t 2012-07-12 22:49:15 +0000
975+++ t/pt-kill/kill.t 2012-07-19 16:42:35 +0000
976@@ -29,7 +29,7 @@
977 plan skip_all => 'Cannot connect to sandbox master';
978 }
979 else {
980- plan tests => 8;
981+ plan tests => 21;
982 }
983
984 my $output;
985@@ -56,8 +56,11 @@
986
987 $output = output(
988 sub { pt_kill::main('-F', $cnf, qw(--kill --print --run-time 1 --interval 1),
989- '--match-info', 'select sleep\(4\)') },
990+ "--match-info", 'select sleep\(4\)',
991+ )
992+ },
993 );
994+
995 like(
996 $output,
997 qr/KILL $pid /,
998@@ -117,6 +120,157 @@
999 );
1000
1001 # #############################################################################
1002+# Test that --log-dsn
1003+# #############################################################################
1004+
1005+$dbh->do("DROP DATABASE IF EXISTS `kill_test`");
1006+$dbh->do("CREATE DATABASE `kill_test`");
1007+
1008+my $sql = OptionParser->read_para_after(
1009+ "$trunk/bin/pt-kill", qr/MAGIC_create_log_table/);
1010+$sql =~ s/kill_log/`kill_test`.`log_table`/;
1011+
1012+$dbh->do($sql);
1013+
1014+{
1015+ system("/tmp/12345/use -h127.1 -P12345 -umsandbox -pmsandbox -e 'select sleep(4)' >/dev/null&");
1016+ sleep 0.5;
1017+ local $EVAL_ERROR;
1018+ eval {
1019+ pt_kill::main('-F', $cnf, qw(--kill --run-time 1 --interval 1),
1020+ "--match-info", 'select sleep\(4\)',
1021+ "--log-dsn", q!h=127.1,P=12345,u=msandbox,p=msandbox,D=kill_test,t=log_table!,
1022+ )
1023+ };
1024+ is(
1025+ $EVAL_ERROR,
1026+ '',
1027+ "--log-dsn works if the table exists and --create-log-table wasn't passed in."
1028+ ) or diag $EVAL_ERROR;
1029+
1030+ local $EVAL_ERROR;
1031+ my $results = eval { $dbh->selectall_arrayref("SELECT * FROM `kill_test`.`log_table`", { Slice => {} } ) };
1032+ is(
1033+ $EVAL_ERROR,
1034+ '',
1035+ "...and we can query the table"
1036+ ) or diag $EVAL_ERROR;
1037+
1038+ is @{$results}, 1, "...which contains one entry";
1039+ use Data::Dumper;
1040+ my $reason = $dbh->selectrow_array("SELECT reason FROM `kill_test`.`log_table` WHERE kill_id=1");
1041+ is $reason,
1042+ 'Query matches Info spec',
1043+ 'reason gets set to something sensible';
1044+
1045+ TODO: {
1046+ local $::TODO = "Time_ms currently isn't reported";
1047+ my $time_ms = $dbh->selectrow_array("SELECT Time_ms FROM `kill_test`.`log_table` WHERE kill_id=1");
1048+ ok $time_ms;
1049+ }
1050+
1051+ my $result = shift @$results;
1052+ my $against = {
1053+ user => 'msandbox',
1054+ host => 'localhost',
1055+ db => undef,
1056+ command => 'Query',
1057+ state => ($sandbox_version lt '5.1' ? "executing" : "User sleep"),
1058+ info => 'select sleep(4)',
1059+ };
1060+ my %trimmed_result;
1061+ @trimmed_result{ keys %$against } = @{$result}{ keys %$against };
1062+ $trimmed_result{host} =~ s/localhost:[0-9]+/localhost/;
1063+ is_deeply(
1064+ \%trimmed_result,
1065+ $against,
1066+ "...and was populated as expected",
1067+ ) or diag(Dumper($result));
1068+
1069+ system("/tmp/12345/use -h127.1 -P12345 -umsandbox -pmsandbox -e 'select sleep(4)' >/dev/null&");
1070+ sleep 0.5;
1071+ local $EVAL_ERROR;
1072+ eval {
1073+ pt_kill::main('-F', $cnf, qw(--kill --run-time 1 --interval 1 --create-log-table),
1074+ "--match-info", 'select sleep\(4\)',
1075+ "--log-dsn", q!h=127.1,P=12345,u=msandbox,p=msandbox,D=kill_test,t=log_table!,
1076+ )
1077+ };
1078+ is(
1079+ $EVAL_ERROR,
1080+ '',
1081+ "--log-dsn works if the table exists and --create-log-table was passed in."
1082+ );
1083+}
1084+
1085+{
1086+ $dbh->do("DROP TABLE `kill_test`.`log_table`");
1087+
1088+ system("/tmp/12345/use -h127.1 -P12345 -umsandbox -pmsandbox -e 'select sleep(4)' >/dev/null&");
1089+ sleep 0.5;
1090+ local $EVAL_ERROR;
1091+ eval {
1092+ pt_kill::main('-F', $cnf, qw(--kill --run-time 1 --interval 1 --create-log-table),
1093+ "--match-info", 'select sleep\(4\)',
1094+ "--log-dsn", q!h=127.1,P=12345,u=msandbox,p=msandbox,D=kill_test,t=log_table!,
1095+ )
1096+ };
1097+ is(
1098+ $EVAL_ERROR,
1099+ '',
1100+ "--log-dsn works if the table doesn't exists and --create-log-table was passed in."
1101+ );
1102+}
1103+
1104+{
1105+ $dbh->do("DROP TABLE `kill_test`.`log_table`");
1106+
1107+ local $EVAL_ERROR;
1108+ eval {
1109+ pt_kill::main('-F', $cnf, qw(--kill --run-time 1 --interval 1),
1110+ "--match-info", 'select sleep\(4\)',
1111+ "--log-dsn", q!h=127.1,P=12345,u=msandbox,p=msandbox,D=kill_test,t=log_table!,
1112+ )
1113+ };
1114+ like $EVAL_ERROR,
1115+ qr/\Q--log-dsn table does not exist. Please create it or specify\E/,
1116+ "By default, --log-dsn doesn't autogenerate a table";
1117+}
1118+
1119+for my $dsn (
1120+ q!h=127.1,P=12345,u=msandbox,p=msandbox,t=log_table!,
1121+ q!h=127.1,P=12345,u=msandbox,p=msandbox,D=kill_test!,
1122+ q!h=127.1,P=12345,u=msandbox,p=msandbox!,
1123+) {
1124+ local $EVAL_ERROR;
1125+ eval {
1126+ pt_kill::main('-F', $cnf, qw(--kill --run-time 1 --interval 1),
1127+ "--match-info", 'select sleep\(4\)',
1128+ "--log-dsn", $dsn,
1129+ )
1130+ };
1131+ like $EVAL_ERROR,
1132+ qr/\Q--log-dsn does not specify a database (D) or a database-qualified table (t)\E/,
1133+ "--log-dsn croaks if t= or D= are absent";
1134+}
1135+
1136+# Run it twice
1137+for (1,2) {
1138+ system("/tmp/12345/use -h127.1 -P12345 -umsandbox -pmsandbox -e 'select sleep(4)' >/dev/null&");
1139+ sleep 0.5;
1140+ pt_kill::main('-F', $cnf, qw(--kill --run-time 1 --interval 1 --create-log-table),
1141+ "--match-info", 'select sleep\(4\)',
1142+ "--log-dsn", q!h=127.1,P=12345,u=msandbox,p=msandbox,D=kill_test,t=log_table!,
1143+ );
1144+}
1145+
1146+my $results = $dbh->selectall_arrayref("SELECT * FROM `kill_test`.`log_table`");
1147+
1148+is @{$results}, 2, "Different --log-dsn runs reuse the same table.";
1149+
1150+$dbh->do("DROP DATABASE kill_test");
1151+
1152+# #############################################################################
1153 # Done.
1154 # #############################################################################
1155 $sb->wipe_clean($dbh);

Subscribers

People subscribed via source and target branches