File Coverage

blib/lib/DBIx/QuickDB/Watcher.pm
Criterion Covered Total %
statement 78 235 33.1
branch 23 118 19.4
condition 14 68 20.5
subroutine 10 30 33.3
pod 0 9 0.0
total 125 460 27.1


line stmt bran cond sub pod time code
1             package DBIx::QuickDB::Watcher;
2 26     26   172084 use strict;
  26         41  
  26         780  
3 26     26   84 use warnings;
  26         54  
  26         1398  
4              
5             our $VERSION = '0.000049';
6              
7 26     26   103 use Carp qw/croak/;
  26         45  
  26         1154  
8 26     26   111 use POSIX qw/:sys_wait_h/;
  26         70  
  26         163  
9 26     26   4934 use Time::HiRes qw/sleep time/;
  26         47  
  26         202  
10 26     26   1490 use Scalar::Util qw/weaken/;
  26         72  
  26         996  
11 26     26   112 use File::Path qw/remove_tree/;
  26         43  
  26         1153  
12              
13 26         178 use DBIx::QuickDB::Util::HashBase qw{
14            
15            
16            
17            
18            
19              
20            
21            
22            
23              
24            
25 26     26   10942 };
  26         82  
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   200164 my $class = shift;
237 3         34 my ($pid, $sig) = @_;
238              
239 3   100     152 $sig ||= 'KILL';
240              
241 3 50       124 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. The outer cap guards pathological cases
249             # (e.g. a process stuck in uninterruptible IO).
250 3         39 my $escalated = $sig eq 'KILL';
251 3         39 my ($check, $exit);
252 3         26 my $start = time;
253              
254 3         25 until ($check) {
255 203         1420 local $?;
256              
257 203         1586 $check = waitpid($pid, WNOHANG);
258 203         526 $exit = $?;
259 203 100       484 last if $check;
260              
261 200         824 my $delta = time - $start;
262              
263 200 100 100     1161 if (!$escalated && $delta > 2) {
264 1         48 kill('KILL', $pid);
265 1         47 $escalated = 1;
266             }
267              
268 200 50       386 last if $delta > 5;
269              
270 200         2025170 sleep 0.01;
271             }
272              
273 3 50       14 die "PID refused to exit after fast kill" unless $check;
274 3 50       13 die "Something else reaped our process" if $check < 0;
275 3 50       9 die "Reaped the wrong process '$check' instead of '$pid'" if $pid != $check;
276              
277 3         34 return;
278             }
279              
280             sub _watcher_kill {
281 2     2   32592 my $class = shift;
282 2         43 my ($sig, $pid, $fast_sig) = @_;
283              
284 2   50     31 $fast_sig ||= 'KILL';
285              
286 2 50       67 kill($sig, $pid) or die "Could not send kill signal";
287              
288             # How long to wait for a graceful shutdown before escalating, and how much
289             # longer before giving up entirely. Keep this generous: a slow or loaded
290             # host (e.g. a CPAN smoke box) can need well over a few seconds to finish
291             # PostgreSQL's shutdown checkpoint. A premature hard kill leaves the data dir
292             # in a crash-recovery state, and a clone of that dir then replays WAL on
293             # first start, jumping SERIAL sequences forward by SEQ_LOG_VALS (32) --
294             # silently corrupting cloned databases. Tunable via QDB_STOP_GRACE.
295 2         38 my $kill_after = $ENV{QDB_STOP_GRACE};
296 2 50 33     175 $kill_after = 4 unless defined($kill_after) && $kill_after =~ /^\d+$/ && $kill_after > 0;
      33        
297              
298             # Two-stage escalation once the graceful signal has not stopped the server by
299             # $kill_after. First send the driver's fast-stop signal -- an immediate but
300             # *clean* shutdown (e.g. PostgreSQL SIGQUIT) that still lets the server run
301             # its exit cleanup and RELEASE OS resources such as SysV semaphores. Only if
302             # that is also ignored do we SIGKILL. Going straight to SIGKILL here would
303             # orphan those semaphores permanently -- the data dir is about to be deleted
304             # so no future server reuses the IPC key -- and a suite that kills many
305             # servers this way exhausts the host's SEMMNI/SEMMNS limits. When the driver
306             # leaves fast_stop_sig at its 'KILL' default both stages are SIGKILL, which
307             # is harmless. A final reap window after the SIGKILL lets it take effect
308             # before we give up; the whole budget stays under Driver::stop's wait()
309             # timeout (2*grace+2) so the watcher is not killed mid-reap.
310 2 50       22 my $step = $kill_after > 1 ? int($kill_after / 2) : 1;
311 2         21 my $fast_at = $kill_after;
312 2         78 my $kill_at = $fast_at + $step;
313 2         28 my $give_up = $kill_at + $step;
314              
315 2         18 my ($check, $exit, $sent_fast, $sent_kill);
316 2         24 my $start = time;
317 2         15 until ($check) {
318 34         338 local $?;
319 34         226 my $delta = time - $start;
320              
321 34 100 100     301 if ($delta >= $fast_at && !$sent_fast) {
322 2         119 warn "Server taking too long to shut down, sending SIG$fast_sig";
323 2         102 kill($fast_sig, $pid);
324 2         4 $sent_fast = 1;
325             }
326              
327 34 100 100     168 if ($delta >= $kill_at && !$sent_kill) {
328 1 50       83 warn "Server still running, sending SIGKILL" unless $fast_sig eq 'KILL';
329 1         61 kill('KILL', $pid);
330 1         24 $sent_kill = 1;
331             }
332              
333 34         545 $check = waitpid($pid, WNOHANG);
334 34         135 $exit = $?;
335              
336 34 100       19982 last if $check;
337 32 50       218 last if $delta > $give_up;
338              
339 32         3204852 sleep 0.1;
340             }
341              
342 2 50       10 die "PID refused to exit" unless $check;
343 2 50       25 die "Something else reaped our process" if $check < 0;
344 2 50       9 die "Reaped the wrong process '$check' instead of '$pid'" if $pid != $check;
345              
346 2         19 return;
347             }
348              
349             # stop(), eliminate(), and detach() each signal the watcher process by pid. Once
350             # ANY terminal teardown has been initiated, the watcher is exiting (or already
351             # gone) and the OS may recycle its pid to an unrelated process -- notably a
352             # sibling database's postmaster. A second signal would then land on the wrong
353             # process and shut down a live server out from under its owner. So once a stop
354             # or eliminate has been sent, never signal this pid again. (Data-dir cleanup for
355             # a stopped database is handled by Driver::DESTROY, not by a second signal.)
356             sub stop {
357 0     0 0   my $self = shift;
358 0 0 0       return if $self->{+STOPPED}++ || $self->{+ELIMINATED};
359 0 0         my $pid = $self->{+WATCHER_PID} or return;
360 0           kill('INT', $pid);
361             }
362              
363             sub eliminate {
364 0     0 0   my $self = shift;
365 0 0 0       return if $self->{+ELIMINATED}++ || $self->{+STOPPED};
366 0 0         my $pid = $self->{+WATCHER_PID} or return;
367 0           kill('TERM', $pid);
368             }
369              
370             # Like eliminate(), but the watcher kills the server immediately with the
371             # driver's fast_stop_sig (SIGKILL by default, or a clean immediate-shutdown
372             # signal) rather than attempting a graceful shutdown. Sets ELIMINATED so the
373             # normal teardown signals are never also sent to this (possibly
374             # soon-to-be-recycled) pid.
375             sub fast_eliminate {
376 0     0 0   my $self = shift;
377 0 0 0       return if $self->{+ELIMINATED}++ || $self->{+STOPPED};
378 0 0         my $pid = $self->{+WATCHER_PID} or return;
379 0           kill('USR1', $pid);
380             }
381              
382             sub detach {
383 0     0 0   my $self = shift;
384 0 0         return if $self->{+DETACHED}++;
385 0 0 0       return if $self->{+STOPPED} || $self->{+ELIMINATED};
386 0 0         my $pid = $self->{+WATCHER_PID} or return;
387 0           kill('HUP', $pid);
388             }
389              
390             sub wait {
391 0     0 0   my $self = shift;
392 0 0         my $pid = $self->{+WATCHER_PID} or return;
393              
394             # Give the watcher long enough to finish a graceful shutdown before we
395             # SIGKILL it. The watcher escalates to SIGKILL on the server after
396             # QDB_STOP_GRACE and gives up at twice that, so this must outlast the
397             # watcher's give-up or we would kill it mid-shutdown and orphan a
398             # half-stopped server. Defaults to 10s (grace 4 -> give-up 8 -> 10).
399 0           my $grace = $ENV{QDB_STOP_GRACE};
400 0 0 0       $grace = 4 unless defined($grace) && $grace =~ /^\d+$/ && $grace > 0;
      0        
401 0           my $timeout = $grace * 2 + 2;
402              
403 0           my $start = time;
404 0           while(kill(0, $pid)) {
405 0           my $waited = time - $start;
406 0 0         if ($waited > $timeout) {
407 0           kill('KILL', $pid);
408 0           $start = time;
409             }
410 0           sleep 0.02;
411             }
412              
413             # The watcher has exited; forget its pid so no later teardown signal (e.g.
414             # from DESTROY) can land on a recycled pid now owned by another process.
415 0           delete $self->{+WATCHER_PID};
416             }
417              
418             sub DESTROY {
419 0     0     my $self = shift;
420              
421 0 0         if ($self->{+MASTER_PID} == $$) {
422 0           $self->eliminate;
423 0           $self->wait;
424             }
425             else {
426 0 0         unlink($self->{+LOG_FILE}) if $self->{+LOG_FILE};
427             }
428             }
429              
430             1;
431              
432             __END__