File Coverage

blib/lib/DBIx/QuickDB/Watcher.pm
Criterion Covered Total %
statement 82 239 34.3
branch 25 118 21.1
condition 10 65 15.3
subroutine 10 30 33.3
pod 0 9 0.0
total 127 461 27.5


line stmt bran cond sub pod time code
1             package DBIx::QuickDB::Watcher;
2 26     26   166265 use strict;
  26         39  
  26         755  
3 26     26   94 use warnings;
  26         43  
  26         1413  
4              
5             our $VERSION = '0.000050';
6              
7 26     26   119 use Carp qw/croak/;
  26         35  
  26         1202  
8 26     26   171 use POSIX qw/:sys_wait_h/;
  26         104  
  26         205  
9 26     26   4709 use Time::HiRes qw/sleep time/;
  26         55  
  26         196  
10 26     26   1544 use Scalar::Util qw/weaken/;
  26         60  
  26         983  
11 26     26   155 use File::Path qw/remove_tree/;
  26         53  
  26         1195  
12              
13 26         199 use DBIx::QuickDB::Util::HashBase qw{
14            
15            
16            
17            
18            
19              
20            
21            
22            
23              
24            
25 26     26   10910 };
  26         77  
26              
27             sub init {
28 0     0 0 0 my $self = shift;
29              
30 0   0     0 $self->{+MASTER_PID} ||= $$;
31              
32 0         0 $self->{+LOG_FILE} = $self->{+DB}->gen_log;
33              
34 0         0 $self->start();
35              
36 0 0       0 weaken($self->{+DB}) if $self->{+MASTER_PID} == $$;
37             }
38              
39             sub start {
40 0     0 0 0 my $self = shift;
41 0 0       0 return if $self->{+SERVER_PID};
42              
43 0         0 my ($rh, $wh);
44 0 0       0 pipe($rh, $wh) or die "Could not open pipe: $!";
45              
46 0         0 my $pid = fork;
47 0 0       0 die "Could not fork: $!" unless defined $pid;
48              
49 0 0       0 if ($pid) {
50 0         0 close($wh);
51 0         0 waitpid($pid, 0);
52 0         0 chomp($self->{+WATCHER_PID} = <$rh>);
53 0         0 chomp($self->{+SERVER_PID} = <$rh>);
54 0         0 close($rh);
55 0 0       0 die "Did not get watcher pid!" unless $self->{+WATCHER_PID};
56 0 0       0 die "Did not get server pid!" unless $self->{+SERVER_PID};
57 0         0 return;
58             }
59              
60 0         0 close($rh);
61 0         0 POSIX::setsid();
62 0         0 setpgrp(0, 0);
63 0         0 $pid = fork;
64 0 0       0 die "Could not fork: $!" unless defined $pid;
65 0 0       0 POSIX::_exit(0) if $pid;
66              
67 0         0 $wh->autoflush(1);
68 0         0 print $wh "$$\n";
69              
70             # In watcher now
71 0 0       0 eval { $self->watch($wh); 1 } or POSIX::_exit(1);
  0         0  
  0         0  
72 0         0 POSIX::_exit(0);
73             }
74              
75             sub watch {
76 0     0 0 0 my $self = shift;
77 0         0 my ($wh) = @_;
78              
79 0         0 $0 = 'db-quick-watcher';
80              
81 0         0 my $kill = '';
82 0         0 my $hup = 0;
83 0     0   0 local $SIG{TERM} = sub { $kill = 'TERM' };
  0         0  
84 0     0   0 local $SIG{INT} = sub { $kill = 'INT' };
  0         0  
85 0     0   0 local $SIG{USR1} = sub { $kill = 'FAST_TERM' };
  0         0  
86 0     0   0 local $SIG{HUP} = sub { $hup = 1 };
  0         0  
87              
88 0         0 my $start_pid = $$;
89 0         0 my $pid = $self->spawn();
90 0         0 print $wh "$pid\n";
91 0         0 close($wh);
92              
93 0         0 my $mpid = $self->{+MASTER_PID};
94 0 0       0 my $spid = $self->{+SERVER_PID} or die "No server pid";
95              
96 0         0 my $ddir = $self->{+DB}->dir;
97 0   0     0 my $ssig = $self->{+DB}->stop_sig // 'TERM';
98 0   0     0 my $fsig = $self->{+DB}->fast_stop_sig // 'KILL';
99              
100             # Ignore SIGTERM/SIGINT before exec so the watcher cannot be killed
101             # during startup before _do_watch installs its signal handlers.
102             # SIG_IGN persists across exec, and any pending signal will be held
103             # until _do_watch replaces these with proper handlers.
104 0         0 $SIG{TERM} = 'IGNORE';
105 0         0 $SIG{INT} = 'IGNORE';
106              
107             # Block (rather than ignore) the fast-eliminate signal across the exec. A
108             # blocked signal stays *pending* instead of being discarded, so a
109             # fast_eliminate() that races server startup -- arriving after the socket is
110             # up (so the caller's start() has returned) but before _do_watch has
111             # installed its handler -- is not lost: _do_watch unblocks it once the
112             # handler is in place and it fires immediately. SIG_IGN would silently drop
113             # it, leaving the caller's wait() to block for the full stop-grace timeout.
114 0         0 POSIX::sigprocmask(POSIX::SIG_BLOCK(), POSIX::SigSet->new(POSIX::SIGUSR1()));
115              
116 0         0 exec(
117             $^X, '-Ilib',
118              
119             '-e' => "require DBIx::QuickDB::Watcher; DBIx::QuickDB::Watcher->_do_watch()",
120              
121             master_pid => $mpid,
122             data_dir => $ddir,
123             server_pid => $spid,
124             signal => $ssig,
125             fast_signal => $fsig,
126             kill => $kill,
127             hup => $hup,
128             );
129             }
130              
131             sub _do_watch {
132 0     0   0 my $class = shift;
133              
134 0         0 $0 = 'db-quick-watcher';
135              
136 0         0 my %params = @ARGV;
137              
138 0   0     0 my $kill = $params{kill} // '';
139 0   0     0 my $hup = $params{hup} // 0;
140 0     0   0 local $SIG{TERM} = sub { $kill = 'TERM' };
  0         0  
141 0     0   0 local $SIG{INT} = sub { $kill = 'INT' };
  0         0  
142 0     0   0 local $SIG{USR1} = sub { $kill = 'FAST_TERM' };
  0         0  
143 0     0   0 local $SIG{HUP} = sub { $hup = 1 };
  0         0  
144              
145             # watch() blocked SIGUSR1 before exec so a fast_eliminate() racing startup
146             # would stay pending rather than be discarded. Now that the handler above is
147             # installed, unblock it -- any pending fast-eliminate fires here and sets
148             # $kill before we enter the watch loop.
149 0         0 POSIX::sigprocmask(POSIX::SIG_UNBLOCK(), POSIX::SigSet->new(POSIX::SIGUSR1()));
150              
151 0         0 my $blah;
152 0         0 close(STDIN);
153 0 0       0 open(STDIN, '<', \$blah) or warn "$!";
154              
155 0 0       0 my $master_pid = $params{master_pid} or die "No master pid provided";
156 0 0       0 my $server_pid = $params{server_pid} or die "No server pid provided";
157 0 0       0 my $data_dir = $params{data_dir} or die "No data dir provided";
158 0   0     0 my $signal = $params{signal} // 'TERM';
159 0   0     0 my $fast_signal = $params{fast_signal} // 'KILL';
160              
161 0         0 my $hupped = 0;
162 0         0 while (!$kill) {
163 0 0 0     0 if ($hup && !$hupped) {
164 0         0 close(STDOUT);
165 0 0       0 open(STDOUT, '>', \$blah) or warn "$!";
166 0         0 close(STDERR);
167 0 0       0 open(STDERR, '>', \$blah) or warn "$!";
168             }
169              
170 0         0 sleep 0.1;
171              
172 0 0       0 next if kill(0, $master_pid);
173 0         0 $kill = 'TERM';
174             }
175              
176 0 0       0 unless (eval { $class->_watcher_terminate(send_sig => $signal, fast_sig => $fast_signal, got_sig => $kill, pid => $server_pid, dir => $data_dir); 1 }) {
  0         0  
  0         0  
177 0         0 my $err = $@;
178 0         0 eval { warn $@ };
  0         0  
179 0         0 POSIX::_exit(1);
180             }
181              
182 0         0 POSIX::_exit(0);
183             }
184              
185             sub spawn {
186 0     0 0 0 my $self = shift;
187              
188 0 0       0 croak "Extra spawn" if $self->{+SERVER_PID};
189              
190 0         0 my $db = $self->{+DB};
191 0   0     0 my $args = $self->{+ARGS} || [];
192              
193 0         0 my $init_pid = $$;
194 0         0 my ($pid, $log_file) = $db->run_command([$db->start_command, @$args], {no_wait => 1, log_file => $self->{+LOG_FILE}});
195 0         0 $self->{+SERVER_PID} = $pid;
196 0         0 $self->{+LOG_FILE} = $log_file;
197              
198 0         0 return $pid;
199             }
200              
201             sub _watcher_terminate {
202 0     0   0 my $class = shift;
203 0         0 my %params = @_;
204              
205 0 0       0 my $pid = $params{pid} or die "No pid";
206 0 0       0 my $dir = $params{dir} or die "No dir";
207              
208 0         0 my $got_sig = $params{got_sig};
209 0   0     0 my $send_sig = $params{send_sig} // $got_sig // 'TERM';
      0        
210              
211             # fast_eliminate(): kill the server immediately with its fast-stop signal
212             # (SIGKILL by default, or a clean immediate-shutdown signal the driver picks
213             # to avoid leaking OS resources), reap it, drop the data dir. No graceful
214             # shutdown -- the data dir is disposable so its integrity does not matter.
215             # Used only for clones being deleted.
216 0 0 0     0 if ($got_sig && $got_sig eq 'FAST_TERM') {
217 0         0 $class->_watcher_kill_fast($pid, $params{fast_sig});
218              
219             # Ignore errors here.
220 0         0 my $err = [];
221 0 0       0 remove_tree($dir, {safe => 1, error => \$err}) if -d $dir;
222              
223 0         0 return;
224             }
225              
226 0         0 $class->_watcher_kill($send_sig, $pid, $params{fast_sig});
227              
228 0 0 0     0 if ($got_sig && $got_sig eq 'TERM') {
229             # Ignore errors here.
230 0         0 my $err = [];
231 0 0       0 remove_tree($dir, {safe => 1, error => \$err}) if -d $dir;
232             }
233             }
234              
235             sub _watcher_kill_fast {
236 3     3   202759 my $class = shift;
237 3         34 my ($pid, $sig) = @_;
238              
239 3   100     123 $sig ||= 'KILL';
240              
241 3 50       139 kill($sig, $pid) or return;
242              
243             # Reap the server. With SIGKILL this resolves almost immediately. With a
244             # clean immediate-shutdown signal (e.g. PostgreSQL's SIGQUIT, chosen so the
245             # postmaster releases its SysV semaphores instead of leaking them) the
246             # server needs a moment to abort its backends and exit; give it a short
247             # window, then escalate to SIGKILL so teardown always completes even if that
248             # signal is caught and ignored. After SIGKILL we block on the reap: SIGKILL
249             # cannot be ignored, so the only thing that can delay the zombie appearing is
250             # transient (kernel delivery, brief uninterruptible IO), and racing a fixed
251             # deadline against it only produces a spurious "PID refused to exit" on a
252             # loaded host.
253 3         30 my $escalated = $sig eq 'KILL';
254 3         34 my ($check, $exit);
255 3         39 my $start = time;
256              
257 3         22 until ($check) {
258 202         1395 local $?;
259              
260 202 100       544 if ($escalated) {
261 2         2415 $check = waitpid($pid, 0);
262 2         15 $exit = $?;
263 2         11 last;
264             }
265              
266 200         1457 $check = waitpid($pid, WNOHANG);
267 200         358 $exit = $?;
268 200 100       519 last if $check;
269              
270 199 100       945 if (time - $start > 2) {
271 1         71 kill('KILL', $pid);
272 1         11 $escalated = 1;
273             }
274              
275 199         2015567 sleep 0.01;
276             }
277              
278 3 50       20 die "PID refused to exit after fast kill" unless $check;
279 3 50       11 die "Something else reaped our process" if $check < 0;
280 3 50       6 die "Reaped the wrong process '$check' instead of '$pid'" if $pid != $check;
281              
282 3         21 return;
283             }
284              
285             sub _watcher_kill {
286 2     2   34461 my $class = shift;
287 2         73 my ($sig, $pid, $fast_sig) = @_;
288              
289 2   50     35 $fast_sig ||= 'KILL';
290              
291 2 50       57 kill($sig, $pid) or die "Could not send kill signal";
292              
293             # How long to wait for a graceful shutdown before escalating, and how much
294             # longer before giving up entirely. Keep this generous: a slow or loaded
295             # host (e.g. a CPAN smoke box) can need well over a few seconds to finish
296             # PostgreSQL's shutdown checkpoint. A premature hard kill leaves the data dir
297             # in a crash-recovery state, and a clone of that dir then replays WAL on
298             # first start, jumping SERIAL sequences forward by SEQ_LOG_VALS (32) --
299             # silently corrupting cloned databases. Tunable via QDB_STOP_GRACE.
300 2         21 my $kill_after = $ENV{QDB_STOP_GRACE};
301 2 50 33     198 $kill_after = 4 unless defined($kill_after) && $kill_after =~ /^\d+$/ && $kill_after > 0;
      33        
302              
303             # Two-stage escalation once the graceful signal has not stopped the server by
304             # $kill_after. First send the driver's fast-stop signal -- an immediate but
305             # *clean* shutdown (e.g. PostgreSQL SIGQUIT) that still lets the server run
306             # its exit cleanup and RELEASE OS resources such as SysV semaphores. Only if
307             # that is also ignored do we SIGKILL. Going straight to SIGKILL here would
308             # orphan those semaphores permanently -- the data dir is about to be deleted
309             # so no future server reuses the IPC key -- and a suite that kills many
310             # servers this way exhausts the host's SEMMNI/SEMMNS limits. When the driver
311             # leaves fast_stop_sig at its 'KILL' default both stages are SIGKILL, which
312             # is harmless.
313 2 50       88 my $step = $kill_after > 1 ? int($kill_after / 2) : 1;
314 2         25 my $fast_at = $kill_after;
315 2         4 my $kill_at = $fast_at + $step;
316              
317 2         4 my ($check, $exit, $sent_fast, $sent_kill);
318 2         32 my $start = time;
319 2         52 until ($check) {
320 33         311 local $?;
321 33         285 my $delta = time - $start;
322              
323 33 100 100     418 if ($delta >= $fast_at && !$sent_fast) {
324 2         175 warn "Server taking too long to shut down, sending SIG$fast_sig";
325 2         65 kill($fast_sig, $pid);
326 2         4 $sent_fast = 1;
327             }
328              
329 33 100 66     253 if ($delta >= $kill_at && !$sent_kill) {
330 1 50       64 warn "Server still running, sending SIGKILL" unless $fast_sig eq 'KILL';
331 1         69 kill('KILL', $pid);
332 1         10 $sent_kill = 1;
333             }
334              
335             # SIGKILL cannot be caught or ignored, so the server WILL terminate and
336             # become reapable -- block until it does instead of racing a wall-clock
337             # deadline. The old give-up window shrank with QDB_STOP_GRACE (only ~1s
338             # at grace=1) and a loaded host could take longer than that just to
339             # deliver the kill and surface the zombie, tripping a spurious "PID
340             # refused to exit". A normal reap returns in well under a second, so the
341             # blocking wait still finishes inside Driver::stop's 2*grace+2 wait()
342             # budget and the watcher is not killed mid-reap.
343 33 100       106 if ($sent_kill) {
344 1         2861 $check = waitpid($pid, 0);
345 1         15 $exit = $?;
346 1         7 last;
347             }
348              
349 32         20770 $check = waitpid($pid, WNOHANG);
350 32         85 $exit = $?;
351              
352 32 100       141 last if $check;
353              
354 31         3104963 sleep 0.1;
355             }
356              
357 2 50       8 die "PID refused to exit" unless $check;
358 2 50       6 die "Something else reaped our process" if $check < 0;
359 2 50       6 die "Reaped the wrong process '$check' instead of '$pid'" if $pid != $check;
360              
361 2         17 return;
362             }
363              
364             # stop(), eliminate(), and detach() each signal the watcher process by pid. Once
365             # ANY terminal teardown has been initiated, the watcher is exiting (or already
366             # gone) and the OS may recycle its pid to an unrelated process -- notably a
367             # sibling database's postmaster. A second signal would then land on the wrong
368             # process and shut down a live server out from under its owner. So once a stop
369             # or eliminate has been sent, never signal this pid again. (Data-dir cleanup for
370             # a stopped database is handled by Driver::DESTROY, not by a second signal.)
371             sub stop {
372 0     0 0   my $self = shift;
373 0 0 0       return if $self->{+STOPPED}++ || $self->{+ELIMINATED};
374 0 0         my $pid = $self->{+WATCHER_PID} or return;
375 0           kill('INT', $pid);
376             }
377              
378             sub eliminate {
379 0     0 0   my $self = shift;
380 0 0 0       return if $self->{+ELIMINATED}++ || $self->{+STOPPED};
381 0 0         my $pid = $self->{+WATCHER_PID} or return;
382 0           kill('TERM', $pid);
383             }
384              
385             # Like eliminate(), but the watcher kills the server immediately with the
386             # driver's fast_stop_sig (SIGKILL by default, or a clean immediate-shutdown
387             # signal) rather than attempting a graceful shutdown. Sets ELIMINATED so the
388             # normal teardown signals are never also sent to this (possibly
389             # soon-to-be-recycled) pid.
390             sub fast_eliminate {
391 0     0 0   my $self = shift;
392 0 0 0       return if $self->{+ELIMINATED}++ || $self->{+STOPPED};
393 0 0         my $pid = $self->{+WATCHER_PID} or return;
394 0           kill('USR1', $pid);
395             }
396              
397             sub detach {
398 0     0 0   my $self = shift;
399 0 0         return if $self->{+DETACHED}++;
400 0 0 0       return if $self->{+STOPPED} || $self->{+ELIMINATED};
401 0 0         my $pid = $self->{+WATCHER_PID} or return;
402 0           kill('HUP', $pid);
403             }
404              
405             sub wait {
406 0     0 0   my $self = shift;
407 0 0         my $pid = $self->{+WATCHER_PID} or return;
408              
409             # Give the watcher long enough to finish a graceful shutdown before we
410             # SIGKILL it. The watcher escalates to SIGKILL on the server after
411             # QDB_STOP_GRACE and gives up at twice that, so this must outlast the
412             # watcher's give-up or we would kill it mid-shutdown and orphan a
413             # half-stopped server. Defaults to 10s (grace 4 -> give-up 8 -> 10).
414 0           my $grace = $ENV{QDB_STOP_GRACE};
415 0 0 0       $grace = 4 unless defined($grace) && $grace =~ /^\d+$/ && $grace > 0;
      0        
416 0           my $timeout = $grace * 2 + 2;
417              
418 0           my $start = time;
419 0           while(kill(0, $pid)) {
420 0           my $waited = time - $start;
421 0 0         if ($waited > $timeout) {
422 0           kill('KILL', $pid);
423 0           $start = time;
424             }
425 0           sleep 0.02;
426             }
427              
428             # The watcher has exited; forget its pid so no later teardown signal (e.g.
429             # from DESTROY) can land on a recycled pid now owned by another process.
430 0           delete $self->{+WATCHER_PID};
431             }
432              
433             sub DESTROY {
434 0     0     my $self = shift;
435              
436 0 0         if ($self->{+MASTER_PID} == $$) {
437 0           $self->eliminate;
438 0           $self->wait;
439             }
440             else {
441 0 0         unlink($self->{+LOG_FILE}) if $self->{+LOG_FILE};
442             }
443             }
444              
445             1;
446              
447             __END__