stevan / promises-perl Goto Github PK
View Code? Open in Web Editor NEWAn implementation of Promises in Perl
An implementation of Promises in Perl
https://rt.cpan.org/Ticket/Display.html?id=96791
The $delay->end(โฆ) is wrong - there is no end() method in this class.
Hiya @stevan
I was about to submit a patch for the memory cycle, but I see you've already fixed it. w00t.
Any idea when you plan on releasing this?
ta
Clint
So in my code I have a bunch of chained thens:
$rpc->api->vim_get_current_buffer
->then(sub{ $buffer_id = ord $_[0]->data })
->then(sub{ $rpc->api->vim_input( "1GdG" ) })
->then(sub{ $rpc->api->buffer_insert( $buffer_id, 0, [
map { $_->{description} } $tw->export_tasks
] ); });
which I already can de-noisify a wee bit via:
reduce { $a->then($b) }
$rpc->api->vim_get_current_buffer,
sub{ $buffer_id = ord $_[0]->data },
sub{ $rpc->api->vim_input( "1GdG" ) },
sub{ $rpc->api->buffer_insert( $buffer_id, 0, [
map { $_->{description} } $tw->export_tasks
] ); };
I'm wondering, though, if it would be worthwhile to have a special case
of then
where
$p->then( \@then, $else )
gets expanded to
$p->then( $then[0] )->then( $then[1] )->..->catch($else)
to that the above code would become
$rpc->api->vim_get_current_buffer->then([
sub{ $buffer_id = ord $_[0]->data },
sub{ $rpc->api->vim_input( "1GdG" ) },
sub{ $rpc->api->buffer_insert( $buffer_id, 0, [
map { $_->{description} } $tw->export_tasks
] ); }
]);
Alternatively, if the different behaviors of then
feels wrong, perhaps a chain
method that does
$p->chain( @then );
# equivalent to
reduce { $a->then($b) } $p, @then;
?
If any of those feel appealing to you, I'd be delighted to submit a patch.
Hiya
It appears that currently Promises are not compliant with http://promises-aplus.github.io/promises-spec/#point-49
Specifically:
promise2 = promise1.then(onFulfilled, onRejected);
2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).
In other words, the reject
handler should be able to handle the error (or rethrow it), so this code:
use Promises qw(deferred);
my $d = deferred;
$d->promise
->then( sub { say "resolve" }, sub { say "reject" })
->then( sub { say "resolve" }, sub { say "reject" });
$d->reject('foo');
should print out:
reject
resolve
when actually it prints out:
reject
reject
The author of AnyEvent::Promises points out this same issue: https://metacpan.org/pod/AnyEvent::Promises#SEE-ALSO
When working with Promises as they are, I found the inability to handle errors and continue down the resolve
chain to be an issue, but I assumed that was just how promises worked.
I think this should be changed, but of course it breaks bwc in quite a big way. What do you think? How to handle bwc?
Promises v0.5 will clear resolve callbacks when a promise is resolved, or rejected callbacks when a promise is rejected, but leaves callback references alive in the other group (ie rejected or resolved).
All callbacks should be cleared up once resolution/rejection is complete.
In the latest version:
my $socket_io;
$socket_io = EV::io($socket_recv, EV::READ, &_do_callbacks);
This $socket_io is an EV handler, and it will prevent EV::loop from exiting.
Unfortunately my program need all EV handler be undefined or mark the handler keepalive(0).
So please either make it keepalive(0) or provide an API like cleanup().
Thanks.
In my quest to make Promises recursive, I've come across another issue: because promises are chained, the return stack can become enormous. JS seems to use tail calls to flatten this out (I think) but of course this doesn't work in Perl.
For a use case imagine processing each record in a database one after another (not all at the same time), then returning when all done (or when a failure occurs)
What I needed was this:
$loop = sub {
if ($done) { return $deferred->resolve;
something()->then(...)->then(...)
->then(
sub { AE::postpone { $loop->() }; return }
sub { $deferred->reject(@_)
)
};
$loop->();
But I want to write that without knowing ahead of time which event loop is being used. I tried to make it work with goto $loop
but that last then() is still waiting for a return value.
The solution I've come up with is then_discard()
which works just like then()
but doesn't return a promise.
$loop = sub {
if ($done) { return $deferred->resolve;
something()->then(...)->then(...)
->then_discard(
sub { AE::postpone { $loop->() }; return }
sub { $deferred->reject(@_)
)
};
$loop->();
So the callback is scheduled for execution in the event loop and then it returns, unwinding the return stack.
See the PR for details.
Does the name then_discard()
make sense to you? It is so called because the return values are discarded instead of being passed to the next promise.
While I do understand this isn't the intended use-case, this code triggered the above condition for me:
collect(map { $_->validate() } @resources)
->then(sub { call_after_all_finished(); });
Where each validate()
method returns a promise, but @resources
was empty. I'm not interested in the values to which the promises resolve; basically, they're only used to wait for all actions to be completed before going to the final step.
Today I had occasion to test this distribution against perl-5.31.10 as part of CPAN-River-3000 testing. I observed a failure in t/090-timeout.t
, which I was able to reproduce repeatedly:
$ thisprove -vb t/090-timeout.t
t/090-timeout.t ..
# Subtest: AnyEvent
1..0 # SKIP Can't locate AnyEvent.pm in @INC (you may need to install the AnyEvent module) (@INC contains: t/lib /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/lib /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/arch /home/jkeenan/var/tad/testing/perl-5.31.10/lib/site_perl/5.31.10/amd64-freebsd-thread-multi /home/jkeenan/var/tad/testing/perl-5.31.10/lib/site_perl/5.31.10 /home/jkeenan/var/tad/testing/perl-5.31.10/lib/5.31.10/amd64-freebsd-thread-multi /home/jkeenan/var/tad/testing/perl-5.31.10/lib/5.31.10) at /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/lib/Promises/Deferred/AnyEvent.pm line 8.
ok 1 # skip Can't locate AnyEvent.pm in @INC (you may need to install the AnyEvent module) (@INC contains: t/lib /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/lib /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/arch /home/jkeenan/var/tad/testing/perl-5.31.10/lib/site_perl/5.31.10/amd64-freebsd-thread-multi /home/jkeenan/var/tad/testing/perl-5.31.10/lib/site_perl/5.31.10 /home/jkeenan/var/tad/testing/perl-5.31.10/lib/5.31.10/amd64-freebsd-thread-multi /home/jkeenan/var/tad/testing/perl-5.31.10/lib/5.31.10) at /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/lib/Promises/Deferred/AnyEvent.pm line 8.
# Subtest: IO::Async
1..6
ok 1
ok 2
ok 3 - timed out
ok 4 - p1 still in progress
ok 5 - p3 resolved
ok 6 - p4 resolved
ok 2 - IO::Async
# Subtest: AE
1..0 # SKIP Can't locate AE.pm in @INC (you may need to install the AE module) (@INC contains: t/lib /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/lib /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/arch /home/jkeenan/var/tad/testing/perl-5.31.10/lib/site_perl/5.31.10/amd64-freebsd-thread-multi /home/jkeenan/var/tad/testing/perl-5.31.10/lib/site_perl/5.31.10 /home/jkeenan/var/tad/testing/perl-5.31.10/lib/5.31.10/amd64-freebsd-thread-multi /home/jkeenan/var/tad/testing/perl-5.31.10/lib/5.31.10) at /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/lib/Promises/Deferred/AE.pm line 8.
ok 3 # skip Can't locate AE.pm in @INC (you may need to install the AE module) (@INC contains: t/lib /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/lib /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/arch /home/jkeenan/var/tad/testing/perl-5.31.10/lib/site_perl/5.31.10/amd64-freebsd-thread-multi /home/jkeenan/var/tad/testing/perl-5.31.10/lib/site_perl/5.31.10 /home/jkeenan/var/tad/testing/perl-5.31.10/lib/5.31.10/amd64-freebsd-thread-multi /home/jkeenan/var/tad/testing/perl-5.31.10/lib/5.31.10) at /usr/home/jkeenan/var/tad/testing/perl-5.31.10/.cpanm/work/1584785680.74982/Promises-1.04/blib/lib/Promises/Deferred/AE.pm line 8.
# Subtest: Mojo
1..6
ok 1
ok 2
ok 3 - timed out
ok 4 - p1 still in progress
ok 5 - p3 resolved
ok 6 - p4 resolved
ok 4 - Mojo
# Subtest: EV
1..6
ok 1
ok 2
ok 3 - timed out
ok 4 - p1 still in progress
ok 5 - p3 resolved
# Looks like you planned 6 tests but ran 5.
not ok 5 - EV
1..5
# Failed test 'EV'
# at t/090-timeout.t line 13.
# Looks like you failed 1 test of 5.
Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/5 subtests
(less 2 skipped subtests: 2 okay)
Test Summary Report
-------------------
t/090-timeout.t (Wstat: 256 Tests: 5 Failed: 1)
Failed test: 5
Non-zero exit status: 1
Files=1, Tests=5, 4 wallclock secs ( 0.05 usr 0.02 sys + 0.26 cusr 0.14 csys = 0.47 CPU)
Result: FAIL
Note: The AnyEvent module currently fails to install against blead, so the fact that certain tests are SKIPped is not the problem here. The problem is that subtest EV
fails to run completely.
Not yet further investigated.
$ uname -mrs
FreeBSD 12.0-RELEASE amd64
$ thisperl -v | head -2 | tail -1
This is perl 5, version 31, subversion 10 (v5.31.10) built for amd64-freebsd-thread-multi
Thank you very much.
Jim Keenan
There is a few things that should be updated.
https://rt.cpan.org/Ticket/Display.html?id=96790
#!/usr/bin/perl use Mojo::Base -strict; use Mojo::UserAgent; my $titles; my $ua = Mojo::UserAgent->new; Mojo::IOLoop->delay( sub { my $delay = shift; $ua->get('http://google.com/', $delay->begin); $ua->get('http://yahoo.com/', $delay->begin); $ua->get('http://perlmonks.org/', $delay->begin); }, sub { my ($delay, $tx1, $tx2, $tx3) = @_; $titles = { google => $tx1->res->dom->at('title')->text, yahoo => $tx2->res->dom->at('title')->text, perlmonks => $tx3->res->dom->at('title')->text, }; }, )->catch( sub { my ($delay, $err) = @_; warn "failed to download or parse title\n"; } )->wait; say Mojo::Util::dumper($titles);
This commit d78f2f4 broke exceptions on Perl < 5.14 by localizing $@
.
See here for an explanation of the change: https://metacpan.org/pod/release/RJBS/perl-5.18.2/pod/perl5140delta.pod#Exception-Handling
The only thing I can think of doing is not localizing $@
in earlier Perls, which can be done efficiently (but hackishly) - see PR.
Hiya @stevan
I'm very much liking Promises, but have run into an issue with deep recursion. You do warn about it:
However, the one important difference that should be noted is that "Promise/A+" strongly suggests that the callbacks given to then should be run asynchronously (meaning in the next turn of the event loop). We do not do this because doing so would bind us to a given event loop implementation, which we very much want to avoid.
...but I didn't quite comprehend how important this issue was until too late.
For a lot of use cases, this limitation is not important. The stack size doesn't get that big before a return
which clears this all up. However, I have a couple of use cases where the size of the stack becomes a major problem.
This could be resolved by providing event-loop specific implementations, eg for AnyEvent all you'd need is something like:
package Promises::Deferred::AE;
use AE;
our @ISA = 'Promises::Deferred';
sub _notify {
my ($self, $callbacks, $result) = @_;
AE::postpone { $_->( @$result ) } foreach @$callbacks;
$self->{'resolved'} = [];
$self->{'rejected'} = [];
}
With an AE version and a Mojo version I think you'd cover most Perl async applications(?). Would you be willing to consider something like this?
The problem in #14 got me thinking about how callbacks are being called. Throwing exceptions in an event loop is problematic -- quite likely it'll end up with some condition never being met.
I'm not that familiar with the Promises spec, but it seems to say (I think) that if any callback throws an exception, that should be propagated to any reject handlers. Currently the code allows callbacks to call fatal exceptions, which would prevent that from happening.
have a look at this code: https://github.com/then/promise/blob/master/core.js#L25-L40
It wraps the callbacks in a try
block, so will never throw exceptions directly. What do you think? I'll put together a PR that I think implements this correctly.
Perhaps similar to #87
I've tried and failed to instal AnyEvent-HTTP-2.24
then Promises-1.02
into two different fresh perl installations (using the plenv
binary manager: https://github.com/tokuhirom/plenv).
Attached:
the full console log bug-report-full.txt
the build logs from perl 5.30.1 promises-build-5-30-1.log
and the SAME result from perl 5.14.4!) promises-build-5-14-4.log
Linux Ubuntu 18.04 / x86_64
$ uname -a
Linux rowan 4.15.0-47-generic #50-Ubuntu SMP Wed Mar 13 10:44:52 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
$ env | egrep '^(PATH|PERL|PLENV)'
PATH=/home/ddd/.plenv/shims:/home/ddd/.plenv/bin:/home/ddd/bin/linux-x86_64:/home/ddd/bin:/home/ddd/ds/bin:/home/ddd/work/home/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/usr/games:/snap/bin:/bin
PLENV_SHELL=bash
PLENV_VERSION=5.30.1
$ type -a perl
perl is /home/ddd/.plenv/shims/perl
perl is /usr/bin/perl
$ perl --version
This is perl 5, version 30, subversion 1 (v5.30.1) built for x86_64-linux
...
<<attached full output from perl -V
>>
$ env | egrep '^(PATH|PERL|PLENV)'
PATH=/home/ddd/.plenv/shims:/home/ddd/.plenv/bin:/home/ddd/bin/linux-x86_64:/home/ddd/bin:/home/ddd/ds/bin:/home/ddd/work/home/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/usr/games:/snap/bin:/bin
PLENV_SHELL=bash
PLENV_VERSION=5.30.1
$ type -a perl
perl is /home/ddd/.plenv/shims/perl
perl is /usr/bin/perl
$ plenv install 5.30.1
$ plenv local 5.30.1
$ plenv shell 5.30.1
$ plenv versions
system
5.14.4
$ type -a perl
perl is /home/ddd/.plenv/shims/perl
perl is /usr/bin/perl
$ plenv install-cpanm
$ type -a cpanm
cpanm is /home/ddd/.plenv/shims/cpanm
$ plenv which cpanm
/home/ddd/.plenv/versions/5.30.1/bin/cpanm
$ cpanm AnyEvent::HTTP
--> Working on AnyEvent::HTTP
...
4 distributions installed
t/031-deferred-AE.t (Wstat: 256 Tests: 2 Failed: 1)
Failed test: 2
Non-zero exit status: 1
t/032-deferred-AnyEvent.t (Wstat: 256 Tests: 2 Failed: 1)
Failed test: 2
Non-zero exit status: 1
Files=36, Tests=155, 10 wallclock secs ( 0.14 usr 0.08 sys + 4.07 cusr 0.54 csys = 4.83 CPU)
Result: FAIL
Failed 2/36 test programs. 2/155 subtests failed.
Makefile:947: recipe for target 'test_dynamic' failed
make: *** [test_dynamic] Error 255
-> FAIL Installing Promises failed. See /home/ddd/.cpanm/work/1577367375.6160/build.log for details. Retry with --force to force install it.
It'd be useful to have some mention of Future.pm, and maybe AnyEvent's condvars as well, perhaps in the SEE ALSO section. Since they're covering similar ground, a link and possibly a few comments about how Promises.pm compares to them in intent and functionality could be helpful to new users.
Also, is there any plan to include compatibility with Future.pm? Most of the IO::Async ecosystem is based on that (see Net::Async::* for example), along with a few other modules (such as Ryu).
(corresponding RT added for Future.pm here: https://rt.cpan.org/Ticket/Display.html?id=122847 )
t/031-deferred-AE.t .............. 1/?
# Failed test '... run asynchronously'
# at t/031-deferred-AE.t line 26.
# got: '0'
# expected: '1'
# Looks like you failed 1 test of 2.
t/031-deferred-AE.t .............. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/2 subtests
t/031-deferred-io-async.t ........ ok
t/032-deferred-AnyEvent.t ........ 1/?
# Failed test '... run asynchronously'
# at t/032-deferred-AnyEvent.t line 26.
# got: '0'
# expected: '1'
# Looks like you failed 1 test of 2.
We have experienced a couple of times that exceptions went unhandled and unnoticed because they were turned into a promise rejection that was never handled.
Looking at Promises.pm, I discovered warn_on_unhandled_reject
that is undocumented at the moment. There is a test for it though.
We would like to go a step further and not warn but die on unhandled rejections. It looks quite easy to refactor the current _set_warn_on_unhandled_reject
to handle both warn_on_unhandled_reject
and a new die_on_unhandled_reject
.
Would you be open to a PR for that?
I briefly considered subclassing the Promises
classes but it doesn't look feasible because of the way the different Promises::* classes interact. But if you can suggest away for us to achieve die_on_unhandled_reject
without modifying the Promises
code that would also be great!
(Edit: Appart from the obvious but kludgy $SIG{__WARN__}
handler that dies if $_[0] =~ /Promise's rejection .* was not handled/
)
Tried building from a fresh git checkout to look more closely at #88 , but the Makefile.PL
mentioned in README.md
is not present. That might be me confusing user documentation with developer documentation, but maybe a note for what explorers & potential contributors need to do?
in the dist.ini
I've been stealing some of the internal design of Promises
for Mojolicious
, and noticed that rejections are not propagated through the promise chain. It looks like this line should be $d->$method(@results);
instead of $d->resolve(@results);
.
You can test this by chaining multiple ->catch(sub {...})
calls. Propagating rejections is a requirement of Promises/A+.
Subset of #63
It'd be useful to have some mention of Future.pm, and maybe AnyEvent's condvars as well, perhaps in the SEE ALSO section. Since they're covering similar ground, a link and possibly a few comments about how Promises.pm compares to them in intent and functionality could be helpful to new users.
This code is found in Promises.pm
s _set_warn_on_unhandled_reject
:
( ' at ', join ' line ', @{$self->{_caller}} ) x !! $self->{_caller}
But even if $self->{_caller}
is undef, @{$self->{_caller}}
is still evaluated.
To reproduce:
#!/usr/bin/perl -w
use strict;
use Promises;
my $p = Promises::rejected();
# Simulate a late loaded class using warn_on_unhandled_reject
Promises->import('warn_on_unhandled_reject' => [1]);
which gives the error:
(in cleanup) Can't use an undefined value as an ARRAY reference at ...Promises.pm line 50.
As per #80 I am subclassing ::Deferred
. However I typically want to subclass a subclass of that depending on the IO loop in order to enable timeouts etc. What is the easiest way of doing this - the only way I could think of at the moment was to declare the subclass with
our @ISA = ( $Promises::Backend );
which seems really hacky to me and will only work if the backend is defined prior to using the subclass...
Now the exceptions happen in 'then' will be treat as reject and the reject handler will be called later, eg:
$promise->then( sub {
RUN_TIME_ERROR_HERE;
})->then(undef, sub {
FATAL @;
});
The FATAL will be called with something like:
Can't use string ("1455362825.71571") as an ARRAY ref while "strict refs" in use at /depot/perl-5.22.0/lib/5.22.0/x86_64-linux/Time/HiRes.pm line 74
But IMHO, exception is different with reject as later is the one that author intend to do so. Also in my application I would like to print the whole call stack when error happens, but now the error stack is like:
[STACK]
Package => main, File => a.pl, Line => 602, Sub => CommonUtil::FATAL
Package => Promises::Deferred, File => .../lib/perl5/Promises/Deferred.pm, Line => 133, Sub => main::ANON
Package => Promises::Deferred, File => .../lib/perl5/Promises/Deferred.pm, Line => 136, Sub => (eval)
Package => Promises::Deferred::EV, File =>.../lib/perl5/Promises/Deferred/EV.pm, Line => 19, Sub => Promises::Deferred::ANON
Package => main, File => a.pl, Line => 934, Sub => Promises::Deferred::EV::ANON
Package => main, File => a.pl, Line => 934, Sub => (eval)
Package => main, File => a.pl, Line => 1667, Sub => main::main
But what I want is to have the FATAL to show the line for _RUN_TIME_ERROR_HERE, so either
Sorry for my English :-)
Thanks.
A simple test case:
use Promises qw( collect deferred );
collect( deferred->resolve->promise );
throws:
Cannot resolve. Already resolved at /home/greg/perl5/perlbrew/perls/perl-5.16.2/lib/site_perl/5.16.3/Promises/Deferred.pm line 49.
It appears that the collect
promise is being resolved both in the loop and at the end of the method:
Cannot resolve. Already resolved at /home/greg/perl5/perlbrew/perls/perl-5.16.2/lib/site_perl/5.16.3/Promises/Deferred.pm line 49.
Promises::Deferred::resolve('Promises::Deferred=HASH(0x23a6848)', 'ARRAY(0x23a6a10)') called at /home/greg/perl5/perlbrew/perls/perl-5.16.2/lib/site_perl/5.16.3/Promises.pm line 60
Promises::collect('Promises::Promise=HASH(0x22b5570)') called at /home/greg/test.pl line 3
I assume this is not the intended behavior.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.