Merge lp:~percona-toolkit-dev/percona-toolkit/pt-kill-reconnect-bug-941469 into lp:percona-toolkit/2.1

Proposed by Daniel Nichter
Status: Merged
Approved by: Daniel Nichter
Approved revision: 313
Merged at revision: 311
Proposed branch: lp:~percona-toolkit-dev/percona-toolkit/pt-kill-reconnect-bug-941469
Merge into: lp:percona-toolkit/2.1
Diff against target: 612 lines (+402/-33)
7 files modified
bin/pt-kill (+317/-23)
lib/Cxn.pm (+5/-1)
t/pt-kill/basics.t (+30/-4)
t/pt-kill/execute_command.t (+4/-3)
t/pt-kill/kill.t (+1/-1)
t/pt-kill/standard_options.t (+1/-1)
util/kill-mysql-process (+44/-0)
To merge this branch: bzr merge lp:~percona-toolkit-dev/percona-toolkit/pt-kill-reconnect-bug-941469
Reviewer Review Type Date Requested Status
Daniel Nichter Approve
Review via email: mp+114748@code.launchpad.net
To post a comment you must log in.
312. By Daniel Nichter

Retry for an hour to get proclist; retry KILL once.

313. By Daniel Nichter

Stabilize some pt-kill tests.

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-12 16:55:54 +0000
3+++ bin/pt-kill 2012-07-12 23:28:21 +0000
4@@ -3686,6 +3686,232 @@
5 # ###########################################################################
6
7 # ###########################################################################
8+# Retry package
9+# This package is a copy without comments from the original. The original
10+# with comments and its test file can be found in the Bazaar repository at,
11+# lib/Retry.pm
12+# t/lib/Retry.t
13+# See https://launchpad.net/percona-toolkit for more information.
14+# ###########################################################################
15+{
16+package Retry;
17+
18+use strict;
19+use warnings FATAL => 'all';
20+use English qw(-no_match_vars);
21+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
22+
23+sub new {
24+ my ( $class, %args ) = @_;
25+ my $self = {
26+ %args,
27+ };
28+ return bless $self, $class;
29+}
30+
31+sub retry {
32+ my ( $self, %args ) = @_;
33+ my @required_args = qw(try fail final_fail);
34+ foreach my $arg ( @required_args ) {
35+ die "I need a $arg argument" unless $args{$arg};
36+ };
37+ my ($try, $fail, $final_fail) = @args{@required_args};
38+ my $wait = $args{wait} || sub { sleep 1; };
39+ my $tries = $args{tries} || 3;
40+
41+ my $last_error;
42+ my $tryno = 0;
43+ TRY:
44+ while ( ++$tryno <= $tries ) {
45+ PTDEBUG && _d("Try", $tryno, "of", $tries);
46+ my $result;
47+ eval {
48+ $result = $try->(tryno=>$tryno);
49+ };
50+ if ( $EVAL_ERROR ) {
51+ PTDEBUG && _d("Try code failed:", $EVAL_ERROR);
52+ $last_error = $EVAL_ERROR;
53+
54+ if ( $tryno < $tries ) { # more retries
55+ my $retry = $fail->(tryno=>$tryno, error=>$last_error);
56+ last TRY unless $retry;
57+ PTDEBUG && _d("Calling wait code");
58+ $wait->(tryno=>$tryno);
59+ }
60+ }
61+ else {
62+ PTDEBUG && _d("Try code succeeded");
63+ return $result;
64+ }
65+ }
66+
67+ PTDEBUG && _d('Try code did not succeed');
68+ return $final_fail->(error=>$last_error);
69+}
70+
71+sub _d {
72+ my ($package, undef, $line) = caller 0;
73+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
74+ map { defined $_ ? $_ : 'undef' }
75+ @_;
76+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
77+}
78+
79+1;
80+}
81+# ###########################################################################
82+# End Retry package
83+# ###########################################################################
84+
85+# ###########################################################################
86+# Cxn package
87+# This package is a copy without comments from the original. The original
88+# with comments and its test file can be found in the Bazaar repository at,
89+# lib/Cxn.pm
90+# t/lib/Cxn.t
91+# See https://launchpad.net/percona-toolkit for more information.
92+# ###########################################################################
93+{
94+package Cxn;
95+
96+use strict;
97+use warnings FATAL => 'all';
98+use English qw(-no_match_vars);
99+use Scalar::Util qw(blessed);
100+use constant {
101+ PTDEBUG => $ENV{PTDEBUG} || 0,
102+ PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0,
103+};
104+
105+sub new {
106+ my ( $class, %args ) = @_;
107+ my @required_args = qw(DSNParser OptionParser);
108+ foreach my $arg ( @required_args ) {
109+ die "I need a $arg argument" unless $args{$arg};
110+ };
111+ my ($dp, $o) = @args{@required_args};
112+
113+ my $dsn_defaults = $dp->parse_options($o);
114+ my $prev_dsn = $args{prev_dsn};
115+ my $dsn = $args{dsn};
116+ if ( !$dsn ) {
117+ $args{dsn_string} ||= 'h=' . ($dsn_defaults->{h} || 'localhost');
118+
119+ $dsn = $dp->parse(
120+ $args{dsn_string}, $prev_dsn, $dsn_defaults);
121+ }
122+ elsif ( $prev_dsn ) {
123+ $dsn = $dp->copy($prev_dsn, $dsn);
124+ }
125+
126+ my $self = {
127+ dsn => $dsn,
128+ dbh => $args{dbh},
129+ dsn_name => $dp->as_string($dsn, [qw(h P S)]),
130+ hostname => '',
131+ set => $args{set},
132+ NAME_lc => $args{NAME_lc},
133+ dbh_set => 0,
134+ OptionParser => $o,
135+ DSNParser => $dp,
136+ };
137+
138+ return bless $self, $class;
139+}
140+
141+sub connect {
142+ my ( $self ) = @_;
143+ my $dsn = $self->{dsn};
144+ my $dp = $self->{DSNParser};
145+ my $o = $self->{OptionParser};
146+
147+ my $dbh = $self->{dbh};
148+ if ( !$dbh || !$dbh->ping() ) {
149+ if ( $o->get('ask-pass') && !$self->{asked_for_pass} ) {
150+ $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: ");
151+ $self->{asked_for_pass} = 1;
152+ }
153+ $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit => 1 });
154+ }
155+ PTDEBUG && _d($dbh, 'Connected dbh to', $self->{name});
156+
157+ return $self->set_dbh($dbh);
158+}
159+
160+sub set_dbh {
161+ my ($self, $dbh) = @_;
162+
163+ if ( $self->{dbh} && $self->{dbh} == $dbh && $self->{dbh_set} ) {
164+ PTDEBUG && _d($dbh, 'Already set dbh');
165+ return $dbh;
166+ }
167+
168+ PTDEBUG && _d($dbh, 'Setting dbh');
169+
170+ if ( !exists $self->{NAME_lc}
171+ || (defined $self->{NAME_lc} && $self->{NAME_lc}) ) {
172+ $dbh->{FetchHashKeyName} = 'NAME_lc';
173+ }
174+
175+ my $sql = 'SELECT @@hostname, @@server_id';
176+ PTDEBUG && _d($dbh, $sql);
177+ my ($hostname, $server_id) = $dbh->selectrow_array($sql);
178+ PTDEBUG && _d($dbh, 'hostname:', $hostname, $server_id);
179+ if ( $hostname ) {
180+ $self->{hostname} = $hostname;
181+ }
182+
183+ if ( my $set = $self->{set}) {
184+ $set->($dbh);
185+ }
186+
187+ $self->{dbh} = $dbh;
188+ $self->{dbh_set} = 1;
189+ return $dbh;
190+}
191+
192+sub dbh {
193+ my ($self) = @_;
194+ return $self->{dbh};
195+}
196+
197+sub dsn {
198+ my ($self) = @_;
199+ return $self->{dsn};
200+}
201+
202+sub name {
203+ my ($self) = @_;
204+ return $self->{dsn_name} if PERCONA_TOOLKIT_TEST_USE_DSN_NAMES;
205+ return $self->{hostname} || $self->{dsn_name} || 'unknown host';
206+}
207+
208+sub DESTROY {
209+ my ($self) = @_;
210+ if ( $self->{dbh}
211+ && blessed($self->{dbh})
212+ && $self->{dbh}->can("disconnect") ) {
213+ PTDEBUG && _d('Disconnecting dbh', $self->{dbh}, $self->{name});
214+ $self->{dbh}->disconnect();
215+ }
216+ return;
217+}
218+
219+sub _d {
220+ my ($package, undef, $line) = caller 0;
221+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
222+ map { defined $_ ? $_ : 'undef' }
223+ @_;
224+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
225+}
226+
227+1;
228+}
229+# ###########################################################################
230+# End Cxn package
231+# ###########################################################################
232+
233+# ###########################################################################
234 # This is a combination of modules and programs in one -- a runnable module.
235 # http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last
236 # Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition.
237@@ -3798,10 +4024,10 @@
238 # ########################################################################
239 # Make input sub that will either get processlist from MySQL or a file.
240 # ########################################################################
241- my $dsn;
242- my $dbh;
243- my $kill_sth;
244- my $get_proclist;
245+ my $cxn;
246+ my $dbh; # $cxn->dbh
247+ my $get_proclist; # callback to SHOW PROCESSLIST
248+ my $kill; # callback to KILL
249 my $files;
250 if ( $files = $o->get('test-matching') ) {
251 PTDEBUG && _d('Getting processlist from files:', @$files);
252@@ -3840,14 +4066,83 @@
253 }
254 else {
255 PTDEBUG && _d('Getting processlist from MySQL');
256- $dsn = $dp->parse_options($o);
257- $dbh = get_cxn($dp, $dsn, 1);
258- $kill_sth = $o->get('kill-query') ? $dbh->prepare('KILL QUERY ?')
259- : $dbh->prepare('KILL ?');
260+ $cxn = Cxn->new(
261+ dsn_string => shift @ARGV,
262+ NAME_lc => 0,
263+ DSNParser => $dp,
264+ OptionParser => $o,
265+ );
266+ $dbh = $cxn->connect();
267+
268+ # Make the get_proclist and kill callbacks. Use Retry in case
269+ # the connection to MySQL is lost, then the dbh and the sths
270+ # will need to be re-initialized.
271+ my $retry = Retry->new();
272+
273 my $proc_sth = $dbh->prepare('SHOW FULL PROCESSLIST');
274 $get_proclist = sub {
275- $proc_sth->execute();
276- return $proc_sth->fetchall_arrayref({});
277+ return $retry->retry(
278+ # Retry for an hour: 1,200 tries x 3 seconds = 3600s/1hr
279+ tries => 1200,
280+ wait => sub { sleep 3; },
281+ try => sub {
282+ $proc_sth->execute();
283+ return $proc_sth->fetchall_arrayref({});
284+ },
285+ fail => sub {
286+ my (%args) = @_;
287+ my $error = $args{error};
288+ # The 1st pattern means that MySQL itself died or was stopped.
289+ # The 2nd pattern means that our cxn was killed (KILL <id>).
290+ if ( $error =~ m/MySQL server has gone away/
291+ || $error =~ m/Lost connection to MySQL server/ ) {
292+ eval {
293+ $dbh = $cxn->connect();
294+ $proc_sth = $dbh->prepare('SHOW FULL PROCESSLIST');
295+ msg('Reconnected to ' . $cxn->name());
296+ };
297+ return 1 unless $EVAL_ERROR; # try again
298+ }
299+ return 0; # call final_fail
300+ },
301+ final_fail => sub {
302+ my (%args) = @_;
303+ die $args{error};
304+ },
305+ );
306+ };
307+
308+ my $kill_sql = $o->get('kill-query') ? 'KILL QUERY ?' : 'KILL ?';
309+ my $kill_sth = $dbh->prepare($kill_sql);
310+ $kill = sub {
311+ my ($id) = @_;
312+ PTDEBUG && _d('Killing process', $id);
313+ return $retry->retry(
314+ tries => 2,
315+ try => sub {
316+ return $kill_sth->execute($id);
317+ },
318+ fail => sub {
319+ my (%args) = @_;
320+ my $error = $args{error};
321+ # The 1st pattern means that MySQL itself died or was stopped.
322+ # The 2nd pattern means that our cxn was killed (KILL <id>).
323+ if ( $error =~ m/MySQL server has gone away/
324+ || $error =~ m/Lost connection to MySQL server/ ) {
325+ eval {
326+ $dbh = $cxn->connect();
327+ $kill_sth = $dbh->prepare($kill_sql);
328+ msg('Reconnected to ' . $cxn->name());
329+ };
330+ return 1 unless $EVAL_ERROR; # try again
331+ }
332+ return 0; # call final_fail
333+ },
334+ final_fail => sub {
335+ my (%args) = @_;
336+ die $args{error};
337+ },
338+ );
339 };
340 }
341
342@@ -3870,7 +4165,7 @@
343 # Start working.
344 # ########################################################################
345 msg("$PROGRAM_NAME starting");
346- msg($dbh ? "Connected to host " . $dp->as_string($dsn)
347+ msg($dbh ? "Connected to host " . $cxn->name()
348 : "Test matching files @$files");
349
350 # Class-based match criteria.
351@@ -4054,7 +4349,7 @@
352 . " seconds before kill");
353 sleep $o->get('wait-before-kill');
354 }
355- eval { $kill_sth->execute($query->{Id}); };
356+ eval { $kill->($query->{Id}) };
357 if ( $EVAL_ERROR ) {
358 msg("Error killing $query->{Id}: $EVAL_ERROR");
359 }
360@@ -4093,16 +4388,6 @@
361 # Subroutines.
362 # ############################################################################
363
364-sub get_cxn {
365- my ( $dp, $dsn, $ac ) = @_;
366- if ( $o->get('ask-pass') ) {
367- $dsn->{p} = OptionParser::prompt_noecho("Enter password: ");
368- }
369- my $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), {AutoCommit => $ac});
370- $dbh->{InactiveDestroy} = 1; # Because of forking.
371- return $dbh;
372-}
373-
374 # Forks and detaches from parent to execute the given command;
375 # does not block parent.
376 sub exec_cmd {
377@@ -4187,7 +4472,7 @@
378
379 =head1 SYNOPSIS
380
381-Usage: pt-kill [OPTIONS]
382+Usage: pt-kill [OPTIONS] [DSN]
383
384 pt-kill kills MySQL connections. pt-kill connects to MySQL and gets queries
385 from SHOW PROCESSLIST if no FILE is given. Else, it reads queries from one
386@@ -4326,6 +4611,9 @@
387
388 L<"--daemonize"> and L<"--test-matching"> are mutually exclusive.
389
390+This tool accepts additional command-line arguments. Refer to the
391+L<"SYNOPSIS"> and usage information for details.
392+
393 =over
394
395 =item --ask-pass
396@@ -4348,6 +4636,12 @@
397 Read this comma-separated list of config files; if specified, this must be the
398 first option on the command line.
399
400+=item --database
401+
402+short form: -D; type: string
403+
404+The database to use for the connection.
405+
406 =item --daemonize
407
408 Fork to the background and detach from the shell. POSIX operating systems
409
410=== modified file 'lib/Cxn.pm'
411--- lib/Cxn.pm 2012-06-05 19:24:32 +0000
412+++ lib/Cxn.pm 2012-07-12 23:28:21 +0000
413@@ -103,6 +103,7 @@
414 dsn_name => $dp->as_string($dsn, [qw(h P S)]),
415 hostname => '',
416 set => $args{set},
417+ NAME_lc => $args{NAME_lc},
418 dbh_set => 0,
419 OptionParser => $o,
420 DSNParser => $dp,
421@@ -149,7 +150,10 @@
422 PTDEBUG && _d($dbh, 'Setting dbh');
423
424 # Set stuff for this dbh (i.e. initialize it).
425- $dbh->{FetchHashKeyName} = 'NAME_lc';
426+ if ( !exists $self->{NAME_lc}
427+ || (defined $self->{NAME_lc} && $self->{NAME_lc}) ) {
428+ $dbh->{FetchHashKeyName} = 'NAME_lc';
429+ }
430
431 # Update the cxn's name. Until we connect, the DSN parts
432 # h and P are used. Once connected, use @@hostname.
433
434=== modified file 't/pt-kill/basics.t'
435--- t/pt-kill/basics.t 2012-06-03 19:14:30 +0000
436+++ t/pt-kill/basics.t 2012-07-12 23:28:21 +0000
437@@ -4,6 +4,7 @@
438 die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n"
439 unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH};
440 unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib";
441+ $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} = 1;
442 };
443
444 use strict;
445@@ -25,7 +26,7 @@
446 plan skip_all => 'Cannot connect to sandbox master';
447 }
448 else {
449- plan tests => 4;
450+ plan tests => 6;
451 }
452
453 my $output;
454@@ -52,7 +53,7 @@
455 ok(
456 @times > 2 && @times < 7,
457 "There were 2 to 5 captures"
458-) or print STDERR Dumper($output);
459+) or diag($output);
460
461 # This is to catch a bad bug where there wasn't any sleep time when
462 # --iterations was 0, and another bug when --run-time was not respected.
463@@ -64,8 +65,7 @@
464 ok(
465 @times > 7 && @times < 12,
466 'Approximately 9 or 10 captures with --iterations 0'
467-) or print STDERR Dumper($output);
468-
469+) or diag($output);
470
471 # ############################################################################
472 # --verbose
473@@ -81,6 +81,32 @@
474 );
475
476 # #############################################################################
477+# Reconnect if cxn lost.
478+# #############################################################################
479+$master_dbh->do("CREATE DATABASE IF NOT EXISTS pt_kill_test");
480+
481+system(qq($trunk/util/kill-mysql-process db=pt_kill_test wait=2 &));
482+
483+$output = output(
484+ sub { pt_kill::main('-F', $cnf, qw(-D pt_kill_test),
485+ qw(--run-time 4 --interval 1 --print --verbose)) },
486+ stderr => 1,
487+);
488+
489+like(
490+ $output,
491+ qr/Reconnected/,
492+ "kill-mysql-process says it reconnected"
493+);
494+
495+my $n_checks =()= $output =~ m/Checking processlist/g;
496+is(
497+ $n_checks,
498+ 4,
499+ "pt-kill still checked the processlist 4 times"
500+) or diag($output);
501+
502+# #############################################################################
503 # Done.
504 # #############################################################################
505 $sb->wipe_clean($master_dbh);
506
507=== modified file 't/pt-kill/execute_command.t'
508--- t/pt-kill/execute_command.t 2012-06-03 19:14:30 +0000
509+++ t/pt-kill/execute_command.t 2012-07-12 23:28:21 +0000
510@@ -47,6 +47,7 @@
511
512 SKIP: {
513 skip 'Cannot connect to sandbox master', 2 unless $master_dbh;
514+ $master_dbh->do("CREATE DATABASE IF NOT EXISTS pt_kill_zombie_test");
515
516 system "/tmp/12345/use -e 'select sleep(2)' >/dev/null 2>&1 &";
517
518@@ -70,7 +71,7 @@
519 diag(`rm $out`);
520
521 # Don't make zombies (https://bugs.launchpad.net/percona-toolkit/+bug/919819)
522- system "/tmp/12345/use -e 'select sleep(2)' >/dev/null 2>&1 &";
523+ $master_dbh->do("USE pt_kill_zombie_test");
524
525 my $sentinel = "/tmp/pt-kill-test.$PID.stop";
526 my $pid_file = "/tmp/pt-kill-test.$PID.pid";
527@@ -79,8 +80,8 @@
528 diag(`rm $pid_file 2>/dev/null`);
529 diag(`rm $log_file 2>/dev/null`);
530
531- `$cmd --daemonize --match-info 'select sleep' --interval 1 --print --execute-command 'echo zombie > $out' --verbose --pid $pid_file --log $log_file --sentinel $sentinel`;
532- sleep 1;
533+ `$cmd --daemonize --match-db pt_kill_zombie_test --interval 1 --print --execute-command 'echo zombie > $out' --verbose --pid $pid_file --log $log_file --sentinel $sentinel`;
534+ PerconaTest::wait_for_files($pid_file, $log_file, $out);
535 $output = `grep Executed $log_file`;
536 like(
537 $output,
538
539=== modified file 't/pt-kill/kill.t'
540--- t/pt-kill/kill.t 2012-06-03 19:14:30 +0000
541+++ t/pt-kill/kill.t 2012-07-12 23:28:21 +0000
542@@ -41,7 +41,7 @@
543
544 # Shell out to a sleep(10) query and try to capture the query.
545 # Backticks don't work here.
546-system("/tmp/12345/use -h127.1 -P12345 -umsandbox -pmsandbox -e 'select sleep(4)' >/dev/null&");
547+system("/tmp/12345/use -h127.1 -P12345 -umsandbox -pmsandbox -e 'select sleep(4)' >/dev/null 2>&1 &");
548 sleep 0.5;
549 my $rows = $dbh->selectall_hashref('show processlist', 'id');
550 my $pid;
551
552=== modified file 't/pt-kill/standard_options.t'
553--- t/pt-kill/standard_options.t 2012-06-05 15:56:56 +0000
554+++ t/pt-kill/standard_options.t 2012-07-12 23:28:21 +0000
555@@ -48,7 +48,7 @@
556 'Log file created'
557 );
558
559- sleep 3; # --run-time=2; if we sleep 2 we'll get intermittent failures.
560+ wait_until(sub { return !-f '/tmp/pt-kill.pid' });
561 ok(
562 !-f '/tmp/pt-kill.pid',
563 'PID file removed'
564
565=== added file 'util/kill-mysql-process'
566--- util/kill-mysql-process 1970-01-01 00:00:00 +0000
567+++ util/kill-mysql-process 2012-07-12 23:28:21 +0000
568@@ -0,0 +1,44 @@
569+#!/usr/bin/env perl
570+
571+# This script helps test that tools reconnect to MySQL. Its meant to be ran
572+# in the background like system(qq($trunk/util/kill-mysql-process DB)) where
573+# DB is the name of special "tracer" database used to isolate the test in
574+# the process list. So, do something like CREATE DATABASE pt_kill_test, then
575+# run the test with D=pt_kill_test (presuming pt_kill_test is unique to
576+# the test). This script will then kill any and all processes that are using
577+# the pt_kill_test db.
578+#
579+# Exits 0 if the tracer db is observed and procs are killed, else exits 1.
580+
581+use strict;
582+use warnings FATAL => 'all';
583+use Time::HiRes qw(sleep time);
584+
585+if ( !@ARGV || @ARGV < 1 || @ARGV > 3 ) {
586+ print STDERR "Usage: kill-mysql-process OPTION=VALUE\n";
587+ print STDERR "Options: db, wait, runtime, interval\n";
588+ exit 1;
589+}
590+
591+my %opt = map { my ($op, $val) = split '=', $_; $op => $val; } @ARGV;
592+
593+$opt{db} ||= 'tracer_db';
594+$opt{runtime} ||= 5.0;
595+$opt{interval} ||= 0.2;
596+
597+sleep $opt{wait} if $opt{wait};
598+
599+my $t_start = time;
600+while ( time - $t_start < $opt{runtime} ) {
601+ my $procs = `/tmp/12345/use -ss -e "show processlist" | grep $opt{db} | cut -f1`;
602+ if ( $procs && $procs =~ /\d/ ) {
603+ foreach my $proc ( split "\n", $procs ) {
604+ chomp $proc;
605+ `/tmp/12345/use -e "KILL $proc"`;
606+ }
607+ exit 0;
608+ }
609+ sleep $opt{interval};
610+}
611+
612+exit 1;

Subscribers

People subscribed via source and target branches