Skip Menu |

This queue is for tickets about the future CPAN distribution.

Report information
The Basics
Id: 94040
Status: resolved
Priority: 0/
Queue: future

People
Owner: Nobody in particular
Requestors: TOSHIOITO [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: 0.25
Fixed in: 0.26



Subject: Cyclic reference in then()
I ran the attached test (future_cycle.t) and found there is a cyclic reference in Futures when then() method is called and some of the futures are pending. I modified Future.pm (attached) so that futures in @{$self->{on_cancel}} are weak-refs. It passed the future_cycle.t test. However, the modified version fails t/06followed_by.t, which seems to claim there SHOULD be a cyclic ref. So my question is, is it your intension to create the cyclic ref?
Subject: Future.pm

Message body is not shown because it is too large.

Subject: future_cycle.t
use strict; use warnings; use Test::More; use Test::Memory::Cycle; use Future; { note("--- pending f1, immediate f2"); my $f1 = Future->new; my $f2 = Future->new->done; my $next = $f1->then(sub { Future->new->done() }); memory_cycle_ok $f1, "f1=pending, f2=resolved, no cycle at f1"; memory_cycle_ok $f2, "f1=pending, f2=resolved, no cycle at f2"; memory_cycle_ok $next, "f1=pending, f2=resolved, no cycle at next"; $f1->done; memory_cycle_ok $f1, "f1=resolved, f2=resolved, no cycle at f1"; memory_cycle_ok $f2, "f1=resolved, f2=resolved, no cycle at f2"; memory_cycle_ok $next, "f1=resolved, f2=resolved, no cycle at next"; } { note("--- immediate f1, pending f2"); my $f1 = Future->new->done; my $f2 = Future->new; my $next = $f1->then(sub { $f2 }); memory_cycle_ok $f1, "f1=resolved, f2=pending, no cycle at f1"; memory_cycle_ok $f2, "f1=resolved, f2=pending, no cycle at f2"; memory_cycle_ok $next, "f1=resolved, f2=pending, no cycle at next"; $f2->done; memory_cycle_ok $f1, "f1=resolved, f2=resolved, no cycle at f1"; memory_cycle_ok $f2, "f1=resolved, f2=resolved, no cycle at f2"; memory_cycle_ok $next, "f1=resolved, f2=resolved, no cycle at next"; } { note("--- pending f1, pending f2"); my $f1 = Future->new; my $f2 = Future->new; my $next = $f1->then(sub { $f2 }); memory_cycle_ok $f1, "f1=pending, f2=pending, no cycle at f1"; memory_cycle_ok $f2, "f1=pending, f2=pending, no cycle at f2"; memory_cycle_ok $next, "f1=pending, f2=pending, no cycle at next"; $f1->done; memory_cycle_ok $f1, "f1=resolved, f2=pending, no cycle at f1"; memory_cycle_ok $f2, "f1=resolved, f2=pending, no cycle at f2"; memory_cycle_ok $next, "f1=resolved, f2=pending, no cycle at next"; $f2->done; memory_cycle_ok $f1, "f1=resolved, f2=resolved, no cycle at f1"; memory_cycle_ok $f2, "f1=resolved, f2=resolved, no cycle at f2"; memory_cycle_ok $next, "f1=resolved, f2=resolved, no cycle at next"; } { note("--- immediate f1, immediate f2"); my $f1 = Future->new->done; my $f2 = Future->new->done; my $next = $f1->then(sub { $f2 }); memory_cycle_ok $f1, "f1=resolved, f2=resolved, no cycle at f1"; memory_cycle_ok $f2, "f1=resolved, f2=resolved, no cycle at f2"; memory_cycle_ok $next, "f1=resolved, f2=resolved, no cycle at next"; } done_testing;
On Thu Mar 20 09:17:44 2014, TOSHIOITO wrote: Show quoted text
> I ran the attached test (future_cycle.t) and found there is a cyclic > reference in Futures when then() method is called and some of the > futures are pending. > > I modified Future.pm (attached) so that futures in @{$self-
> >{on_cancel}} are weak-refs. It passed the future_cycle.t test.
> > However, the modified version fails t/06followed_by.t, which seems to > claim there SHOULD be a cyclic ref. > > So my question is, is it your intension to create the cyclic ref?
Short answer - yes. Longer answer: a ready Future should certainly not take part in any reference cycles; once complete a Future doesn't need to remember where it came from. However, a pending one might need a reference in either direction - done has to push results up the tree, cancel has to stop activity down it. So there might be references in both directions. It's hard to argue the case for whether any of these references should be weakened, because it could be possible to hold a reference at any point and be able to reach the rest. It may be possible to work out a scheme of weakening that works for all cases, but it may require some tests to be updated, and some current semantics to change. I'll have some more thought on it. -- Paul Evans
On Tue Mar 25 13:58:37 2014, PEVANS wrote: Show quoted text
> It's hard to argue the case for whether any of these references should > be weakened, because it could be possible to hold a reference at any > point and be able to reach the rest.
Actually thinking about it, the tree-shaped dependent futures are already weakened in one direction, so there's no reason why linear sequenced futures shouldn't be as well. Patch attached. -- Paul Evans
Subject: rt94040.patch
=== modified file 'lib/Future.pm' --- lib/Future.pm 2014-03-11 21:17:43 +0000 +++ lib/Future.pm 2014-03-25 18:22:06 +0000 @@ -322,6 +322,7 @@ } elsif( $flags & (CB_SEQ_ONDONE|CB_SEQ_ONFAIL) ) { my ( undef, undef, $fseq ) = @$cb; + next unless defined $fseq; # weaken()ed; it might be gone now my $f2; if( $done and $flags & CB_SEQ_ONDONE or @@ -361,6 +362,7 @@ } else { push @{ $f2->{callbacks} }, [ CB_DONE|CB_FAIL, $fseq ]; + weaken( $f2->{callbacks}[-1][2] ); } } else { @@ -918,6 +920,7 @@ $fseq->on_cancel( $f1 ); push @{ $f1->{callbacks} }, [ CB_DONE|CB_FAIL|$flags, $code, $fseq ]; + weaken( $f1->{callbacks}[-1][2] ); return $fseq; } === modified file 't/03then.t' --- t/03then.t 2014-01-11 14:41:41 +0000 +++ t/03then.t 2014-03-25 18:22:06 +0000 @@ -25,8 +25,7 @@ ok( defined $fseq, '$fseq defined' ); isa_ok( $fseq, "Future", '$fseq' ); - # Two refs; one in lexical $fseq, one via $f1 - is_refcount( $fseq, 2, '$fseq has refcount 2 initially' ); + is_oneref( $fseq, '$fseq has refcount 1 initially' ); ok( !$f2, '$f2 not yet defined before $f1 done' ); @@ -164,6 +163,17 @@ ok( $f2->is_cancelled, '$f2 cancelled by $fseq cancel' ); } +# then dropping $fseq doesn't fail ->done +{ + my $f1 = Future->new; + my $fseq = $f1->then( sub { return Future->new->done() } ); + + undef $fseq; + + is( exception { $f1->done; }, undef, + 'Dropping $fseq does not cause $f1->done to die' ); +} + # Void context raises a warning { my $warnings; === modified file 't/04else.t' --- t/04else.t 2014-01-11 14:41:41 +0000 +++ t/04else.t 2014-03-25 18:22:06 +0000 @@ -21,8 +21,7 @@ ok( defined $fseq, '$fseq defined' ); isa_ok( $fseq, "Future", '$fseq' ); - # Two refs; one in lexical $fseq, one via $f1 - is_refcount( $fseq, 2, '$fseq has refcount 2 initially' ); + is_oneref( $fseq, '$fseq has refcount 1 initially' ); $f1->done( results => "here" ); @@ -47,7 +46,7 @@ ok( defined $fseq, '$fseq defined' ); isa_ok( $fseq, "Future", '$fseq' ); - is_refcount( $fseq, 2, '$fseq has refcount 2 initially' ); + is_oneref( $fseq, '$fseq has refcount 1 initially' ); ok( !$f2, '$f2 not yet defined before $f1 fails' ); === modified file 't/06followed_by.t' --- t/06followed_by.t 2013-12-28 18:07:31 +0000 +++ t/06followed_by.t 2014-03-25 18:22:06 +0000 @@ -23,8 +23,7 @@ ok( defined $fseq, '$fseq defined' ); isa_ok( $fseq, "Future", '$fseq' ); - # Two refs; one in lexical $fseq, one via $f1 - is_refcount( $fseq, 2, '$fseq has refcount 2 initially' ); + is_oneref( $fseq, '$fseq has refcount 1 initially' ); # Two refs; one in lexical $f1, one in $fseq's cancellation closure is_refcount( $f1, 2, '$f1 has refcount 2 initially' ); @@ -54,8 +53,7 @@ ok( defined $fseq, '$fseq defined' ); isa_ok( $fseq, "Future", '$fseq' ); - # Two refs; one in lexical $fseq, one via $f1 - is_refcount( $fseq, 2, '$fseq has refcount 2 initially' ); + is_oneref( $fseq, '$fseq has refcount 1 initially' ); is( $called, 0, '$called before $f1 done' );
On 2014-3月-25 火 14:24:06, PEVANS wrote: Show quoted text
> On Tue Mar 25 13:58:37 2014, PEVANS wrote:
> > It's hard to argue the case for whether any of these references > > should > > be weakened, because it could be possible to hold a reference at any > > point and be able to reach the rest.
> > Actually thinking about it, the tree-shaped dependent futures are > already weakened in one direction, so there's no reason why linear > sequenced futures shouldn't be as well. > > Patch attached.
Thanks. It Looks good. I like cyclic-ref-free objects.
It turns out that this bugfix does actually break a number of other CPAN modules that were relying on the strong references in Future. Currently known broken are IO::Async 0.61 IO::Async::SSL 0.13 Net::Async::HTTP 0.33 I'm in the process of fixing these and finding any others, but for now I'll hold off on actually releasing this version of Future, until I've uploaded fixed versions of everything else, so it won't break them. I'm also adding a safety feature to Future itself, which will abort the install process if a still-broken version of any of the above is found. -- Paul Evans
On 2014-3月-27 木 07:43:07, PEVANS wrote: Show quoted text
> It turns out that this bugfix does actually break a number of other > CPAN modules that were relying on the strong references in Future. > Currently known broken are > > IO::Async 0.61 > IO::Async::SSL 0.13 > Net::Async::HTTP 0.33 > > I'm in the process of fixing these and finding any others, but for now > I'll hold off on actually releasing this version of Future, until I've > uploaded fixed versions of everything else, so it won't break them. > I'm also adding a safety feature to Future itself, which will abort > the install process if a still-broken version of any of the above is > found.
Wow, it breaks IO::Async? That's serious. So they rely on the fact that Futures are persistent due to the cyclic refs? I thought it was an unusual case because someone is supposed to keep the original Future to call done() or fail(). Anyway if the backward-incompatibility is so serious, I'm ok with the current behavior. Take your time :)
With hopefully all of the CPAN modules this might have affected now nicely updated, I've released this fix on CPAN as 0.26. The Build.PL script currently aborts if any version of a module known to break is still present. Eventually though, that will cause a deadlock problem, as newer versions of those become available that will now depend on a newer Future. So, you can $ perl Build.PL --force to turn that into simply a warning, allowing upgrade of Future, and then upgrade of the modules that depended on it. -- Paul Evans