Async-Event-Interval
view release on metacpan or search on metacpan
t/09-locking.t view on Meta::CPAN
POSIX::_exit(0);
}
close $ready_w;
my $ready = <$ready_r>;
chomp $ready;
is $ready, 'ready', "_end timeout: child holds LOCK_EX";
close $ready_r;
is $knot->sem->getval(SEM_WRITERS), 1,
"_end timeout: SEM_WRITERS confirms the child's LOCK_EX is held";
no warnings 'redefine';
local *IPC::Shareable::clean_up_protected = sub {};
my $timeout = Async::Event::Interval::END_LOCK_TIMEOUT();
use Time::HiRes ();
my $t0 = Time::HiRes::time();
Async::Event::Interval::_end();
my $elapsed = Time::HiRes::time() - $t0;
# Upper bound loosened for slow-VM SIGALRM-delivery jitter; the
# behavioral claim is still "_end() bails out instead of blocking
# forever," which is enforced by a finite ceiling well below the
# child's sleep 30. The lower bound (unchanged) keeps the "actually
# waited" assertion.
cmp_ok $elapsed, '<', $timeout + 8,
"_end() bailed out instead of blocking "
. "($elapsed s, timeout=$timeout)";
cmp_ok $elapsed, '>=', $timeout - 0.5,
"_end() actually waited ~END_LOCK_TIMEOUT seconds before bailing "
. "($elapsed s)";
kill 'TERM', $pid;
waitpid $pid, 0;
}
# After a timeout-aborted _end(), the next _end() call (with no
# contention) still works â the eval / local $SIG{ALRM} pair leaves no
# leaked alarm or handler state behind.
{
no warnings 'redefine';
my $cleanup_calls = 0;
local *IPC::Shareable::clean_up_protected = sub { $cleanup_calls++ };
Async::Event::Interval::_end();
cmp_ok $cleanup_calls, '>=', 1,
"_end() recovers normally after a prior timeout";
# No leaked alarm â alarm(0) returns the seconds remaining on any
# pending alarm, so a clean state returns 0.
is alarm(0), 0,
"_end() leaves no pending alarm in the global state";
}
# 2.2: stop() polls kill(0) at STOP_KILL_POLL_INTERVAL cadence for up to
# STOP_KILL_TIMEOUT seconds instead of always sleeping a fixed 1s. It
# returns as soon as the target process is gone, and croaks only if the
# process is still alive after the timeout.
# Constants exist and have sane values; poll interval < total timeout.
{
can_ok 'Async::Event::Interval', 'STOP_KILL_TIMEOUT';
can_ok 'Async::Event::Interval', 'STOP_KILL_POLL_INTERVAL';
cmp_ok Async::Event::Interval::STOP_KILL_TIMEOUT(), '>', 0,
"STOP_KILL_TIMEOUT is positive";
cmp_ok Async::Event::Interval::STOP_KILL_POLL_INTERVAL(), '>', 0,
"STOP_KILL_POLL_INTERVAL is positive";
cmp_ok Async::Event::Interval::STOP_KILL_POLL_INTERVAL(), '<',
Async::Event::Interval::STOP_KILL_TIMEOUT(),
"poll interval is smaller than the total timeout";
}
# Already-dead pid: stop() returns essentially immediately because the
# first `kill 0` returns 0 and the while-loop body never executes.
{
my $pid = fork;
die "fork: $!" unless defined $pid;
if (! $pid) {
require POSIX;
POSIX::_exit(0);
}
# $SIG{CHLD} = 'IGNORE' (set by AEI at load time) auto-reaps; sleep
# briefly so the child has actually exited.
select(undef, undef, undef, 0.05);
my $e = Async::Event::Interval->new(0, sub {});
$e->_pid($pid);
use Time::HiRes ();
my $t0 = Time::HiRes::time();
$e->stop;
my $elapsed = Time::HiRes::time() - $t0;
cmp_ok $elapsed, '<',
Async::Event::Interval::STOP_KILL_POLL_INTERVAL(),
"stop() on an already-dead pid returns under one poll interval "
. "($elapsed s)";
is $e->_started, 0,
"stop() cleared _started flag";
$e->_pid(0);
}
# Live child: stop() actually sends SIGKILL and returns well under the
# old fixed 1s sleep â the polling loop catches the child's death on
# the first or second poll.
{
my $pid = fork;
die "fork: $!" unless defined $pid;
if (! $pid) {
sleep 30; # parent's stop() will SIGKILL us long before this
require POSIX;
POSIX::_exit(0);
}
t/09-locking.t view on Meta::CPAN
# Polling actually polled: at STOP_KILL_POLL_INTERVAL=0.05 over
# ~1.5s we expect ~30 kill(0) checks. Allow a generous floor.
cmp_ok scalar @kill_zero_calls, '>=', 10,
"stop() polled kill(0) across both signal phases (got "
. scalar(@kill_zero_calls) . ")";
is $e->_started, 0,
"stop() cleared _started flag even before croaking";
$e->_pid(0);
}
# stop() with no pid is a no-op (covers the early `if ($self->pid)`
# guard so the polling loop doesn't run on a fresh, never-started event).
{
my $e = Async::Event::Interval->new(0, sub {});
# new() does not call _pid, so $e->pid is undef at this point.
is $e->pid, undef, "fresh event has no pid before start()";
use Time::HiRes ();
my $t0 = Time::HiRes::time();
$e->stop;
my $elapsed = Time::HiRes::time() - $t0;
cmp_ok $elapsed, '<', 0.01,
"stop() on a never-started event is an instant no-op ($elapsed s)";
}
# 3.3: stop() sends SIGTERM first, escalates to SIGKILL only if TERM fails.
# Tests verify the new STOP_TERM_TIMEOUT constant, the _signal_and_wait
# helper, the TERM-then-KILL ordering, and the short-circuit when TERM
# succeeds.
# STOP_TERM_TIMEOUT exists and has a sane value.
{
can_ok 'Async::Event::Interval', 'STOP_TERM_TIMEOUT';
cmp_ok Async::Event::Interval::STOP_TERM_TIMEOUT(), '>', 0,
"STOP_TERM_TIMEOUT is positive";
cmp_ok Async::Event::Interval::STOP_TERM_TIMEOUT(), '<=', 5,
"STOP_TERM_TIMEOUT <= 5 seconds (sanity bound)";
cmp_ok Async::Event::Interval::STOP_TERM_TIMEOUT(), '<',
Async::Event::Interval::STOP_KILL_TIMEOUT(),
"STOP_TERM_TIMEOUT is shorter than STOP_KILL_TIMEOUT";
}
# _signal_and_wait is a method on Async::Event::Interval.
{
can_ok 'Async::Event::Interval', '_signal_and_wait';
}
# SIGTERM success: when a child dies from SIGTERM, stop() returns
# without ever escalating to SIGKILL. Uses the CORE::GLOBAL::kill mock
# to track signal order and simulate TERM-induced death.
{
my @signals_sent;
my $kill_zero_alive = 0; # count of kill(0) calls seen so far
local $kill_mock = sub {
my ($sig, @pids) = @_;
push @signals_sent, $sig;
if ($sig eq 'TERM') {
# After SIGTERM is delivered, subsequent kill(0) calls
# should report the process as dead.
$kill_zero_alive = 0;
return 1;
}
if ($sig eq '0') {
$kill_zero_alive++;
# Return alive for the first kill(0) (pre-TERM probe in
# _signal_and_wait's while loop), then dead afterwards
# (simulating TERM-induced death).
return $kill_zero_alive <= 1 ? 1 : 0;
}
return 1;
};
my $e = Async::Event::Interval->new(0, sub {});
$e->_pid(99999);
eval { $e->stop };
# stop() must have sent TERM (it always tries TERM first).
ok ((grep { $_ eq 'TERM' } @signals_sent) >= 1,
"SIGTERM success: SIGTERM was sent");
# SIGKILL must NOT have been sent â TERM killed the child.
ok (! (grep { $_ eq 'KILL' } @signals_sent),
"SIGTERM success: SIGKILL was NOT sent (TERM sufficed)");
is $e->_started, 0,
"SIGTERM success: _started cleared";
$e->_pid(0);
}
# Live child actually dies from SIGTERM: fork a child that sleeps,
# call stop(), verify it returns quickly (under STOP_TERM_TIMEOUT) and
# the child is gone. SIGTERM's default action (terminate) kills the
# sleeping child on the first poll.
{
my $pid = fork;
die "fork: $!" unless defined $pid;
if (! $pid) {
sleep 30;
require POSIX;
POSIX::_exit(0);
}
my $e = Async::Event::Interval->new(0, sub {});
$e->_pid($pid);
$e->_started(1);
select(undef, undef, undef, 0.05);
use Time::HiRes ();
my $t0 = Time::HiRes::time();
$e->stop;
my $elapsed = Time::HiRes::time() - $t0;
my $term_timeout = Async::Event::Interval::STOP_TERM_TIMEOUT();
cmp_ok $elapsed, '<', $term_timeout,
"live child: stop() returns under STOP_TERM_TIMEOUT when SIGTERM works "
. "($elapsed s, term_timeout=$term_timeout)";
is $e->_started, 0,
"live child: _started cleared";
ok ! (kill 0, $pid),
"live child: pid is gone after stop()";
$e->_pid(0);
}
t/09-locking.t view on Meta::CPAN
$e->_pid($pid);
use Time::HiRes ();
my $t0 = Time::HiRes::time();
my $rv = $e->_signal_and_wait('TERM',
Async::Event::Interval::STOP_TERM_TIMEOUT());
my $elapsed = Time::HiRes::time() - $t0;
is $rv, 1,
"_signal_and_wait returns 1 when pid is already dead";
cmp_ok $elapsed, '<',
Async::Event::Interval::STOP_KILL_POLL_INTERVAL(),
"_signal_and_wait returns immediately for already-dead pid "
. "($elapsed s)";
$e->_pid(0);
}
# _signal_and_wait with a live child: sends the signal, polls until
# the child dies, returns 1 within one poll interval.
{
my $pid = fork;
die "fork: $!" unless defined $pid;
if (! $pid) {
sleep 30;
require POSIX;
POSIX::_exit(0);
}
select(undef, undef, undef, 0.05);
my $e = Async::Event::Interval->new(0, sub {});
$e->_pid($pid);
use Time::HiRes ();
my $t0 = Time::HiRes::time();
my $rv = $e->_signal_and_wait('TERM',
Async::Event::Interval::STOP_TERM_TIMEOUT());
my $elapsed = Time::HiRes::time() - $t0;
is $rv, 1,
"_signal_and_wait returns 1 when child dies from signal";
cmp_ok $elapsed, '<',
Async::Event::Interval::STOP_TERM_TIMEOUT(),
"_signal_and_wait returns within STOP_TERM_TIMEOUT "
. "($elapsed s)";
ok ! (kill 0, $pid),
"_signal_and_wait: child pid is gone";
$e->_pid(0);
}
# _signal_and_wait timeout: when kill(0) keeps returning truthy past
# the timeout, returns 0 (used by stop() to decide escalation).
{
my @kill_calls;
local $kill_mock = sub {
my ($sig, @pids) = @_;
push @kill_calls, $sig;
return 1; # always alive
};
my $e = Async::Event::Interval->new(0, sub {});
$e->_pid(99999);
use Time::HiRes ();
my $t0 = Time::HiRes::time();
my $rv = $e->_signal_and_wait('TERM',
Async::Event::Interval::STOP_TERM_TIMEOUT());
my $elapsed = Time::HiRes::time() - $t0;
is $rv, 0,
"_signal_and_wait returns 0 on timeout";
my $term_timeout = Async::Event::Interval::STOP_TERM_TIMEOUT();
cmp_ok $elapsed, '>=', $term_timeout - 0.2,
"_signal_and_wait waited ~STOP_TERM_TIMEOUT before returning 0 "
. "($elapsed s)";
$e->_pid(0);
}
( run in 0.962 second using v1.01-cache-2.11-cpan-df04353d9ac )