Skip Menu |

This queue is for tickets about the future CPAN distribution.

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

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

Bug Information
Severity: Normal
Broken in: 0.11
Fixed in: 0.12



Subject: $f1->on_ready($f2) and $f1->cancel() throws an exception
With the attached test script, I examined Future's behavior when it is cancelled. Then I found that $f1->on_ready($f2) and $f1->cancel() throws an exception. Example: my $f1 = Future->new; my $f2 = Future->new; $f1->on_ready($f2); $f1->cancel(); Result: $f1->cancel() throws an exception Future=HASH(0x9a76fd8) ->fail requires an exception that is true at eg.pl line 10. It throws an excetion if on_ready() is called after cancel(), too. The document does not clearly explain what will happen to $f2 when $f1 is cancelled. I suppose $f2 should be cancelled, too.
Subject: test_callbacks_on_cancel.t
use strict; use warnings; use Future 0.11; use Test::More; use Test::Fatal qw(lives_ok); note("Test whether various callbacks are executed or not on cancelling."); { my $f = Future->new; $f->cancel(); ok($f->is_ready, "If the Future is cancelled, is_ready should be true"); } note("--- on_ready(CODE), cancel later"); lives_ok { my $f = Future->new; my $executed = 0; $f->on_ready(sub { $executed = 1 }); $f->cancel(); ok($executed, "on_ready CODE should be executed on cancel"); } 'on_ready(CODE) should live'; note("--- on_ready(CODE), already canceled"); lives_ok { my $f = Future->new; my $executed = 0; $f->cancel(); $f->on_ready(sub { $executed = 1 }); ok($executed, "on_ready CODE should be executed on cancel"); } 'on_ready(CODE) should live'; note("--- on_ready(FUTURE), cancel later"); lives_ok { my $f = Future->new; my $cf = Future->new; $f->on_ready($cf); $f->cancel(); ok($cf->is_ready, "child future should be ready"); ## really? ok($cf->is_cancelled, "child future should be cancelled"); ## really? } 'on_ready(FUTURE) should live'; note("--- on_ready(FUTURE), already canceled"); lives_ok { my $f = Future->new; my $cf = Future->new; $f->cancel(); $f->on_ready($cf); ok($cf->is_ready, "child future should be ready"); ## really? ok($cf->is_cancelled, "child future should be cancelled"); ## really? } 'on_ready(FUTURE) should live'; foreach my $method (qw(on_done on_fail)) { note("--- $method(CODE), cancel later"); lives_ok { my $f = Future->new; my $executed = 0; $f->$method(sub { $executed = 1 }); $f->cancel(); ok(!$executed, "$method CODE should not be executed on cancel"); } "$method(CODE) should live"; note("--- $method(CODE), already canceled"); lives_ok { my $f = Future->new; my $executed = 0; $f->cancel(); $f->$method(sub { $executed = 1 }); ok(!$executed, "$method CODE should not be executed on cancel"); } "$method(CODE) should live"; note("--- $method(FUTURE), cancel later"); lives_ok { my $f = Future->new; my $cf = Future->new; $f->$method($cf); $f->cancel(); ok(!$cf->is_ready, "child future should not be ready"); } "$method(FUTURE) should live"; note("--- $method(FUTURE), already cancelled"); lives_ok { my $f = Future->new; my $cf = Future->new; $f->cancel(); $f->$method($cf); ok(!$cf->is_ready, "child future should not be ready"); } "$method(FUTURE) should live"; } foreach my $method (qw(and_then or_else followed_by)) { note("--- $method(CODE)"); lives_ok { my $f1 = Future->new; my $executed = 0; my $f = $f1->$method(sub { $executed = 1; return Future->done; }); $f->cancel(); ok(!$executed, "$method CODE should not be executed on cancel"); ok($f1->is_cancelled, "$method f1 should be cancelled"); } "$method(CODE) should live"; } done_testing();
I think on this one I am coming to the conclusion that it's a mistake for "on_ready" to be fired when a Future is cancelled. There's already a separate on_cancel chain for that. On the other hand, it might be nice for dependent futures to still know one of their components got cancelled. More thought needed on this one I think. -- Paul Evans
At first I was surprised to see on_ready() fires when the future is cancelled. However it made sense to me afterwards when I found out is_ready() returns true for cancelled futures. In the current (v0.11) spec, Future seems to have four distinctive states. 1. not ready 2. done \ 3. failed | ready 4. cancelled / This seems a bit counter-intuitive, but at least its behavior is consistent. Personally I don't think it's a mistake.
On Sun Mar 31 05:02:32 2013, TOSHIOITO wrote: Show quoted text
> In the current (v0.11) spec, Future seems to have four distinctive > states. > > 1. not ready > 2. done \ > 3. failed | ready > 4. cancelled /
Hmmyes; I think you've just convinced me there with that diagram. "cancelled" should be a sub-state of "ready". -- Paul Evans
Find attached a patch to make your unit test file pass. -- Paul Evans
Subject: rt84312.patch
=== modified file 'lib/Future.pm' --- lib/Future.pm 2013-03-31 13:06:15 +0000 +++ lib/Future.pm 2013-03-31 13:40:04 +0000 @@ -341,23 +341,24 @@ my $self = shift; $self->{ready} = 1; - my $failed = defined $self->failure; - my $done = !$failed && !$self->is_cancelled; + my $fail = defined $self->failure; + my $done = !$fail && !$self->is_cancelled; foreach my $cb ( @{ $self->{callbacks} } ) { my ( $type, $code ) = @$cb; my $is_future = blessed $code and $code->isa( "Future" ); if( $type eq "ready" ) { - $is_future ? ( $done ? $code->done( $self->get ) - : $code->fail( $self->failure ) ) + $is_future ? ( $done ? $code->done( $self->get ) : + $fail ? $code->fail( $self->failure ) : + $code->cancel ) : $code->( $self ); } elsif( $type eq "done" and $done ) { $is_future ? $code->done( $self->get ) : $code->( $self->get ); } - elsif( $type eq "failed" and $failed ) { + elsif( $type eq "failed" and $fail ) { $is_future ? $code->fail( $self->failure ) : $code->( $self->failure ); } @@ -556,8 +557,8 @@ =head2 $future->on_ready( $f ) If passed another C<Future> instance, the passed instance will have its -C<done> or C<fail> methods invoked when the original future completes -successfully or fails respectively. +C<done>, C<fail> or C<cancel> methods invoked when the original future +completes successfully, fails, or is cancelled respectively. =cut @@ -568,10 +569,13 @@ if( $self->is_ready ) { my $is_future = blessed $code and $code->isa( "Future" ); - my $done = !$self->failure && !$self->is_cancelled; - - $is_future ? ( $done ? $code->done( $self->get ) - : $code->fail( $self->failure ) ) + + my $fail = defined $self->failure; + my $done = !$fail && !$self->is_cancelled; + + $is_future ? ( $done ? $code->done( $self->get ) : + $fail ? $code->fail( $self->failure ) : + $code->cancel ) : $code->( $self ); } else { === modified file 't/02cancel.t' --- t/02cancel.t 2013-03-31 13:08:13 +0000 +++ t/02cancel.t 2013-03-31 13:40:04 +0000 @@ -23,6 +23,10 @@ $future->on_done( sub { die "on_done called for cancelled future" } ); $future->on_fail( sub { die "on_fail called for cancelled future" } ); + $future->on_ready( my $ready_f = Future->new ); + $future->on_done( my $done_f = Future->new ); + $future->on_fail( my $fail_f = Future->new ); + $future->cancel; ok( $future->is_ready, '$future->cancel marks future ready' ); @@ -32,6 +36,10 @@ is( $ready, 1, '$future on_ready still called by cancel' ); + ok( $ready_f->is_cancelled, 'on_ready chained future cnacelled after cancel' ); + ok( !$done_f->is_ready, 'on_done chained future not ready after cancel' ); + ok( !$fail_f->is_ready, 'on_fail chained future not ready after cancel' ); + like( exception { $future->get }, qr/cancelled/, '$future->get throws exception by cancel' ); ok( !exception { $future->cancel }, '$future->cancel a second time is OK' ); @@ -56,10 +64,24 @@ my $future = Future->new; $future->cancel; - my $called = 0; - $future->on_ready( sub { $called++ } ); - - is( $called, 1, 'on_ready invoked for already-cancelled future' ); + my $ready_called; + $future->on_ready( sub { $ready_called++ } ); + my $done_called; + $future->on_done( sub { $done_called++ } ); + my $fail_called; + $future->on_fail( sub { $fail_called++ } ); + + $future->on_ready( my $ready_f = Future->new ); + $future->on_done( my $done_f = Future->new ); + $future->on_fail( my $fail_f = Future->new ); + + is( $ready_called, 1, 'on_ready invoked for already-cancelled future' ); + ok( !$done_called, 'on_done not invoked for already-cancelled future' ); + ok( !$fail_called, 'on_fail not invoked for already-cancelled future' ); + + ok( $ready_f->is_cancelled, 'on_ready chained future cnacelled for already-cancelled future' ); + ok( !$done_f->is_ready, 'on_done chained future not ready for already-cancelled future' ); + ok( !$fail_f->is_ready, 'on_fail chained future not ready for already-cancelled future' ); } # cancel chaining
Thank you for fixing!