Promise::Me - Fork Based Promise with Asynchronous Execution, Async, Await and Shared Data
use Promise::Me; # exports async, await and share my $p = Promise::Me->new(sub { # $_ is available as an array reference containing # $_->[0] the code reference to the resolve method # $_->[1] the code reference to the reject method # Some regular code here })->then(sub { my $res = shift( @_ ); # return value from the code executed above # more processing... })->then(sub { my $more = shift( @_ ); # return value from the previous then # more processing... })->catch(sub { my $exception = shift( @_ ); # error that occured is caught here })->finally(sub { # final processing })->then(sub { # A last then may be added after finally }; # You can share data among processes for all systems, including Windows my $data : shared = {}; my( $name, %attributes, @options ); share( $name, %attributes, @options ); my $p1 = Promise::Me->new( $code_ref )->then(sub { my $res = shift( @_ ); # more processing... })->catch(sub { my $err = shift( @_ ); # Do something with the exception }); my $p2 = Promise::Me->new( $code_ref )->then(sub { my $res = shift( @_ ); # more processing... })->catch(sub { my $err = shift( @_ ); # Do something with the exception }); my @results = await( $p1, $p2 ); # Wait for all promise to resolve. If one is rejected, this super promise is rejected my @results = Promise::Me->all( $p1, $p2 ); # First promise that is resolved or rejected makes this super promise resolved and # return the result my @results = Promise::Me->race( $p1, $p2 ); # Automatically turns this subroutine into one that runs asynchronously and returns # a promise async sub fetch_remote { # Do some http request that will run asynchronously thanks to 'async' } sub do_something { # some code here my $p = Promise::Me->new(sub { # some work that needs to run asynchronously })->then(sub { # More processing here })->catch(sub { # Oops something went wrong my $exception = shift( @_ ); }); # No need for this subroutine 'do_something' to be prefixed with 'async'. # This is not JavaScript you know await $p; } sub do_something { # some code here my $p = Promise::Me->new(sub { # some work that needs to run asynchronously })->then(sub { # More processing here })->catch(sub { # Oops something went wrong my $exception = shift( @_ ); })->wait; # Always returns a reference my $result = $p->result; }
v0.4.7
Promise::Me is an implementation of the JavaScript promise using fork for asynchronous tasks. Fork is great, because it is well supported by all operating systems (except AmigaOS, RISC OS and VMS) and effectively allows for asynchronous execution.
While JavaScript has asynchronous execution at its core, which means that two consecutive lines of code will execute simultaneously, under perl, those two lines would be executed one after the other. For example:
# Assuming the function getRemote makes an http query of a remote resource that takes time let response = getRemote('https://example.com/api'); console.log(response);
Under JavaScript, this would yield: undefined, but in perl
undefined
my $resp = $ua->get('https://example.com/api'); say( $resp );
Would correctly return the response object, but it will hang until it gets the returned object whereas in JavaScript, it would not wait.
In JavaScript, because of this asynchronous execution, before people were using callback hooks, which resulted in "callback from hell", i.e. something like this[1]:
getData(function(x){ getMoreData(x, function(y){ getMoreData(y, function(z){ ... }); }); });
[1] Taken from this StackOverflow discussion
And then, they came up with Promise, so that instead of wrapping your code in a callback function you get instead a promise object that gets called when certain events get triggered, like so[2]:
const myPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve('foo'); }, 300); }); myPromise .then(handleResolvedA, handleRejectedA) .then(handleResolvedB, handleRejectedB) .then(handleResolvedC, handleRejectedC);
[2] Taken from Mozilla documentation
Chaining is easy to implement in perl and Promise::Me does it too. Where it gets more tricky is returning a promise immediately without waiting for further execution, i.e. a deferred promise, like the following in JavaScript:
function getRemote(url) { let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // Maybe do some other stuff here return( promise ); }
In this example, under JavaScript, the promise will be returned immediately. However, under perl, the equivalent code would be executed sequentially. For example, using the excellent module Promise::ES6:
promise
sub get_remote { my $url = shift( @_ ); my $p = Promise::ES6->new(sub($res) { $res->( Promise::ES6->resolve(123) ); }); # Do some more work that would take some time return( $p ); }
In the example above, the promise $p would not be returned until all the tasks are completed before the return statement, contrary to JavaScript where it would be returned immediately.
$p
return
So, in perl people have started to use loop such as AnyEvent or IO::Async with "conditional variable" to get that asynchronous execution, but you need to use loops. For example (taken from Promise::AsyncAwait):
use Promise::AsyncAwait; use Promise::XS; sub delay { my $secs = shift; my $d = Promise::XS::deferred(); my $timer; $timer = AnyEvent->timer( after => $secs, cb => sub { undef $timer; $d->resolve($secs); }, ); return $d->promise(); } async sub wait_plus_1 { my $num = await delay(0.01); return 1 + $num; } my $cv = AnyEvent->condvar(); wait_plus_1()->then($cv, sub { $cv->croak(@_) }); my ($got) = $cv->recv();
So, in the midst of this, I have tried to provide something without event loop by using fork instead as exemplified in the "SYNOPSIS"
For a framework to do asynchronous tasks, you might also be interested in Coro, from Marc A. Lehmann original author of AnyEvent event loop.
my $p = Promise::Me->new(sub { # $_ is available as an array reference containing # $_->[0] the code reference to the resolve method # $_->[1] the code reference to the reject method my( $resolve, $reject ) = @$_; # some code to run asynchronously $resolve->(); # or $reject->(); # or maybe just die( "Something\n" ); # will be trapped by catch() }); # or my $p = Promise::Me->new(sub { # some code to run asynchronously }, { debug => 4, result_shared_mem_size => 2097152, shared_vars_mem_size => 65536, timeout => 2, medium => 'mmap' });
Instantiate a new Promise::Me object.
Promise::Me
It takes a code reference such as an anonymous subroutine or a reference to a subroutine, and optionally an hash reference of options.
The variable $_ is available and contains an array reference containing a code reference for $resolve and $reject. Thus if you wanted the execution fo your code to be resolved and calling "then", you could either return some return values, or explicitly call the code reference $resolve->(). Likewise if you want to force the promise to be rejected so it call the next chained "catch", you can explicitly call $reject->(). This is similar in spirit to what JavaScript Promise does.
$_
$resolve
$reject
$resolve->()
$reject->()
Also, if you return an exception object, whose class you have set with the exception_class option, Promise::Me will be able to detect it and call "reject" accordingly and pass it the exception object as its sole argument.
You can also die with a an exception object (see "die" in perlfunc) and it will be caught by Promise::Me and the exception object will be passed to "reject" calling the next chained "catch" method.
The options supported are:
Sets the debug level. This can be quite verbose and will slow down the process, so use with caution.
The exception class you want to use, so that Promise::Me can properly detect it when it is return from the main callback and call "reject", passing the exception object as it sole parameter.
This sets the medium type to use to share data between parent and child process. Possible values are: memory, mmap or file
memory
mmap
file
It defaults to the class variable $SHARE_MEDIUM
$SHARE_MEDIUM
See also the related method "medium"
Sets the shared memory segment to store the asynchronous process results. This default to the value of the global variable $RESULT_MEMORY_SIZE, which is by default 512K bytes, or if empty or not defined, the value of the constant Module::Generic::SharedMemXS::SHM_BUFSIZ, which is 64K bytes.
$RESULT_MEMORY_SIZE
Module::Generic::SharedMemXS::SHM_BUFSIZ
String. Specify the serialiser to use for Promise::Me. Possible values are: cbor, sereal or storable
By default, the value is set to the global variable $SERIALISER, which defaults to storable
$SERIALISER
storable
This value is passed to Module::Generic::File::Mmap, Module::Generic::File::Cache, or Module::Generic::SharedMemXS depending on your choice of shared memory medium.
Sets the shared memory segment to store the shared variable data, i.e. the ones declared with "shared". This defaults to the value of the global variable $SHARED_MEMORY_SIZE, which is by default 64K bytes, or if empty or not defined, the value of the constant Module::Generic::SharedMemXS::SHM_BUFSIZ, which is 64K bytes.
$SHARED_MEMORY_SIZE
The optional path to the temporary directory to use when you want to use file cache as a medium for shared data.
Currently unused.
Boolean. If true, Promise::Me will use a cache file instead of shared memory block. If you are on system that do not support shared memory, Promise::Me will automatically revert to Module::Generic::File::Cache to handle data shared among processes.
You can use the global package variable $SHARE_MEDIUM to set the default value for all object instantiation.
$SHARE_MEDIUM value can be either memory for shared memory, mmap for cache mmap or file for shared cache file.
Boolean. If true, Promise::Me will use a cache mmap file with Module::Generic::File::Mmap instead of a shared memory block. However, please note that you need to have installed Cache::FastMmap in order to use this.
This takes a code reference as its unique argument and is added to the chain of handlers.
It will be called upon an exception being met or if "reject" is called.
The callback subroutine will be passed the error object as its unique argument.
Be careful not to intentionally die in the catch block unless you have another catch block after, because if you die, it will trigger another catch, and you will not see that you died in the first place, because, well, it was caught... Instead you want to get the exception and log it, print it, do something with it.
catch
Sets or gets the medium type to be used to share data between parent and child process. Valid values are: memory, mmap and file
This takes one or more arguments that will be passed to the next "catch" handler, if any.
It will mark the promise as rejected and will go no further in the chain.
rejected
Takes a boolean value and sets or gets the rejected status of the promise.
This is typically set by "reject" and you should not call this directly, but use instead "reject".
This takes one or more arguments that will be passed to the next "then" handler, if any.
It will mark the promise as resolved and will the next "then" handler.
resolved
Takes a boolean value and sets or gets the resolved status of the promise.
This is typically set by "resolve" and you should not call this directly, but use instead "resolve".
This sets or gets the result returned by the asynchronous process. The data is exchanged through shared memory.
This method is used internally in combination with "await", "all" and "race"
The value returned is always a reference, such as array, hash or scalar reference.
If the asynchronous process returns a simple string for example, result will be an array reference containing that string.
result
Thus, unless the value returned is 1 element and it is a reference, it will be made of an array reference.
String. Sets or gets the serialiser to use for Promise::Me. Possible values are: cbor, sereal or storable
It will be called upon resolution of the promise or when "resolve" is called.
The callback subroutine is passed as arguments whatever the previous callback returned.
Sets gets a timeout. This is currently not used. There is no timeout for the asynchronous process.
If you want to set a timeout, you can use "wait", or "await"
This is a chain method whose purpose is to indicate that we must wait for the asynchronous process to complete.
It can be used before or after a call to "then" or "catch"
Promise::Me->new(sub { # Some operation to be run asynchronously })->then(sub { # Do some processing of the result })->catch(sub { # Cath any exceptions })->wait; # or Promise::Me->new(sub { # Some operation to be run asynchronously })->wait->then(sub { # Do some processing of the result }); # or even possibly Promise::Me->new(sub { # Some operation to be run asynchronously })->wait->catch(sub { my $exception = shift( @_ ); # Do some processing of a possible error });
But doing the following would not yield the expected result:
my $result = Promise::Me->new(sub { # Some operation to be run asynchronously })->wait->result;
That's because the promise has not been given a chance to be executed, and the promise is executed on the last "then" or "catch"
Provided with one or more Promise::Me objects, and this will wait for all of them to be resolved.
It returns an array equal in size to the number of promises provided initially.
However, if one promise is rejected, "all" stops and returns it immediately.
my @results = Promise::Me->all( $p1, $p2, $p3 );
Contrary to its JavaScript equivalent, you do not need to pass an array reference of promises, although you could.
# Works too, but not mandatory my @results = Promise::Me->all( [ $p1, $p2, $p3 ] );
See also Mozilla documentation for more information.
Provided with one or more Promise::Me objects, and this will return the result of the first promise that resolves or is rejected.
# Works too, but not mandatory my @results = Promise::Me->race( [ $p1, $p2, $p3 ] );
This is a static function exported by default and that wrap the subroutine thus prefixed into one that returns a promise and return its code asynchronously.
For example:
async sub fetch { my $ua = LWP::UserAgent->new; my $res = $ua->get( 'https://example.com' ); }
This would be equivalent to:
Promise::Me->new(sub { my $ua = LWP::UserAgent->new; my $res = $ua->get( 'https://example.com' ); });
Of course, since, in our example above, fetch would return a promise, you could chain "then", "catch" and "finally", such as:
fetch
async sub fetch { my $ua = LWP::UserAgent->new; my $res = $ua->get( 'https://example.com' ); }->then(sub { my $res = shift( @_ ); if( !$resp->is_success ) { die( My::Exception->new( "Unable to fetch remote content." ) ); } })->catch(sub { my $exception = shift( @_ ); $logger->warn( $exception ); })->finally(sub { $dbi->disconnect; });
See Mozilla documentation for more information on async
async
Provided with one or more promises and "await" will wait until each one of them is completed and return an array of their result with one entry per promise. Each promise result is a reference (array, hash, or scalar, or object for example)
my @results = await( $p1, $p2, $p3 );
This locks a shared variable.
my $data : shared = {}; lock( $data ); $data->{location} = 'Tokyo'; unlock( $data );
See "SHARED VARIABLES" for more information about shared variables.
Provided with one or more variables and this will enable them to be shared with the asynchronous processes.
Currently supported variable types are: array, hash and scalar (string) reference.
my( $name, @first_names, %preferences ); share( $name, @first_names, %preferences ); $name = 'Momo Taro'; Promise::Me->new(sub { $preferences{name} = $name = 'Mr. ' . $name; print( "Hello $name\n" ); $preferences{location} = 'Okayama'; $preferences{lang} = 'ja_JP'; $preferences{locale} = '桃太郎'; # Momo Taro my $rv = $tbl->insert( \%$preferences )->exec || die( My::Exception->new( $tbl->error ) ); $rv; })->then(sub { my $mail = My::Mailer->new( to => $preferences{email}, name => $preferences{name}, body => $welcome_ja_file, ); $mail->send || die( $mail->error ); })->catch(sub { my $exception = shift( @_ ); $logger->write( $exception ); })->finally(sub { $dbh->disconnect; });
It will try to use shared memory or shared cache file depending on the value of the global package variable $SHARE_MEDIUM, which can be either file for Module::Generic::File::Cache, mmap for Module::Generic::File::Mmap or memory for Module::Generic::File::SharedMem
The value of $SHARED_MEMORY_SIZE, and $SERIALISER will be passed when instantiating objects for those shared memory medium.
This unlocks a shared variable. It has no effect on variable that have not already been shared.
Unshare a variable. It has no effect on variable that have not already been shared.
This should only be called before the promise is created.
This is called each time a "finally" method is called and will add to the chain the code reference provided.
This is called each time a "catch" method is called and will add to the chain the code reference provided.
This is called each time a "then" method is called and will add to the chain the code reference provided.
This method is called upon promise object instantiation when initially called by "async".
It is used to capture arguments so they can be passed to the code executed asynchronously.
This method is called at the end of the chain. It will prepare shared variable for the child process, launch a child process using "fork" in perlfunc and will call the next "then" handler if the code executed successfully, or "reject" if there was an error.
This corresponds to $?. After the child process exited, "_set_exit_values" is called and sets the value for this.
$?
This corresponds to the integer value of the signal, if any, used to interrupt the asynchronous process.
This is the integer value of the exit for the asynchronous process. If a process exited normally, this value should be 0.
This is called by the import method to filter the code using perl filter with XS module Filter::Util::Call and enables data sharing, and implementation of async subroutine prefix. It relies on XS module PPI for parsing perl code.
import
This is called when all chaining is complete to get the "finally" handler, if any.
Get the next handler by type, i.e. then, catch or finally
then
finally
This is called to get the next "catch" handler when a promise has been rejected, such as when an error has occurred.
This is called to get the next "then" handler and execute its code passing it the return value from previous block in the chain.
Returns true if the asynchronous process last exited with a core dump, false otherwise.
Returns true if we are called from within the asynchronous process.
Returns true if we are called from within the main parent process.
This is set to true automatically when the end of the method chain has been reached.
Returns the pid of the asynchronous process.
This is a promise instantiation option. When set to true, the shared variables will be automatically removed from memory upon end of the main process.
This is true by default. If you want to set it to false, you can do:
Promise::Me->new(sub { # some code here }, {share_auto_destroy => 0})->then(sub { # some more work here, etc. });
This returns the object used for sharing data and result between the main parent process and the asynchronous child process. It can be Module::Generic::SharedMemXS, Module::Generic::File::Mmap or Module::Generic::File::Cache depending on the value of $SHARE_MEDIUM, which can be set to, respectively, memory, mmap or file
Boolean. Default to true. If true, the shared space used by the parent and child processes will be destroy automatically. Disable this if you want to debug or take a sneak peek into the data. The shared space will be either shared memory of cache file depending on the value of $SHARE_MEDIUM
This is a boolean value which is set automatically when a promise is instantiated from "async".
It enables subroutine arguments to be passed to the code being run asynchronously.
Used for debugging purpose only, this will print out the PPI structure of the code filtered and parsed.
After the code has been collected, this method will quickly parse it and make changes to enable "async"
This is a common code called by either "resolve" or "reject"
This is called upon the exit of the asynchronous process to set some general value about how the process exited.
See "exit_bit", "exit_signal" and "exit_status"
This is called in "exec" to share data including result between main parent process and asynchronous process.
It is important to be able to share variables between processes in a seamless way.
When the asynchronous process is executed, the main process first fork and from this point on all data is being duplicated in an impermeable way so that if a variable is modified, it would have no effect on its alter ego in the other process; thus the need for shareable variables.
You can enable shared variables in two ways:
my $name : shared; # Initiate a value my $location : shared = 'Tokyo'; # you can also use 'pshared' my $favorite_programming_language : pshared = 'perl'; # You can share array, hash and scalar my %preferences : shared; my @names : shared;
my( $name, %prefs, @middle_names ); share( $name, %prefs, @middle_names );
Once shared, you can use those variables normally and their values will be shared between the parent process and the asynchronous process.
my( $name, @first_names, %preferences ); share( $name, @first_names, %preferences ); $name = 'Momo Taro'; Promise::Me->new(sub { $preferences{name} = $name = 'Mr. ' . $name; print( "Hello $name\n" ); $preferences{location} = 'Okayama'; $preferences{lang} = 'ja_JP'; $preferences{locale} = '桃太郎'; my $rv = $tbl->insert( \%$preferences )->exec || die( My::Exception->new( $tbl->error ) ); $rv; })->then(sub { my $mail = My::Mailer->new( to => $preferences{email}, name => $preferences{name}, body => $welcome_ja_file, ); $mail->send || die( $mail->error ); })->catch(sub { my $exception = shift( @_ ); $logger->write( $exception ); })->finally(sub { $dbh->disconnect; });
If you want to mix this feature and the usage of threads' shared feature, use the keyword pshared instead of shared, such as:
shared
pshared
my $name : pshared;
Otherwise the two keywords would conflict.
This module uses shared memory using Module::Generic::SharedMemXS, or shared cache file using Module::Generic::File::Cache if shared memory is not supported, or if the value of the global package variable $SHARE_MEDIUM is set to file instead of memory. Alternatively you can also have Promise::Me use cache mmap file by setting $SHARE_MEDIUM to mmap. This will have it use Module::Generic::File::Mmap, but note that you will need to install Cache::FastMmap
The value of $SHARE_MEDIUM is automatically initialised to memory if the system, on which this module runs, supports IPC::SysV, or mmap if you have Cache::FastMmap installed, or else to file
Shared memory is used for:
You can control how much shared memory is allocated for each by:
If you use shared cache file, then not setting a size is ok. It will use the space on the filesystem as needed and obviously return an error if there is no space left.
You can alternatively use Module::Generic::File::Mmap, which has an API similar to Module::Generic::File::Cache, but uses an mmap file instead of a simple cache file and rely on the XS module Cache::FastMmap, and thus is faster.
Because Promise::Me forks a separate process to run the code provided in the promise, two promises can run simultaneously. Let's take the following example:
use Time::HiRes; my $result : shared = ''; my $p1 = Promise::Me->new(sub { sleep(1); $result .= "Peter "; })->then(sub { print( "Promise 1: result is now: '$result'\n" ); }); my $p2 = Promise::Me->new(sub { sleep(0.5); $result .= "John "; })->then(sub { print( "Promise 2: result is now: '$result'\n" ); }); await( $p1, $p2 ); print( "Result is: '$result'\n" );
This will yield:
Promise 2: result is now: 'John ' Promise 1: result is now: 'John Peter ' Result is: 'John Peter '
This is the size in bytes of the shared memory block used for sharing result between sub process and main process, such as when you call:
my $res = $prom->result;
It defaults to 512Kb
A string representing the serialiser to use by default. A serialiser is used to serialiser data to share them between processes. This defaults to storable
Currently supported serialisers are: CBOR::XS, Sereal and Storable
You can set accordingly the value for $SERIALISER to: cbor, sereal or storable
cbor
sereal
You can override this global value when you instantiate a new Promise::Me object with the serialiser option. See "new"
serialiser
Note that the serialiser used to serialise shared variable, is set only via this class variable $SERIALISER
This is the size in bytes of the shared memory block used for sharing variables between the main process and the sub processes. This is used when you share variables, such as:
my $name : shared; my( $name, %prefs, @middle_names ); share( $name, %prefs, @middle_names );
See "SHARED VARIABLES"
Promise::Me uses the following supported serialiser to serialise shared data across processes:
CBOR
Sereal
Storable
You can set which one to use globally by setting the class variable $SERIALISER to cbor, sereal or to storable
You can also set which serialiser to use on a per promise object by setting the option serialiser. See "new"
Jacques Deguest <jack@deguest.jp>
Promise::XS, Promise::E6, Promise::AsyncAwait, AnyEvent::XSPromises, Async, Promises, Mojo::Promise
Mozilla documentation on promises
Copyright(c) 2021-2022 DEGUEST Pte. Ltd. DEGUEST Pte. Ltd.
All rights reserved
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
To install Promise::Me, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Promise::Me
CPAN shell
perl -MCPAN -e shell install Promise::Me
For more information on module installation, please visit the detailed CPAN module installation guide.