DBIx-QuickDB
view release on metacpan or search on metacpan
rather than rebuilding on top of it and failing with "table already
exists".
0.000056 2026-07-21 11:35:25-07:00 America/Los_Angeles
- Added a no_disconnect_handles driver attribute (inheritable by clones) that skips the teardown-time DBI handle disconnect, for callers that hold their own live handles to the db being stopped (e.g. MySQL, which shuts down fine with clients conn...
0.000055 2026-07-14 12:20:42-07:00 America/Los_Angeles
- Data-dir cleanup is now robust on Windows: remove_tree could leave a SQLite data dir non-empty on MSWin32 (the OS releases file handles asynchronously, so an immediate rmdir gets ENOTEMPTY), which left the stale db behind and made the rebuilt i...
- Driver cleanup() now disconnects this process's DBI handles before deleting the data dir; the watcher teardown paths already did, but the watcher-less path (SQLite, whose stop() is a no-op) reached cleanup() with handles still open, which on Wi...
0.000054 2026-07-13 10:53:23-07:00 America/Los_Angeles
0.000053 2026-07-10 11:07:52-07:00 America/Los_Angeles
- Temp dir names now include the pid: File::Temp derives names from rand(), so forked or identically-seeded processes (e.g. under Test2::Plugin::SRand's date seed) drew identical DB-QUICK-* name sequences and could converge on one directory, corr...
- MySQL-family servers are now started with loose-mysqlx=OFF; the X Plugin defaults ON with a global default socket (/tmp/mysqlx.sock) and TCP port (33060), so concurrent instances contended on shared resources despite per-instance datadirs.
- Fixed stop()/eliminate() signals being silently lost when they raced watcher startup: SIGTERM/SIGINT were SIG_IGN'd (discarded) across the watcher's exec instead of blocked (held pending) like SIGUSR1 already was, so a stop landing in that wind...
- Watcher::wait no longer kills a watcher that is legitimately blocked reaping an unkillable (e.g. disk-sleep) server, which orphaned a live server and let stop() return while the data dir was still locked; it now waits longer (QDB_STOP_LEASH), a...
- Raised the default QDB_STOP_GRACE from 4 to 10 seconds and dropped the Pool tests' small-grace override: a normal MySQL 8 or loaded-host PostgreSQL clean shutdown routinely exceeds 4-5s, making escalation warnings and needless SIGKILLs (plus In...
- PostgreSQL instances now run with autovacuum=off by default: on PostgreSQL <= 14 the autovacuum launcher could stall shutdown for 10s (PGSTAT_MAX_WAIT_TIME) waiting on the stats collector, routinely blowing the stop grace and drawing SIGQUIT es...
- DESTROY/eliminate teardown now disconnects this process's DBI handles first (like stop() already did); a connected handle made PostgreSQL's smart shutdown wait out the whole stop grace.
- Clones now start with fresh logs; clone_dir copied the source's error.log and cmd-log-* files, so a clone's logs appeared to contain the parent's history.
- t/watcher_fast_kill.t child-readiness now uses pipe synchronization instead of polling for a marker file under a 5s deadline a loaded smoker could blow.
- t/watcher_fast_kill.t: widened the SIGQUIT-to-SIGKILL escalation gap (grace 1 -> 4) in the fast-sig subtest; on slow smokers the child was not scheduled in time to run its QUIT handler before SIGKILL landed, spuriously failing the marker assert...
- Data-dir deletion no longer aborts the process when the watcher and the owning process race to delete the same directory; File::Path's "cannot chdir ... aborting" die bypasses its error handler, and turned otherwise-passing FreeBSD smoker runs ...
- The Drivers/QuickDB/Pool tests now run once per database installation, forking a subprocess per install; with AUTHOR_TESTING set the test helpers live-scan ~/dbs/*/bin for developer installs in addition to the system install (nothing in lib/ co...
- Removed the stale hardcoded ~/dbs PATH prepends from the tests; added AGENTS.md with the developer testing protocol.
- t/Drivers tests now quote the -target class name; the barewords failed to compile under Test2::Harness (yath), which enables strict before line 1.
0.000050 2026-06-10 14:14:43-07:00 America/Los_Angeles
- Watcher teardown blocks on the reap after sending SIGKILL instead of racing a deadline that was too short at low QDB_STOP_GRACE.
- 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
lib/DBIx/QuickDB/Pool.pm view on Meta::CPAN
my ($age) = @_;
my $dir = $self->{+CACHE_DIR};
opendir(my $dh, $dir) or die "Could not open cache dir '$dir': $!";
for my $name (readdir($dh)) {
my $full = "$dir/$name";
# Quarantines sit outside the canonical cache path (see
# _remove_or_quarantine). Retry their removal on every pass: the
# transient Windows file lock that blocked deletion may have cleared.
if ($name =~ m/\.STALE-/) {
remove_tree_robust($full) if -d $full;
next;
}
# A live cache entry is a bare directory name. Its marker files
# ("$name.lock", "$name.READY") do contain dots and are handled
# through their entry, never scanned directly.
next if $name =~ m/\./;
lib/DBIx/QuickDB/Pool.pm view on Meta::CPAN
$stamp = 0 unless $stamp =~ /^\d+\z/;
next unless $age <= (time - $stamp);
eval {
# An expired SQLite source may still have DBI handles retained by
# application/test code. On Windows those handles prevent unlinking
# the database file. Close every matching handle before removal.
disconnect_dbi_handles($full);
# If removal remains blocked, moving the tree out of its canonical
# path keeps a later build from seeing partially-removed SQLite
# schema/data. Either way, invalidate the external markers: expiry
# was explicitly requested, and build_db() will refuse to build on
# any stubborn tree that could not be quarantined.
$self->_remove_or_quarantine($full);
unlink("$full.lock") if -e "$full.lock";
unlink("$full.READY") if -e "$full.READY";
1;
} or warn $@;
}
lib/DBIx/QuickDB/Watcher.pm view on Meta::CPAN
my $ddir = $self->{+DATA_DIR};
my $ssig = $self->{+DB}->stop_sig // 'TERM';
my $fsig = $self->{+DB}->fast_stop_sig // 'KILL';
my $delete_data = $self->{+DB}->should_cleanup ? 1 : 0;
my $owner_kill = $delete_data && $self->{+DB}->fast_destroy
? 'FAST_TERM'
: 'TERM';
# Block (rather than ignore) every teardown signal across the exec. A
# blocked signal stays *pending* instead of being discarded, so a stop(),
# eliminate(), or fast_eliminate() that races server startup -- arriving
# after the socket is up (so the caller's start() has returned) but before
# _do_watch has installed its handlers -- is not lost: _do_watch unblocks
# them once the handlers are in place and a pending one fires immediately.
#
# SIGTERM/SIGINT used to be SIG_IGN'd here instead, on the wrong belief
# that an ignored signal is "held" across exec -- it is DISCARDED. A
# stop() landing in the exec window (perl startup + module load in the
# fresh watcher) was silently dropped, the watcher never learned it should
# stop, and the caller's wait() eventually gave up and killed the watcher,
# orphaning a still-running server whose data dir stayed locked. The
# build-then-immediately-stop pattern (DBIx::QuickDB::Pool) hit that
# window constantly.
POSIX::sigprocmask(POSIX::SIG_BLOCK(), POSIX::SigSet->new(POSIX::SIGUSR1(), POSIX::SIGINT(), POSIX::SIGTERM()));
exec(
$^X, _watcher_inc_args(),
'-e' => "require DBIx::QuickDB::Watcher; DBIx::QuickDB::Watcher->_do_watch()",
master_pid => $mpid,
lib/DBIx/QuickDB/Watcher.pm view on Meta::CPAN
my %params = @ARGV;
my $kill = $params{kill} // '';
my $hup = $params{hup} // 0;
local $SIG{TERM} = sub { $kill = 'TERM' };
local $SIG{INT} = sub { $kill = 'INT' };
local $SIG{USR1} = sub { $kill = 'FAST_TERM' };
local $SIG{HUP} = sub { $hup = 1 };
# watch() blocked the teardown signals before exec so a stop/eliminate/
# fast_eliminate racing startup stays pending rather than being discarded.
# Now that the handlers above are installed, unblock them -- any pending
# teardown fires here and sets $kill before we enter the watch loop.
POSIX::sigprocmask(POSIX::SIG_UNBLOCK(), POSIX::SigSet->new(POSIX::SIGUSR1(), POSIX::SIGINT(), POSIX::SIGTERM()));
my $blah;
close(STDIN);
open(STDIN, '<', \$blah) or warn "$!";
my $master_pid = $params{master_pid} or die "No master pid provided";
lib/DBIx/QuickDB/Watcher.pm view on Meta::CPAN
# escalates to SIGKILL on the server after QDB_STOP_GRACE and then BLOCKS
# until the server is reaped, so this must outlast the watcher's own
# escalation schedule (grace + grace/2, plus slack).
my $timeout = _stop_grace() * 2 + 2;
# A watcher that outlives $timeout is almost never hung -- the usual
# cause is a server the kernel has not been able to kill yet (e.g. stuck
# in disk-sleep under heavy I/O), with the watcher dutifully blocking on
# the post-SIGKILL reap. Killing the watcher at that point orphans a
# still-alive server, so stop() would return "success" while the data dir
# is locked by a live postmaster/mysqld and the next start on that dir
# fails on the stale lock file. So past $timeout we only warn, keep
# waiting on a much longer leash, and SIGKILL the watcher purely as a
# last resort. Tunable via QDB_STOP_LEASH (extra seconds past $timeout).
my $extra = $ENV{QDB_STOP_LEASH};
$extra = 60 unless defined($extra) && $extra =~ /^\d+$/ && $extra > 0;
my $leash = $timeout + $extra;
my ($warned, $nuked);
my $start = time;
while(kill(0, $pid)) {
lib/DBIx/QuickDB/Watcher.pm view on Meta::CPAN
# reaped the server; the pid may since have been recycled, and probing it
# would report a false "alive" and stall for the whole timeout. A missing
# data dir proves nothing here -- teardown removes a disposable dir whether
# or not it managed to stop the server.
$self->_latch_server_reaped;
if (!$self->{+SERVER_REAPED} and my $spid = $self->{+SERVER_PID}) {
my $sstart = time;
while (kill(0, $spid)) {
if (time - $sstart > $timeout) {
warn "Server (pid $spid) still appears to be alive after its watcher exited; its data dir may still be locked";
last;
}
sleep 0.02;
}
}
}
sub DESTROY {
my $self = shift;
( run in 2.031 seconds using v1.01-cache-2.11-cpan-800906f7e73 )