package Net::Curl::Promiser;
use strict;
use warnings;
our $VERSION = '0.10';
=encoding utf-8
=head1 NAME
Net::Curl::Promiser - Asynchronous L , the easy way!
=head1 DESCRIPTION
L is powerful but tricky to use: polling, callbacks,
timers, etc. This module does all of that for you and puts a Promise
interface on top of it, so asynchronous I/O becomes almost as simple as
synchronous I/O.
L itself is a base class; you’ll need to provide
an interface to whatever event loop you use. See L
below.
This distribution provides the following as both demonstrations and
portable implementations:
=over
=item * L (for L)
=item * L (for L)
=item * L (for L)
=item * L (for manually-written
C loops)
=back
(See the distribution’s F directory for one based on Linux’s
C.)
=head1 PROMISE IMPLEMENTATION
This class’s default Promise implementation is L.
You can use a different one by overriding the C method in
a subclass, as long as the substitute class’s C method works the
same way as Promise::ES6’s (which itself follows the ECMAScript standard).
(NB: L uses L instead of
Promise::ES6.)
=head2 B L support
Try out experimental Promise::XS support by running with
C in your environment.
This will override C.
=cut
#----------------------------------------------------------------------
use Net::Curl::Multi ();
use constant _DEBUG => 0;
use constant _DEFAULT_TIMEOUT => 1000;
use constant PROMISE_CLASS => 'Promise::ES6';
#----------------------------------------------------------------------
=head1 GENERAL-USE METHODS
The following are of interest to any code that uses this module:
=head2 I->new(@ARGS)
Instantiates this class. This creates an underlying
L object and calls the subclass’s C<_INIT()>
method at the end, passing a reference to @ARGS.
(Most end classes of this module do not require @ARGS.)
=cut
sub new {
my ($class, @args) = @_;
my %props = (
callbacks => {},
to_fail => {},
);
my $self = bless \%props, $class;
my $multi = Net::Curl::Multi->new();
$self->{'multi'} = $multi;
$multi->setopt(
Net::Curl::Multi::CURLMOPT_SOCKETDATA,
$self,
);
$multi->setopt(
Net::Curl::Multi::CURLMOPT_SOCKETFUNCTION,
\&_socket_fn,
);
$self->_INIT(\@args);
return $self;
}
#----------------------------------------------------------------------
=head2 promise($EASY) = I->add_handle( $EASY )
A passthrough to the underlying L object’s
method of the same name, but the return is given as a Promise object.
That promise resolves with the passed-in $EASY object.
It rejects with either the error given to C or the
error that L object’s C returns.
B As with libcurl itself, HTTP-level failures
(e.g., 4xx and 5xx responses) are B considered failures at this level.
=cut
sub add_handle {
my ($self, $easy) = @_;
$self->{'multi'}->add_handle($easy);
my $env_engine = $ENV{'NET_CURL_PROMISER_PROMISE_ENGINE'} || q<>;
my $promise;
if ($env_engine eq 'Promise::XS') {
require Promise::XS;
my $deferred = Promise::XS::deferred();
$self->{'deferred'}{$easy} = $deferred;
$promise = $deferred->promise();
}
elsif ($env_engine) {
die "bad promise engine: [$env_engine]";
}
else {
$self->PROMISE_CLASS()->can('new') or do {
my $class = $self->PROMISE_CLASS();
local $@;
die if !eval "require $class";
};
$promise = $self->PROMISE_CLASS()->new( sub {
$self->{'callbacks'}{$easy} = \@_;
} );
}
return $promise;
}
=head2 $obj = I->cancel_handle( $EASY )
Prematurely cancels $EASY. The associated promise will be abandoned
in pending state, never to resolve nor reject.
Returns I.
=cut
sub cancel_handle {
my ($self, $easy) = @_;
$self->{'to_fail'}{$easy} = [ $easy ];
return $self;
}
=head2 $obj = I->fail_handle( $EASY, $REASON )
Like C but rejects $EASY’s associated promise
with the given $REASON.
Returns I.
=cut
sub fail_handle {
my ($self, $easy, $reason) = @_;
$self->{'to_fail'}{$easy} = [ $easy, \$reason ];
return $self;
}
#----------------------------------------------------------------------
=head2 $obj = I->setopt( … )
A passthrough to the underlying L object’s
method of the same name. Returns I to facilitate chaining.
C or C are set internally;
any attempt to set them via this interface will prompt an error.
=cut
sub setopt {
my $self = shift;
for my $opt ( qw( SOCKETFUNCTION SOCKETDATA ) ) {
my $fullopt = "CURLMOPT_$opt";
if ($_[0] == Net::Curl::Multi->can($fullopt)->()) {
my $ref = ref $self;
die "Don’t set $fullopt via $ref!";
}
}
$self->{'multi'}->setopt(@_);
return $self;
}
=head2 $obj = I->handles( … )
A passthrough to the underlying L object’s
method of the same name.
=cut
sub handles {
return shift()->{'multi'}->handles();
}
#----------------------------------------------------------------------
=head1 EVENT LOOP METHODS
The following are needed only when you’re managing an event loop directly:
=head2 $num = I->get_timeout()
Returns the underlying L object’s C
value, with a suitable (positive) default substituted if that value is
less than 0.
(NB: This value is in I.)
This may not suit your needs; if you wish/need, you can handle timeouts
via the L
callback instead.
This should only be called (if it’s called at all) from event loop logic.
=cut
sub get_timeout {
my ($self) = @_;
my $timeout = $self->{'multi'}->timeout();
return( $timeout < 0 ? _DEFAULT_TIMEOUT() : $timeout );
}
#----------------------------------------------------------------------
=head2 $obj = I->process( @ARGS )
Tell the underlying L object which socket events have
happened.
If, in fact, no events have happened, then this calls
C on the
L object (similar to C).
Finally, this reaps whatever pending HTTP responses may be ready and
resolves or rejects the corresponding Promise objects.
This should only be called from event loop logic.
Returns I.
=cut
sub process {
my ($self, @fd_action_args) = @_;
my $fd_action_hr = $self->_GET_FD_ACTION(\@fd_action_args);
if (%$fd_action_hr) {
for my $fd (keys %$fd_action_hr) {
$self->{'multi'}->socket_action( $fd, $fd_action_hr->{$fd} );
}
}
else {
$self->{'multi'}->socket_action( Net::Curl::Multi::CURL_SOCKET_TIMEOUT() );
}
$self->_process_pending();
return $self;
}
#----------------------------------------------------------------------
=head2 $is_active = I->time_out();
Tell the underlying L object that a timeout happened,
and reap whatever pending HTTP responses may be ready.
Calls C on the
underlying L object. The return is the same as
that operation returns.
Since C can also do the work of this function, a call to this
function is just an optimization.
This should only be called from event loop logic.
=cut
sub time_out {
my ($self) = @_;
my $is_active = $self->{'multi'}->socket_action( Net::Curl::Multi::CURL_SOCKET_TIMEOUT() );
$self->_process_pending();
return $is_active;
}
#----------------------------------------------------------------------
=head1 SUBCLASS INTERFACE
B The distribution provides several ready-built end classes;
unless you’re managing your own event loop, you don’t need to concern
yourself with this.
To use Net::Curl::Promiser, you’ll need a subclass that defines
the following methods:
=over
=item * C<_INIT(\@ARGS)>: Called at the end of C. Receives a reference
to the arguments given to C.
=item * C<_SET_POLL_IN($FD)>: Tells the event loop that the given file
descriptor is ready to read.
=item * C<_SET_POLL_OUT($FD)>: Like C<_SET_POLL_IN()> but for a write event.
=item * C<_SET_POLL_INOUT($FD)>: Like C<_SET_POLL_IN()> but registers
a read and write event simultaneously.
=item * C<_STOP_POLL($FD)>: Tells the event loop that the given file
descriptor is finished.
=item * C<_GET_FD_ACTION(\@ARGS)>: Receives a reference to the arguments
given to C and returns a reference to a hash of
( $fd => $event_mask ). $event_mask is the sum of
C and/or
C, depending on which events
are available.
=back
B Your event loop B B close file descriptors. This means
that, if you create Perl filehandles from the file descriptors, you need to
prevent Perl from closing the underlying file descriptors.
=cut
#----------------------------------------------------------------------
sub _socket_fn {
my ( $fd, $action, $self ) = @_[2, 3, 5];
if ($action == Net::Curl::Multi::CURL_POLL_IN) {
print STDERR "FD $fd, IN\n" if _DEBUG;
$self->_SET_POLL_IN($fd);
}
elsif ($action == Net::Curl::Multi::CURL_POLL_OUT) {
print STDERR "FD $fd, OUT\n" if _DEBUG;
$self->_SET_POLL_OUT($fd);
}
elsif ($action == Net::Curl::Multi::CURL_POLL_INOUT) {
print STDERR "FD $fd, INOUT\n" if _DEBUG;
$self->_SET_POLL_INOUT($fd);
}
elsif ($action == Net::Curl::Multi::CURL_POLL_REMOVE) {
print STDERR "FD $fd, STOP\n" if _DEBUG;
$self->_STOP_POLL($fd);
# In case we got a read and a remove right away.
# This *may* not be needed but doesn’t seem to hurt.
$self->_process_pending();
}
else {
warn "$self: Unrecognized action $action on FD $fd\n";
}
return 0;
}
sub _finish_handle {
my ($self, $easy, $cb_idx, $payload) = @_;
# If $cb_idx == 0, then $payload is a promise resolution.
# If $cb_idx == 1, then $payload is either:
# undef - request canceled
# scalar ref - promise rejection
my $err = $@;
# Don’t depend on the caller to report failures.
# (AnyEvent, for example, blackholes them.)
warn if !eval {
delete $self->{'to_fail'}{$easy};
if ( my $cb_ar = delete $self->{'callbacks'}{$easy} ) {
$cb_ar->[$cb_idx]->($cb_idx ? $$payload : $payload) if !$cb_idx || $payload;
}
elsif ( my $deferred = delete $self->{'deferred'}{$easy} ) {
if ($cb_idx) {
$deferred->reject($$payload) if $payload;
}
else {
$deferred->resolve($payload);
}
}
else {
# This shouldn’t happen, but just in case:
require Data::Dumper;
print STDERR Data::Dumper::Dumper( ORPHAN => $easy => $payload );
}
$self->{'multi'}->remove_handle( $easy );
1;
};
$@ = $err;
return;
}
sub _clear_failed {
my ($self) = @_;
for my $val_ar ( values %{ $self->{'to_fail'} } ) {
my ($easy, $reason_sr) = @$val_ar;
$self->_finish_handle( $easy, 1, $reason_sr );
}
%{ $self->{'to_fail'} } = ();
return;
}
sub _process_pending {
my ($self) = @_;
$self->_clear_failed();
while ( my ( $msg, $easy, $result ) = $self->{'multi'}->info_read() ) {
if ($msg != Net::Curl::Multi::CURLMSG_DONE()) {
die "Unrecognized info_read() message: [$msg]";
}
$self->_finish_handle(
$easy,
($result == 0) ? ( 0 => $easy ) : ( 1 => \$result ),
);
}
return;
}
#----------------------------------------------------------------------
=head1 EXAMPLES
See the distribution’s F directory.
=head1 SEE ALSO
If you use L, then L with
L may be a nicer fit for you.
=head1 REPOSITORY
L
=head1 LICENSE & COPYRIGHT
Copyright 2019-2020 Gasper Software Consulting.
This library is licensed under the same terms as Perl itself.
=cut
1;