Search code examples
perlpromiseconcurrencymojolicious

Perl Mojolicious: chaining Promises


In a Mojolicious Controller I'd like to chain 2 sets of Promises. The first (e.g. delDB1 & delDB2) deletes from a source, and when done, the second set (e.g. addBD1 & addDB2) adds to the source.

Do you add another then? And if so, how do I pass the new set of Promises?

It doesn't seem right to nest the call inside.

my @promise;
foreach my $code (\&delDB1, \&delDB2) {
  my $prom = Mojo::Promise->new;
  Mojo::IOLoop->subprocess(
    sub {
      return $code->($number);
    },
    sub {
      my ($subprocess, $err, @res) = @_;
      return $prom->reject($err) if $err;
      $prom->resolve(@res);
    },
  );
  push @promise, $prom;
}

Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
Mojo::Promise->all(@promise)
  ->then(
  sub {
    my ($delDB1response, $delDB2response) = map {$_->[0]} @_;
    ### Nest a call for another set of promises?
    $app->render(openapi=>{messages=>"success"});
  })->catch(
    sub ($err) {
    warn "### Something went wrong: $err";
  })->wait;

Solution

  • Your question is unclear. I think you want the following:

    1. Try,
      1. Start delDB1.
      2. Start delDB2.
      3. Wait for both to complete.
      4. Start addDB1.
      5. Start addDB2.
      6. Wait for both to complete.
      7. Render a successful response.
    2. Catch,
      1. Render a failure response.

    This can be achieved using the following:

    Mojo::Promise
    ->all( delDB1_async(), delDB2_async() )
    ->then(sub {
       return Mojo::Promise->all( addDB1_async(), addDB2_async() );
    })
    ->then(sub {
       # Render successful response here...
    })
    ->catch(sub {
       # Render failure response here...
    })
    ->wait;
    

    In the above, _async functions are expected to return a promise.

    If we give the promises names for the sake of discussion, the code becomes

    my $promise1 = delDB1_async();
    my $promise2 = delDB2_async();
    
    my $promise3 = Mojo::Promise->all( $promise1, $promise2 );
    my $promise4 = $promise3->then(sub {
       my $promise5 = addDB1_async();
       my $promise6 = addDB2_async();
    
       my $promise7 = Mojo::Promise->all( $promise5, $promise6 );
    
       return $promise7;
    });
    
    my $promise8 = $promise4->then(sub {
       # Render successful response here...
    });
    
    my $promise9 = $promise8->catch(sub {
       # Render failure response here...
    });
    
    $promise9->wait;
    

    The key part is that $promise4 becomes fulfilled when $promise7 becomes fulfilled, and with the same value.


    But let's say you wanted

    1. Try,
      1. Start:
        1. Start delDB1.
        2. Wait for it to complete.
        3. Start addDB1.
        4. Wait for it to complete.
      2. Start:
        1. Start delDB2.
        2. Wait for it to complete.
        3. Start addDB2.
        4. Wait for it to complete.
      3. Wait for both to complete.
      4. Render a successful response.
    2. Catch,
      1. Render a failure response.

    This can be achieved using the following:

    Mojo::Promise
    ->all(
        delDB1_async()->then(sub { return addDB1_async() }),
        delDB2_async()->then(sub { return addDB2_async() }),
    )
    ->then(sub {
       # Render successful response here...
    })
    ->catch(sub {
       # Render failure response here...
    })
    ->wait;
    

    Example delDB1_async:

    sub delDB1_async {
       return Mojo::Promise->new(sub {
          my ( $resolve, $reject ) = @_;
          Mojo::IOLoop->subprocess(
             sub {
                ...
             },
             sub {
                my ( $subprocess, $err, @res ) = @_;
                $reject->( $err ) if $err;
                $resolve->( @res );
             }
          );
      });
    }
    

    Passing a sub to ->new is better because exceptions from that code result in in the promise getting rejected. This is great if the code accidentally throws an exception.

    ...except Mojo::Promise violates the spec to which it claims to abide. So you're currently screwed if the code accidentally throws an exception.