DBIx-QuickDB
view release on metacpan or search on metacpan
t/watcher_fast_kill.t view on Meta::CPAN
use strict;
use warnings;
use Test2::V0;
use POSIX ();
use File::Temp qw/tempdir/;
use Time::HiRes qw/time sleep/;
use DBIx::QuickDB::Watcher;
use DBIx::QuickDB::Driver;
use DBIx::QuickDB::Driver::PostgreSQL;
# _watcher_kill_fast() backs the fast/disposable teardown. It must:
# - send the requested signal (not always SIGKILL) so a driver can pick a
# clean immediate-shutdown signal that releases OS resources, and
# - escalate to SIGKILL if that signal does not stop the server promptly,
# so teardown always completes.
# These need fork() and real signals; skip where that does not apply.
skip_all "fork/POSIX signals not supported on $^O" if $^O eq 'MSWin32';
my $tmp = tempdir(CLEANUP => 1);
sub pid_alive { my $p = shift; return kill(0, $p) ? 1 : 0 }
# Fork a child that installs $disposition for SIGQUIT, announces readiness by
# creating $tmp/ready-$$, and otherwise sleeps forever.
sub spawn_child {
my ($disposition) = @_;
my $pid = fork;
die "fork failed: $!" unless defined $pid;
if (!$pid) {
$SIG{QUIT} = $disposition;
open(my $fh, '>', "$tmp/ready-$$") or POSIX::_exit(1);
close($fh);
sleep 0.05 while 1;
POSIX::_exit(0);
}
# Parent: wait until the child has installed its handler.
my $start = time;
until (-e "$tmp/ready-$pid") {
die "child never became ready" if time - $start > 5;
sleep 0.01;
}
return $pid;
}
subtest custom_signal_used => sub {
# A child that exits cleanly on SIGQUIT must be stopped by SIGQUIT itself
# (not a SIGKILL), proving the requested signal is what gets sent.
my $pid = fork;
die "fork failed: $!" unless defined $pid;
if (!$pid) {
$SIG{QUIT} = sub { open(my $f, '>', "$tmp/got-quit"); close($f); POSIX::_exit(0) };
open(my $fh, '>', "$tmp/ready-$$") or POSIX::_exit(1);
close($fh);
sleep 0.05 while 1;
POSIX::_exit(0);
}
my $start = time;
until (-e "$tmp/ready-$pid") { die "not ready" if time - $start > 5; sleep 0.01 }
DBIx::QuickDB::Watcher->_watcher_kill_fast($pid, 'QUIT');
ok(-e "$tmp/got-quit", "child handled SIGQUIT (requested signal was sent, not SIGKILL)");
ok(!pid_alive($pid), "child was reaped");
};
subtest escalates_to_sigkill => sub {
# A child that ignores SIGQUIT must still be reaped: _watcher_kill_fast
# escalates to SIGKILL after its grace window.
my $pid = spawn_child('IGNORE');
my $start = time;
ok(lives { DBIx::QuickDB::Watcher->_watcher_kill_fast($pid, 'QUIT') },
"_watcher_kill_fast reaped a process that ignores the requested signal")
or diag($@);
my $elapsed = time - $start;
ok(!pid_alive($pid), "child gone after escalation to SIGKILL");
ok($elapsed < 5, "escalation happened within the grace window (${elapsed}s)");
};
subtest default_is_sigkill => sub {
# No signal argument: defaults to SIGKILL, which cannot be caught.
my $pid = spawn_child('IGNORE');
DBIx::QuickDB::Watcher->_watcher_kill_fast($pid);
ok(!pid_alive($pid), "default SIGKILL reaped the child");
};
subtest driver_fast_stop_sig => sub {
is(DBIx::QuickDB::Driver->fast_stop_sig, 'KILL',
"base driver fast_stop_sig defaults to SIGKILL");
is(DBIx::QuickDB::Driver::PostgreSQL->fast_stop_sig, 'QUIT',
"PostgreSQL fast_stop_sig is SIGQUIT (immediate shutdown releases SysV semaphores)");
};
# The GRACEFUL teardown path (_watcher_kill, used by stop()/eliminate()) must
# also escalate through the driver's fast_stop_sig BEFORE SIGKILL, so a server
# that ignores the polite stop signal is still given its clean immediate-stop
# signal (which releases SysV semaphores) instead of being SIGKILLed outright
# and leaking them.
subtest graceful_kill_escalates_via_fast_sig => sub {
local $ENV{QDB_STOP_GRACE} = 1; # fast_at=1s, kill_at=2s, give_up=2s
local $SIG{__WARN__} = sub { }; # silence the expected escalation warnings
# Child ignores SIGTERM (the polite stop) but exits cleanly on SIGQUIT, the
# way PostgreSQL's immediate-shutdown lets the postmaster release its
# semaphores. It must be stopped by SIGQUIT, never reaching SIGKILL.
my $pid = fork;
die "fork failed: $!" unless defined $pid;
if (!$pid) {
$SIG{TERM} = 'IGNORE';
$SIG{QUIT} = sub { open(my $f, '>', "$tmp/grace-quit"); close($f); POSIX::_exit(0) };
open(my $fh, '>', "$tmp/ready-$$") or POSIX::_exit(1);
close($fh);
sleep 0.05 while 1;
POSIX::_exit(0);
}
my $w = time;
until (-e "$tmp/ready-$pid") { die "not ready" if time - $w > 5; sleep 0.01 }
my $start = time;
ok(lives { DBIx::QuickDB::Watcher->_watcher_kill('TERM', $pid, 'QUIT') },
"_watcher_kill reaped a server that ignores the polite stop signal") or diag($@);
my $elapsed = time - $start;
ok(-e "$tmp/grace-quit", "graceful escalation sent the fast_stop_sig (SIGQUIT), not a bare SIGKILL");
ok(!pid_alive($pid), "child reaped");
ok($elapsed < 5, "escalation happened within the grace window (${elapsed}s)");
};
# If even the fast_stop_sig is ignored, _watcher_kill must still escalate to
# SIGKILL so teardown always completes.
subtest graceful_kill_escalates_to_sigkill => sub {
local $ENV{QDB_STOP_GRACE} = 1;
local $SIG{__WARN__} = sub { };
my $pid = fork;
die "fork failed: $!" unless defined $pid;
if (!$pid) {
$SIG{TERM} = 'IGNORE';
$SIG{QUIT} = 'IGNORE';
open(my $fh, '>', "$tmp/ready-$$") or POSIX::_exit(1);
close($fh);
sleep 0.05 while 1;
POSIX::_exit(0);
}
my $w = time;
until (-e "$tmp/ready-$pid") { die "not ready" if time - $w > 5; sleep 0.01 }
ok(lives { DBIx::QuickDB::Watcher->_watcher_kill('TERM', $pid, 'QUIT') },
"_watcher_kill reaped a server that ignores both stop and fast_stop signals") or diag($@);
ok(!pid_alive($pid), "child gone after escalation to SIGKILL");
};
done_testing;
( run in 0.819 second using v1.01-cache-2.11-cpan-df04353d9ac )