view release on metacpan or search on metacpan
- t/Pool wraps every server-spawning db()/fetch_db() in the resource-skip helper so a semaphore-exhausted host skips instead of failing.
0.000049 2026-06-09 17:14:55-07:00 America/Los_Angeles
- Added Driver::destroy_quietly(): fast-destroy teardown for disposable clones (cleanup => 1) that kills, reaps, and removes the data dir immediately instead of a graceful shutdown that can block 2*QDB_STOP_GRACE+2 seconds.
- Added an opt-in fast_destroy attribute: DESTROY uses destroy_quietly() when cleanup => 1, falls through to the graceful path when cleanup => 0, and is inherited by clones via clone_data().
- Added per-driver fast_stop_sig() for fast/forced teardown: default SIGKILL, PostgreSQL overrides to SIGQUIT so the postmaster releases its SysV semaphores instead of leaking them; watcher escalates to SIGKILL if it does not stop promptly.
- Added a watcher fast-eliminate request (SIGUSR1) that kills with fast_stop_sig, reaps, and deletes the data dir; blocked (not ignored) across the startup exec so a request racing startup stays pending instead of being dropped.
- The graceful teardown path (stop()/eliminate()) now escalates a stuck shutdown through fast_stop_sig before SIGKILL, so it releases SysV semaphores instead of leaking them.
- get_db() and the pool tests skip_all instead of failing when a host is out of System V semaphores/shared memory; added skipall_on_resource_error(), also covering the DBIx::QuickDB->import path (t/QuickDB) and mid-run pool clone builds (t/Pool).
- On a start timeout, start() now reports the server's launch log and whether its process is still alive, instead of only the often-empty error log.
- Added t/fast_destroy.t, t/watcher_fast_kill.t, and t/resource_skip.t.
0.000048 2026-06-06 10:53:27-07:00 America/Los_Angeles
- Fixed a pid-reuse race that could shut down the wrong database server. After a server was stopped, its watcher exited and the OS could recycle the watcher's pid; a later teardown signal (e.g. eliminate() from the watcher object's DESTROY, which...
- Removed two redundant pid-signalling paths that carried the same pid-reuse hazard: the post-wait() server-pid poll in Driver::stop() (the watcher already reaps the server before it exits, so stop() now just trusts wait()) and the direct server-...
- Added t/clone_sequence.t, an explicit PostgreSQL regression asserting a clone of a stopped source continues its SERIAL sequence with the exact next id (no SEQ_LOG_VALS +32 jump); the broad Pool test only checks id ordering
0.000047 2026-06-05 22:35:47-07:00 America/Los_Angeles
lib/DBIx/QuickDB/Driver.pm view on Meta::CPAN
until (-S $socket) {
my $waited = time - $start;
if ($waited > $timeout) {
# Capture diagnostics BEFORE eliminate() removes the data dir (which
# holds both the error log and the watcher's launch log). The server
# process's own stdout/stderr go to the watcher's log_file, not the
# driver's error_log, so a server that died (or never launched)
# before writing error_log leaves error_log showing only inherited
# template history -- the real failure is in the launch log. Also
# report whether the server pid is still alive: "not running" points
# at a launch/early-exit failure, "alive" at a slow or hung startup.
my $spid = $watcher->server_pid;
my $alive = ($spid && kill(0, $spid)) ? "alive (pid $spid)" : "not running";
my $error_log = $self->read_error_log;
my $launch_log = $self->_read_file($watcher->log_file);
$watcher->eliminate();
my $msg = "Timed out waiting for server to start after ${timeout}s; server process is $alive.";
$msg .= "\n=== server launch log ===\n$launch_log" if length $launch_log;
$msg .= "\n=== error log ===\n$error_log" if length $error_log;
confess $msg;
}
sleep 0.01;
}
return;
}
t/Drivers/MariaDB.t view on Meta::CPAN
"Original DB not changed"
);
};
subtest cleanup => sub {
my $db = get_db {driver => 'MariaDB', load_sql => [quickdb => 't/schema/mariadb.sql']};
my $dir = $db->dir;
my $pid = $db->watcher->server_pid;
ok(-d $dir, "Can see the db dir");
ok(kill(0, $pid), "Can signal the db process (it's alive!)");
$db = undef;
my $start = time;
my $pid_gone = 0;
my $dir_gone = 0;
while (1) {
$pid_gone ||= !kill(0, $pid);
$dir_gone ||= !-d $dir;
last if $pid_gone && $dir_gone;
t/Drivers/MySQL.t view on Meta::CPAN
"Original DB not changed"
);
};
subtest cleanup => sub {
my $db = get_db {driver => 'MySQL', load_sql => [quickdb => 't/schema/mysql.sql']};
my $dir = $db->dir;
my $pid = $db->watcher->server_pid;
ok(-d $dir, "Can see the db dir");
ok(kill(0, $pid), "Can signal the db process (it's alive!)");
$db = undef;
my $start = time;
my $pid_gone = 0;
my $dir_gone = 0;
while (1) {
$pid_gone ||= !kill(0, $pid);
$dir_gone ||= !-d $dir;
last if $pid_gone && $dir_gone;
t/Drivers/MySQLCom.t view on Meta::CPAN
"Original DB not changed"
);
};
subtest cleanup => sub {
my $db = get_db {driver => 'MySQLCom', load_sql => [quickdb => 't/schema/mysql.sql']};
my $dir = $db->dir;
my $pid = $db->watcher->server_pid;
ok(-d $dir, "Can see the db dir");
ok(kill(0, $pid), "Can signal the db process (it's alive!)");
$db = undef;
my $start = time;
my $pid_gone = 0;
my $dir_gone = 0;
while (1) {
$pid_gone ||= !kill(0, $pid);
$dir_gone ||= !-d $dir;
last if $pid_gone && $dir_gone;
t/Drivers/Percona.t view on Meta::CPAN
"Original DB not changed"
);
};
subtest cleanup => sub {
my $db = get_db {driver => 'Percona', load_sql => [quickdb => 't/schema/percona.sql']};
my $dir = $db->dir;
my $pid = $db->watcher->server_pid;
ok(-d $dir, "Can see the db dir");
ok(kill(0, $pid), "Can signal the db process (it's alive!)");
$db = undef;
my $start = time;
my $pid_gone = 0;
my $dir_gone = 0;
while (1) {
$pid_gone ||= !kill(0, $pid);
$dir_gone ||= !-d $dir;
last if $pid_gone && $dir_gone;
t/Drivers/PostgreSQL.t view on Meta::CPAN
"Original DB not changed"
);
};
subtest cleanup => sub {
my $db = get_db {driver => 'PostgreSQL', load_sql => [quickdb => 't/schema/postgresql.sql']};
my $dir = $db->dir;
my $pid = $db->watcher->server_pid;
ok(-d $dir, "Can see the db dir");
ok(kill(0, $pid), "Can signal the db process (it's alive!)");
$db = undef;
my $start = time;
my $pid_gone = 0;
my $dir_gone = 0;
while (1) {
$pid_gone ||= !kill(0, $pid);
$dir_gone ||= !-d $dir;
last if $pid_gone && $dir_gone;
t/Pool/Pool.pm view on Meta::CPAN
push @out, " socket: " . (defined $sock ? $sock : '?')
. " exists=" . (defined $sock && -e $sock ? 1 : 0)
. " is_socket=" . (defined $sock && -S $sock ? 1 : 0);
push @out, " started(): " . (eval { $db->started } ? 1 : 0);
my $w = $db->{ +DBIx::QuickDB::Driver::WATCHER() };
if ($w) {
my $spid = eval { $w->server_pid };
push @out, " server_pid: " . (defined $spid ? $spid : '?')
. " alive=" . (defined $spid && kill(0, $spid) ? 1 : 0);
}
else {
push @out, " watcher: none (object thinks it is stopped)";
}
my $log = eval { $db->error_log };
if ($log && -f $log) {
open(my $fh, '<', $log) or push(@out, " error.log: open failed: $!");
if ($fh) {
my @l = <$fh>;
t/Pool/Pool.pm view on Meta::CPAN
# sources it was cloned from, then rethrow so the test still fails.
sub diag_connect {
my ($db, $label, @others) = @_;
my $dbh;
return $dbh if eval { $dbh = $db->connect(); 1 };
my $err = $@;
print STDERR "==== QDB-DIAG connect FAILED for [$label]: $err\n";
qdb_diag($db, $label);
qdb_diag($_->[1], $_->[0]) for @others;
# Global snapshot: which postgres processes are alive (orphans?), and the
# System V IPC objects in play -- the prime suspect for this 9.3 race.
my $ps = eval { `ps -axww 2>/dev/null | grep '[p]ostgres'` };
print STDERR "==== QDB-DIAG postgres processes ====\n", (length($ps // '') ? $ps : " (none)\n");
my $ipcs = eval { `ipcs -a 2>/dev/null` };
print STDERR "==== QDB-DIAG ipcs -a ====\n", (length($ipcs // '') ? $ipcs : " (none)\n");
die $err;
}
ok($driver, "Got a driver ($driver)") or die "Cannot continue without a driver";
t/fast_destroy.t view on Meta::CPAN
# SQLite driver is viable, so there is nothing to exercise here. The PostgreSQL
# requirement below would skip anyway; this is an explicit, faster guard.
skip_all "fast destroy relies on the Unix-only watcher (no POSIX signals on $^O)"
if $^O eq 'MSWin32';
my $db = get_db_or_skipall({driver => 'PostgreSQL'});
# clone() requires a stopped source.
$db->stop if $db->started;
sub pid_alive { my $pid = shift; return 0 unless $pid; return kill(0, $pid) ? 1 : 0 }
subtest destroy_quietly_basic => sub {
my $clone = $db->clone(autostart => 1, cleanup => 1);
my $dir = $clone->dir;
my $watcher = $clone->watcher;
my $spid = $watcher->server_pid;
my $wpid = $watcher->watcher_pid;
ok(pid_alive($spid), "server is alive before destroy_quietly");
ok(-d $dir, "data dir exists before destroy_quietly");
# Big grace: if destroy_quietly used the graceful path this would block for
# tens of seconds. It must not.
local $ENV{QDB_STOP_GRACE} = 30;
my $start = time;
$clone->destroy_quietly;
my $elapsed = time - $start;
# Threshold is generous (grace=30 means the graceful path would block up to
# 2*30+2=62s) so a loaded host running this suite under prove -j8 does not
# flake; it still proves the grace wait was skipped entirely.
ok($elapsed < 15, "destroy_quietly did not wait for QDB_STOP_GRACE (${elapsed}s)");
# Watcher reaps the server before exiting; once the watcher is gone the
# server is gone too.
my $gone_start = time;
while (pid_alive($wpid) || pid_alive($spid)) {
last if time - $gone_start > 15;
select(undef, undef, undef, 0.02);
}
ok(!pid_alive($spid), "server process is gone after destroy_quietly");
ok(!pid_alive($wpid), "watcher process is gone after destroy_quietly");
ok(!-d $dir, "data dir removed after destroy_quietly");
};
subtest no_checkpoint_no_stop => sub {
my $clone = $db->clone(autostart => 1, cleanup => 1);
our ($checkpoint_called, $stop_called) = (0, 0);
{
no warnings 'once';
package Spy::FastDestroy;
t/fast_destroy.t view on Meta::CPAN
my $start = time;
undef $clone;
my $elapsed = time - $start;
# Generous threshold (vs grace=30 -> up to 62s graceful) to stay robust
# under prove -j8 load while still proving the fast path was taken.
ok($elapsed < 15, "DESTROY used fast path, no QDB_STOP_GRACE wait (${elapsed}s)");
my $gone = time;
while (pid_alive($wpid) || pid_alive($spid)) {
last if time - $gone > 15;
select(undef, undef, undef, 0.02);
}
ok(!pid_alive($spid), "server gone after fast DESTROY");
ok(!pid_alive($wpid), "watcher gone after fast DESTROY");
ok(!-d $dir, "data dir removed after fast DESTROY");
};
subtest fast_destroy_attr_inherited_by_clone => sub {
# The attribute must propagate through clone_data() so a clone of a
# fast_destroy source is itself fast_destroy.
my $clone = $db->clone(cleanup => 1, fast_destroy => 1);
ok($clone->fast_destroy, "clone carries fast_destroy");
my %data = $clone->clone_data;
t/watcher_fast_kill.t view on Meta::CPAN
# _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) {
t/watcher_fast_kill.t view on Meta::CPAN
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
t/watcher_fast_kill.t view on Meta::CPAN
}
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;
t/watcher_fast_kill.t view on Meta::CPAN
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;