DBIx-QuickDB

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

    - 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;



( run in 2.404 seconds using v1.01-cache-2.11-cpan-df04353d9ac )