DBIx-QuickDB

 view release on metacpan or  search on metacpan

t/fast_destroy.t  view on Meta::CPAN

use strict;
use warnings;

use Test2::V0;
use Test2::Tools::QuickDB;
use Time::HiRes qw/time/;
use File::Path qw/remove_tree/;

# Fast destroy for disposable clones: instead of a graceful shutdown that can
# block for up to 2*QDB_STOP_GRACE+2 seconds, destroy_quietly() asks the watcher
# to SIGKILL the server immediately, reap it, and remove the data dir. The
# watcher (the server's parent) still owns/kills/reaps the server -- the Driver
# never signals the stored server pid directly.

# The whole feature relies on the Unix-only watcher (fork + setsid + setpgrp +
# SIGUSR1/sigprocmask). On Win32 the watcher cannot run and only the watcherless
# 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;
        our @ISA = (ref($db));
        sub checkpoint { $checkpoint_called++; return }
        sub stop       { $stop_called++; my $s = shift; $s->SUPER::stop(@_) }
    }
    bless $clone, 'Spy::FastDestroy';

    $clone->destroy_quietly;

    is($checkpoint_called, 0, "destroy_quietly did not call checkpoint()");
    is($stop_called,       0, "destroy_quietly did not call stop()");
};

subtest idempotent => sub {
    my $clone = $db->clone(autostart => 1, cleanup => 1);
    my $dir   = $clone->dir;
    my $wpid  = $clone->watcher->watcher_pid;

    ok(lives { $clone->destroy_quietly }, "first destroy_quietly lives") or diag($@);

    # Second call is a no-op: the watcher is already gone, so it must not throw
    # or try to signal a (possibly recycled) pid again.
    ok(lives { $clone->destroy_quietly }, "second destroy_quietly is a safe no-op") or diag($@);

    # Dropping the object after destroy_quietly must not signal again either.
    ok(lives { undef $clone }, "dropping after destroy_quietly does not re-signal") or diag($@);

    ok(!-d $dir, "data dir stayed removed");
};

subtest fast_destroy_attr_cleanup => sub {
    # fast_destroy => 1 with cleanup => 1: dropping the clone must use the fast
    # path (no QDB_STOP_GRACE wait) and remove the data dir.
    my $clone = $db->clone(autostart => 1, cleanup => 1, fast_destroy => 1);

    ok($clone->fast_destroy, "clone carries the fast_destroy attribute");

    my $dir  = $clone->dir;
    my $wpid = $clone->watcher->watcher_pid;
    my $spid = $clone->watcher->server_pid;

    local $ENV{QDB_STOP_GRACE} = 30;

    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;
    ok($data{DBIx::QuickDB::Driver::FAST_DESTROY()}, "clone_data propagates fast_destroy to further clones");

    $clone->destroy_quietly;
};

subtest fast_destroy_attr_no_cleanup => sub {
    # fast_destroy => 1 but cleanup => 0: the _CLEANUP guard means DESTROY must
    # NOT take the fast path. The data dir must survive (cleanup => 0).
    my $clone = $db->clone(autostart => 1, cleanup => 0, fast_destroy => 1);
    my $dir = $clone->dir;

    $clone->stop;
    undef $clone;

    ok(-d $dir, "cleanup => 0 data dir preserved (fast path not taken)");

    # Clean up the preserved dir ourselves.
    File::Path::remove_tree($dir) if -d $dir;
};

subtest dbi_handles_disconnected => sub {
    # destroy_quietly() disconnects this process's DBI handles before the server
    # is killed, so we do not retain a live-but-broken handle that would later
    # report "server has gone away".
    my $clone = $db->clone(autostart => 1, cleanup => 1);
    my $dbh   = $clone->connect('quickdb');

    ok($dbh->{Active}, "handle is active before destroy_quietly");

    $clone->destroy_quietly;

    ok(!$dbh->{Active}, "handle was disconnected by destroy_quietly");
};

done_testing;



( run in 1.105 second using v1.01-cache-2.11-cpan-df04353d9ac )