EV-Future
view release on metacpan or search on metacpan
ctx->num_cvs = 0;
ctx->shared_cv = NULL;
ctx->abandoned = 0;
ifp_guard *guard = ifp_guard_push(aTHX_ &ctx->is_freed_ptr, NULL,
NULL, &ctx->abandoned);
int *is_freed = &guard->is_freed;
/* Counted before anything is dispatched: a task that completes the whole
operation synchronously runs cleanup, which drops the context's
reference, and only this one keeps the cell alive until we return it. */
evf_handle *h = NULL;
if (want_handle) {
h = evf_handle_new(aTHX_ NULL, EVF_KIND_PARALLEL);
evf_handle_attach(aTHX_ h, ctx);
ctx->h = h;
guard->pending_h = h;
}
SV *done_rv = NULL;
if (unsafe) {
t/repro_reentrant_cancel.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use EV;
use EV::Future;
# A cancel issued from inside a DESTROY that fires *during* cleanup itself is
# a different hazard from an ordinary user cancel. Cleanup drops
# ctx->tasks/final_cb/worker, and any of those can be the last reference
# keeping a blessed object alive. If that object's DESTROY calls $h->cancel
# while the handle had not been detached yet, the handle still looked live
# and cancel would re-enter cleanup on a context that is already half torn
# down: it double-decs ctx->tasks and ctx->final_cb, walks whatever CV field
# the first pass freed without NULLing, then double-frees the handle cell and
# the context itself. Detaching the handle first, before any SvREFCNT_dec that
# can run Perl, makes the reentrant call see h->ctx == NULL and bail out
# immediately.
#
# All four primitives have their own cleanup with its own copy of that
# ordering, so all four are exercised here: covering only parallel let the
t/repro_reentrant_cancel.t view on Meta::CPAN
{
name => 'race',
start => sub { race($_[0], $_[1]) },
# race treats a non-coderef element as an instant winner, which would
# settle it inside the XSUB before the handle reaches Perl - too early
# for the DESTROY to have a handle to cancel. So race's guard travels
# inside a task CV that never completes; the tasks array is still the
# only owner, and race_cleanup still frees it.
guard_element => sub {
my $guard = shift;
return sub { my $keep_alive = $guard; return };
},
},
);
for my $kind (@kinds) {
my $name = $kind->{name};
subtest "reentrant cancel from a guard freed via the final_cb closure ($name)" => sub {
our @w;
my ($final, $reentries, $err) = (0, 0, '');
t/repro_reentrant_cancel.t view on Meta::CPAN
$err = $@ if $@;
});
# $guard's only remaining owner once this block ends is the
# closure's own captured copy. The closure itself is passed
# straight through as an argument and never bound to a variable
# that outlives this statement, so ctx->final_cb ends up as its
# sole reference; freeing it during cleanup is what frees $guard
# in turn.
$h = $kind->{start}->(
[ sub { my $d = shift; push @w, EV::timer 0.01, 0, sub { $d->() } } ],
sub { my $keep_alive = $guard; $final++ },
);
}
$h->cancel;
is($reentries, 1, 'guard DESTROY ran its re-entrant cancel exactly once');
is($final, 0, 'final_cb was freed, not called (plain cancel does not fire it)');
ok(!$err, 'the re-entrant cancel call did not die') or diag $err;
is_deeply(\@warnings, [], 'no "Attempt to free unreferenced scalar" or similar')
or diag explain \@warnings;
t/repro_unsafe_abandon.t view on Meta::CPAN
# cannot run cleanup. A handle held over it is the only thing that can still
# reach that context, and cancel() is the documented way to reclaim it.
#
# The assertions below pin the observable half of that contract: stale non-zero
# counts while abandoned, zero after cancel, and final_cb firing only for
# cancel(1). The reclaim of the memory itself is deliberately not asserted here,
# because no leak checker can see it - the leaked context stays reachable
# through the shared done CV's payload pointer, which lives in a Perl SV arena,
# so valgrind reports it as still reachable rather than lost either way. It was
# measured instead by counting live allocations at exit: 20 abandonments leave
# 45 blocks from plimit_start alive without the cancel and 2 with it.
subtest 'cancel reclaims an abandoned operation' => sub {
our @w;
# limit 2: tasks 1 and 2 go out in the opening burst, so the handle is
# assigned before task 3 is dispatched from the event loop and dies there.
my ($final, $h) = (0);
$h = parallel_limit([
sub { my $d = shift; push @w, EV::timer 0.01, 0, sub { $d->() } },
sub { my $d = shift; push @w, EV::timer 0.50, 0, sub { $d->() } },
sub { die "boom\n" },
( run in 1.985 second using v1.01-cache-2.11-cpan-14f38c9f855 )