h q
h q

Reputation: 1510

Fetching data simultaneously in Mojolicious

I'm trying to run multiple subroutines in Parallel (to fetch data from external systems). To simulate, I use sleep in my example below. My question is: how can I achieve this in Mojolicious?

#!/usr/bin/perl
use Mojolicious::Lite;
use Benchmark qw(:hireswallclock);

sub  add1 { my $a = shift; sleep 1; return $a+1; }
sub mult2 { my $b = shift; sleep 1; return $b*2; }
sub power { my ($x, $y) = @_; sleep 1; return $x ** $y; }

any '/' => sub {    
    my ( $self ) = @_;

    my $n = int(rand(5));

    my $t0 = Benchmark->new;
    my $x = mult2($n); # Need to run in parallel
    my $y =  add1($n); # Need to run in parallel
    my $z = power($x,$y);
    my $t1 = Benchmark->new;
    my $t = timediff($t1,$t0)->real();

    $self->render(text => "n=$n, x=$x, y=$y, z=$z;<br>T=$t seconds");
};

app->start;

In other words, I'd like to reduce the time it takes to run down to 2 seconds (instead of 3) by running (add1 & mult2) in parallel.

This thread uses a Mojo::IOLoop->timer which doesn't seem relevant in my case? If so, I don't know how to use it. Thanks!

Upvotes: 2

Views: 891

Answers (2)

patrickiom
patrickiom

Reputation: 33

Concurrent vs Parallel

In order for two events to occur simultaneously ( in parallel ) you require multiple processing units.

For instance, with a single CPU you are only able to carry out a single mathematical operation at any single time so they will have to run in series regardless of the concurrent themes in your code.

Non-mathematical operations such as input/output ( e.g. network, HDD ) can occur in parallel fashion as these are, in most part, independent of your single CPU (I'll leave multi-core systems out of the explanation as generally speaking Perl is not optimised for their use).

Hypnotoad, Mojolicious' production web-server, relies on proper implementation of non-blocking IO. As such they have provided a non-blocking user-agent as part of your controller.

$controller->ua->get(
    $the_url,
    sub {
        my ( $ua, $tx ) = @_;
        if ( my $result = $tx->success ) {
            # do stuff with the result
        }
        else {
            # throw error
        }
    }
);

You can implement Mojo::Promise here to improve the flow your code.

If possible, I would recommend implementing a non-blocking UA when fetching data from "external systems". If you find that your Hypnotoad worker processes are blocking for too long ( 5 seconds ) they are likely to be killed off and replaced which may disrupt your system.

Upvotes: 1

amon
amon

Reputation: 57600

To avoid long waiting times, you can use the Mojolicious non-blocking operations. Instead of running a synchronous request to an external system, use non-blocking methods that instead run some callback upon completion. E.g. to avoid a sleep, we would use Mojo::IOLoop->timer(...).

Here is a variant of your code that uses non-blocking operations, and uses Promises to properly sequence the functions:

use Mojolicious::Lite;
use Benchmark qw(:hireswallclock);

# example using non-blocking APIs
sub add1 {
    my ($a) = @_;
    my $promise = Mojo::Promise->new;
    Mojo::IOLoop->timer(1 => sub { $promise->resolve($a + 1) });
    return $promise;
}

# example using blocking APIs in a subprocess
sub mult2 {
    my ($b) = @_;
    my $promise = Mojo::Promise->new;
    Mojo::IOLoop->subprocess(
        sub {  # first callback is executed in subprocess
            sleep 1;
            return $b * 2;
        },
        sub {  # second callback resolves promise with subprocess result
            my ($self, $err, @result) = @_;
            return $promise->reject($err) if $err;
            $promise->resolve(@result);
        },
    );
    return $promise;
}

sub power {
    my ($x, $y) = @_;
    my $result = Mojo::Promise->new;
    Mojo::IOLoop->timer(1 => sub { $result->resolve($x ** $y) });
    return $result;
}

any '/' => sub {
    my ( $self ) = @_;

    my $n = int(rand(5));

    my $t0 = Benchmark->new;
    my $x_promise = mult2($n);
    my $y_promise = add1($n);
    my $z_promise = Mojo::Promise->all($x_promise, $y_promise)
        ->then(sub {
            my ($x, $y) = map { $_->[0] } @_;
            return power($x, $y);
        });
    Mojo::Promise->all($x_promise, $y_promise, $z_promise)
        ->then(sub {
            my ($x, $y, $z) = map { $_->[0] } @_;
            my $t1 = Benchmark->new;
            my $t = timediff($t1, $t0)->real();

            $self->render(text => "n=$n, x=$x, y=$y, z=$z;\nT=$t seconds\n");
        })
        ->wait;
};

app->start;

This is a lot more complex than your code, but completes within two seconds instead of three seconds, and does not block other requests that happen at the same time. So you could request this route thirty times at once, and get thirty responses two seconds later!

Note that Promise->all returns a promise with the values of all awaited promises, but puts the values of each promise into an array ref so we need to unpack those to get the actual values.

If you cannot use non-blocking operations, you can run the blocking code in a subprocess, using Mojo::IOLoop->subprocess(...). You can still coordinate the data flow through promises. E.g. see the above mult2 function for an example.

Upvotes: 8

Related Questions