package Mojo::AsyncAwait; use Mojo::Base -strict; use Carp (); use Coro::State (); use Mojo::Util; use Mojo::Promise; use Scalar::Util (); use Sub::Util (); use Exporter 'import'; our @EXPORT = (qw/async await/); my $main = Coro::State->new; $main->{desc} = 'Mojo::AsyncAwait/$main'; # LIFO stack of coroutines waiting to come back to # always has $main as the bottom of the stack my @stack = ($main); # Coroutines that are ostensible done but need someone to kill them my @clean; # _push adds a coroutine to the stack and enters it # when control returns to the original pusher, it will clean up # any coroutines that are waiting to be cleaned up sub _push { push @stack, @_; $stack[-2]->transfer($stack[-1]); $_->cancel for @clean; @clean = (); } # _pop pops the current coroutine off the stack. If given a callback, it calls # a callback on it, otherwise, schedules it for cleanup. It then transfers to # the next one on the stack. Note that it can't pop-and-return (which would # make more sense) because any action on it must happen before control is # transfered away from it sub _pop (;&) { Carp::croak "Cannot leave the main thread" if $stack[-1] == $main; my ($cb) = @_; my $current = pop @stack; if ($cb) { $cb->($current) } else { push @clean, $current } $current->transfer($stack[-1]); } sub async { my $body = pop; my $opts = _parse_opts(@_); my @caller = caller; my $subname = "$caller[0]::__ASYNCSUB__"; my $bodyname = "$caller[0]::__ASYNCBODY__"; if (defined(my $name = $opts->{-name})) { $subname = $opts->{-install} ? "$caller[0]::$name" : "$subname($name)"; $bodyname .= "($name)"; } my $desc = "declared at $caller[1] line $caller[2]"; Sub::Util::set_subname($bodyname => $body) if Sub::Util::subname($body) =~ /::__ANON__$/; my $wrapped = sub { my @caller = caller; my $promise = Mojo::Promise->new; my $coro = Coro::State->new(sub { eval { BEGIN { $^H{'Mojo::AsyncAwait/async'} = 1 } $promise->resolve($body->(@_)); 1 } or $promise->reject($@); _pop; }, @_); $coro->{desc} = "$subname called at $caller[1] line $caller[2], $desc"; _push $coro; return $promise; }; if ($opts->{-install}) { Mojo::Util::monkey_patch $caller[0], $opts->{-name} => $wrapped; return; } Sub::Util::set_subname $subname => $wrapped; return $wrapped; } # this prototype prevents the perl tokenizer from seeing await as an # indirect method sub await (*) { { # check that our caller is actually an async function no warnings 'uninitialized'; my $level = 1; my ($caller, $hints) = (caller($level))[3, 10]; # being inside of an eval is ok too ($caller, $hints) = (caller(++$level))[3, 10] while $caller eq '(eval)'; Carp::croak 'await may only be called from in async function' unless $hints->{'Mojo::AsyncAwait/async'}; } my $promise = shift; $promise = Mojo::Promise->new->resolve($promise) unless Scalar::Util::blessed($promise) && $promise->can('then'); my (@retvals, $err); _pop { my $current = shift; $promise->then( sub { @retvals = @_; _push $current; }, sub { $err = shift; _push $current; } ); }; # "_push $current" in the above callback brings us here Carp::croak($err) if $err; return wantarray ? @retvals : $retvals[0]; } sub _parse_opts { return {} unless @_; return { -name => shift, -install => 1, } if @_ == 1; my %opts = @_; Carp::croak 'Cannot install a sub without a name' if $opts{-install} && !defined $opts{-name}; return \%opts; } 1; =encoding utf8 =head1 NAME Mojo::AsyncAwait - An Async/Await implementation for Mojolicious =head1 SYNOPSIS use Mojolicious::Lite -signatures; use Mojo::AsyncAwait; get '/' => async sub ($c) { my $mojo = await $c->ua->get_p('https://mojolicious.org'); my $cpan = await $c->ua->get_p('https://metacpan.org'); $c->render(json => { mojo => $mojo->result->code, cpan => $cpan->result->code }); }; app->start; =head1 DESCRIPTION Async/await is a language-independent pattern that allows nonblocking asynchronous code to be structured simliarly to blocking code. This is done by allowing execution to be suspended by the await keyword and returning once the promise passed to await has been fulfilled. This pattern simplies the use of both promises and nonblocking code in general and is therefore a very exciting development for writing asynchronous systems. If you are going to use this module to create async controllers actions in L applications (as seen in the L), you are highly encouraged to also use L in order to properly handle exceptions in your action. =head1 GOALS The primary goal of this module is to provide a useful Async/Await implementation for users of the Mojolicious ecosystem. It is for this reason that L is used when new promises are created. Because this is the primary goal, the intention is for it to remain useful even as other goals are considered. Secondarily, it is intended to be a testbed for early implementations of Async/Await in the Perl 5 language. It is for this reason that the implementation details are intended to be replaceable. This may manifest as a pluggable backend or rather as wholesale rewrites of the internals. The result should hopefully be backwards compatible, mostly because the interface is so simple, just two keywords. Of course, I always intend as much as possible that Mojolicious-focused code is as useful as practically possible for the broader Perl 5 ecosystem. It is for this reason that while this module returns Ls, it can accept any then-able (read: promise) which conforms enough to the Promises/A+ standard. The Promises/A+ standard is intended to increase the interoperability of promises, and while that line becomes more gray in Perl 5 where we don't have a single ioloop implementation, we try our best. As implementations stabilze, or change, certain portions may be spun off. The initial implementation depends on L. Should that change, or should users want to use it with other promise implementations, perhaps that implementation will be spun off to be used apart from L and/or L, perhaps not. Finally the third goal is to improve the mobility of the knowledge of this pattern between languages. Users of Javascript probably are already familiar with this patthern; when coming to Perl 5 they will want to continue to use it. Likewise, as Perl 5 users take on new languages, if they are familiar with common patterns in their new language, they will have an easier time learning. Having a useable Async/Await library in Perl 5 is key to keeping Perl 5 relevent in moderning coding. =head1 CAVEATS First and foremost, this is all a little bit crazy. Please consider carefully before using this code in production. While many languages have async/await as a core language feature, currently in Perl we must rely on modules that provide the mechanism of suspending and resuming execution. The default implementation relies on L which does some very magical things to the Perl interpreter. Other less magical implementations are in the works however none are available yet. In the future if additional implementations are available, this module might well be made pluggable. Please do not rely on L being the implmementation of choice. Also note that while a L-based implementation need not rely on L being called directly from an L function, it is currently prohibitied because it is likely that other/future implementations will rely on that behavior and thus it should not be relied upon. =head1 KEYWORDS L provides two keywords (i.e. functions), both exported by default. =head2 async my $sub = async sub { ... }; The async keyword wraps a subroutine as an asynchronous subroutine which is able to be suspended via L. The return value(s) of the subroutine, when called, will be wrapped in a L. The async keyword must be called with a subroutine reference, which will be the body of the async subroutine. Note that the returned subroutine reference is not invoked for you. If you want to immediately invoke it, you need to so manually. my $promise = async(sub{ ... })->(); If called with a preceding name, the subroutine will be installed into the current package with that name. async installed_sub => sub { ... }; installed_sub(); If called with key-value arguments starting with a dash, the following options are available. =over =item -install If set to a true value, the subroutine will be installed into the current package. Default is false. Setting this value to true without a C<-name> is an error. =item -name If C<-install> is false, this is a diagnostic name to be included in the subname for debugging purposes. This name is seen in diagnostic information, like stack traces. my $named_sub = async -name => my_name => sub { ... }; $named_sub->(); Otherwise this is the name that will be installed into the current package. =back Therefore, passing a bare name as is identical to setting both C<-name> and C<< -install => 1 >>. async -name => installed_sub, -install => 1 => sub { ... }; installed_sub(); If the subroutine is installed, whether by passing a bare name or the C<-install> option, nothing is returned. Otherwise the return value is the wrapped async subroutine reference. =head2 await my $tx = await Mojo::UserAgent->new->get_p('https://mojolicious.org'); my @results = await (async sub { ...; return @async_results })->(); The await keyword suspends execution of an async sub until a promise is fulfilled, returning the promise's results. In list context all promise results are returned. For ease of use, in scalar context the first promise result is returned and the remainder are discarded. If the value passed to await is not a promise (defined as having a C method>), it will be wrapped in a Mojo::Promise for consistency. This is mostly inconsequential to the user. Note that await can only take one promise as an argument. If you wanted to await multiple promises you probably want L or less likely L. my $results = await Mojo::Promise->all(@promises); =head1 AUTHORS Joel Berger Marcus Ramberg =head1 CONTRIBUTORS Sebastian Riedel =head1 ADDITIONAL THANKS Matt S Trout (mst) Paul Evans (LeoNerd) John Susek =head1 COPYRIGHT AND LICENSE Copyright (C) 2018, L and L. This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0. =head1 SEE ALSO L L L L L L =cut