Search code examples
perlpromiseanyevent

Didn't go next **then** in Perl Promises and Anyevent


I try to make sync function

#!/usr/bin/perl
use strict;
use warnings;
use AnyEvent;
use AE;
use Time::Piece;
use Promises backend =>['AnyEvent'], 'deferred';
use feature q(say);
use Data::Dumper;

$| = 1;
my $inputs = 'starting';
my $w2; $w2 = AE::timer 5, 0, sub {
    say '***> AE::io timer : pass 5sec : ',
        Time::Piece->new(AE::now, "%s")->strftime("%Y-%m-%d %H:%M:%S");
    say '***> making : inputs = next';
    $inputs = 'next';
};
my $w3; $w3 = AE::timer 15, 15, sub {
    say 'AE::io timer : passed 15 sec : ',
        Time::Piece->new(AE::now, "%s")->strftime("%Y-%m-%d %H:%M:%S");
};
sub delay {
    my ($dl, $cb) = @_;
    my $t;
    $t = AE::timer $dl, 0, sub {
        say '===> delay : delayed ', $dl, ' sec';
        undef $t;
        $cb->() if ref $cb eq 'CODE';
    };
}
sub my_p_await{
    my ($sec, $cond, $cb) = @_;
    my $d = deferred;
    say '===> my_p_await: staring...with ',$sec, ' sec: ',
        Time::Piece->new(AE::now, "%s")->strftime("%Y-%m-%d %H:%M:%S");
    my $result = eval { $cond->() };
    if(!$result) {
        say '===> my_p_await: condition is false';
        delay($sec, sub {
            $cb->() if ref $cb eq 'CODE';
            my_p_await($sec, $cond, $cb);
        });
    }else{
        say '===> my_p_await: condition is true';
        $d->resolve;
        return;
    }
    return $d->promise;
}

deferred->resolve->promise->then( sub {
    my_p_await(3, sub { $inputs eq 'next'},sub {say '0: cb..'} )
})
->then( sub {
    say '';
    say '****>==> Time: 0: ',
        Time::Piece->new(AE::now, "%s")->strftime("%Y-%m-%d %H:%M:%S");
});

AE::cv->recv;

and result

===> my_p_await: staring...with 3 sec: 2024-08-26 18:55:13
===> my_p_await: condition is false
===> delay : delayed 3 sec
0: cb..
===> my_p_await: staring...with 3 sec: 2024-08-26 18:55:16
===> my_p_await: condition is false
***> AE::io timer : pass 5sec : 2024-08-26 18:55:18
***> making : inputs = next
===> delay : delayed 3 sec
0: cb..
===> my_p_await: staring...with 3 sec: 2024-08-26 18:55:19
===> my_p_await: condition is true
AE::io timer : passed 15 sec : 2024-08-26 18:55:28

It do not go next then, so I never see '****>==> Time: 0: ' message.

Even AE timer fired after 15 second, which AnyEvent running is fine.

First 5 second timer fired for $inputs string to 'next'.

so my_p_await's condition is true.

What I am doing wrong?


Solution

  • This is all you need:

    sub delay {
       my $sec = shift;
    
       my $promise = deferred;
    
       my $t;
       $t = AE::timer $sec, 0, sub {
          $t = undef;
          log "delay: Delayed $sec s.";
          $promise->resolve;
       };
    
       return $promise;
    }
    
    my $done = AE::cv;
    
    delay( 5 )->then( sub {
       log "then";
       $done->send;
    } );
    
    $done->recv;
    

    Explanation of Problem

    First of all,

    deferred->resolve->promise->then(sub {
        my_p_await(3, sub { $inputs eq 'next' })
    })
    ->then( sub {
        ...
    });
    

    is a complicated way of writing

    my_p_await(3, sub { $inputs eq 'next' })
    ->then(sub {
        ...
    });
    

    In both cases, the then code is only executed once the promise returned by my_p_await becomes resolved. If the condition is initially false, this never happens in your code.

    Sure, the promise returned by another call to my_p_await returns a resolved promise. But that's a different promise.

    So, we need to make sure the promise returned by initial call to my_p_wait gets resolved.


    Fix

    my_p_await is overly complicated, and fixing it would just add one to that. Addressing the complexity is the way to fix this. It's the use of a mix of "on success" callbacks and promises that makes my_p_await complex. There should only be one success mechanism.

    I'm going to look at delay first. delay doesn't mix "on success" callbacks and promises, but converting it to use promises will provide us guidance in fixing my_p_await.

    Promise-returning delay:

    sub delay {
       my $sec = shift;
    
       my $promise = deferred;
    
       log "delay: Delaying $sec s.";
    
       my $t;
       $t = AE::timer $sec, 0, sub {
          $t = undef;
          log "delay: Delayed $sec s.";
          $promise->resolve();
       };
    
       return $promise;
    }
    

    One catch: Were this to throw an exception, everything would go really wrong. The JavaScript Promise constructor provides a solution for this, but it didn't make it into the spec, and Promises(.pm) didn't copy that feature. So we're going to implement it:

    # The JavaScript Promise class provides an
    # exception-safe way of creating promises.
    # This feature didn't make it into the Promise/A+ spec.
    # And it didn't make it into Perl's Promises module.
    # So we're going to provide such a mechanism.
    # Note that the this passes a promise to the callback
    # instead of two functions.
    sub promise {
       my $cb = shift;  # Optional.
    
       my $promise = Promises::deferred;
    
       if ( $cb ) {
          eval { $cb->( $promise ); };
    
          $promise->reject( $@ ) if $@;
       }
    
       return $promise;
    }
    

    Using this, we can write a safe delay.

    sub delay {
       my $sec = shift;
    
       return promise sub {
          my $promise = shift;
    
          log "delay: Delaying $sec s.";
    
          my $t;
          $t = AE::timer $sec, 0, sub {
             $t = undef;
             log "delay: Delayed $sec s.";
             $promise->resolve();
          };
       };
    }
    

    We probably didn't need to do this since no exception should be thrown, but it's a bad practice to make such assumptions.

    Now, we have an idea what my_p_wait should look like. Let's eliminate the "on success" callback from my_p_wait (and rename it).

    sub polling_cond_wait {
       my ( $sec, $cond ) = @_;
    
       return promise sub {
          my $promise = shift;
    
          log "polling_cond_wait: Start";
    
          my $result = eval { $cond->() };
          if ( $result ) {
             log "polling_cond_wait: Condition is true";
             $promise->resolve();
          } else {
             log "polling_cond_wait: Condition is false";
    
             delay( $sec )->then( sub {
                polling_cond_wait( $sec, $cond )->then( sub {
                   $promise->resolve();
                } );
             } );
          }
       };
    }
    

    That just leaves the main code.

    use strict;
    use warnings;
    use feature qw( say );
    
    use AE;
    use AnyEvent;
    use Promises backend => [ 'AnyEvent' ];
    use Time::Piece qw( localtime );
    
    $| = 1;
    
    sub log {
       say localtime->strftime( "[%Y-%m-%d %H:%M:%S] " ), @_;
    }
    
    # ...
    
    my $done = AE::cv;
    my $inputs = 'starting';
    
    {
       my $t; $t = AE::timer 5, 0, sub {
          $t = undef;
          log "5 s timer completed. Making `$inputs` next.";
          $inputs = 'next';
       };
    }
    
    polling_cond_wait( 3, sub { $inputs eq 'next' } )->then( sub {
       log "then";
       $done->send;
    } );
    
    $done->recv;
    

    Output:

    [2024-08-26 12:38:53] polling_cond_wait: Starting.
    [2024-08-26 12:38:53] polling_cond_wait: Condition is false.
    [2024-08-26 12:38:53] delay: Delaying 3 s.
    [2024-08-26 12:38:56] delay: Delayed 3 s.
    [2024-08-26 12:38:56] polling_cond_wait: Starting.
    [2024-08-26 12:38:56] polling_cond_wait: Condition is false.
    [2024-08-26 12:38:56] delay: Delaying 3 s.
    [2024-08-26 12:38:58] 5 s timer completed. Making `starting` next.
    [2024-08-26 12:38:59] delay: Delayed 3 s.
    [2024-08-26 12:38:59] polling_cond_wait: Starting.
    [2024-08-26 12:38:59] polling_cond_wait: Condition is true.
    [2024-08-26 12:38:59] then
    

    Better alternatives

    There's no reason to poll here. The following is a better solution:

    my $done = AE::cv;
    my $promise = promise;
    
    {
       my $t; $t = AE::timer 5, 0, sub {
          $t = undef;
          log "5 s timer completed. Resolving promise.";
          $promise->resolve();
       };
    }
    
    $promise->then( sub {
       log "then";
       $done->send;
    } );
    
    $done->recv;
    

    And that simplifies to the following:

    my $done = AE::cv;
    
    delay( 5 )->then( sub {
       say ts(), " then";
       $done->send;
    } );
    
    $done->recv;