Mojolicous, render_later and weaken transactions

When using render_later method, did you ever encountered this Mojolicious error?

Can't call method "res" on an undefined value at /home/max/perl5/lib/perl5/Mojolicious/Controller.pm line 275.

Take the following simple server example using AnyEvent and EV:

use 5.010;
use strict;
use warnings;

use EV;
use AnyEvent;
use Mojolicious::Lite;
use Mojo::Server::Daemon;

get '/test/:num' => sub
{
    my $self = shift;

    $self->render_later;

    my $timer;
    $timer = AE::timer(0.2, 0, sub
                       {
                           undef $timer;
                           $self->render(json => { value => 1234 });
                       });
};

my $daemon = Mojo::Server::Daemon::->new(app => app());

$daemon->start;

AE::cv->recv;

For each request /test/* it replies 0.2 second later with a JSON content. Pretty simple. Execute it without any argument:

~> ./server.pl
[Fri Mar 29 10:33:44 2013] [info] Listening at "http://*:3000".
Server available at http://127.0.0.1:3000.

Now try to produce a small load on it using the following program:

use 5.010;
use strict;
use warnings;

use AnyEvent;
use AnyEvent::HTTP;

my($url, $simultaneous_req) = @ARGV;

$url // die "usage: $0 URL [NUM_OF_SIMULTANEOUS_REQUESTS]\n";
$simultaneous_req //= 5;

my $cv = AE::cv;

my $cur_num_req = 0;
my $num = 0;
sub relaunch
{
    while ($cur_num_req < $simultaneous_req)
    {
        my $local_num = $num++;
        $cur_num_req++;

        http_get("$url/$num", sub
                 {
                     my(undef, $hdr) = @_;
                     if ($hdr->{Status} =~ /^2/)
                     {
                         say "$local_num OK";
                         $cur_num_req--;
                         AnyEvent::postpone { relaunch() };
                     }
                     else
                     {
                         # In case of HTTP errors, we stop immediately
                         say "Status failed: $hdr->{Status}";
                         $cv->send;
                     }
                 });
    }
}

relaunch();

$cv->recv;

By default, it will send 5 simultaneous HTTP requests. Each request will contain its ordering number suffixed in the URL to follow it easily on the server side.

~> ./client.pl http://127.0.0.1:3000/test
0 OK
1 OK
2 OK
3 OK
...and so on...

All works fine until you hit ^C. When you do it on the client side, the server will display the famous exceptions EV caught:

...
[Fri Mar 29 10:32:02 2013] [debug] GET /test/56 (Mozilla/5.0 (compatible; U; AnyEvent-HTTP/2.15; +http://software.schmorp.de/pkg/AnyEvent)).
[Fri Mar 29 10:32:02 2013] [debug] Routing to a callback.
[Fri Mar 29 10:32:02 2013] [debug] 200 OK (0.199531s, 5.012/s).
EV: error in callback (ignoring): Can't call method "res" on an undefined value at /home/max/perl5/lib/perl5/Mojolicious/Controller.pm line 275.
EV: error in callback (ignoring): Can't call method "res" on an undefined value at /home/max/perl5/lib/perl5/Mojolicious/Controller.pm line 275.
EV: error in callback (ignoring): Can't call method "res" on an undefined value at /home/max/perl5/lib/perl5/Mojolicious/Controller.pm line 275.

Now the reason of all this noise.

On the server side, in get '/test/:num' sub, $self is the unique Mojolicious::Controller instance. It contains the instance of the HTTP transaction Mojo::Transaction::HTTP we are handling, in its tx attribute.

This transaction has been weakened just before calling us by Mojolicious in its handler method.

When we stop the client, all the transactions that are waiting for the render call are destroyed by the server logic: the connection to the client is lost, then the transaction has to be destroyed, in a logical way.

So when our timer calls its callback, we call naturally render_json that will call render that will call res (which contains the Mojo::Message::Response to be filled) that will call tx (because it is the Mojo::Transaction::HTTP instance that contains the response). But tx has been undefined when the transaction has been destroyed, so we get an exception: Can't call method "res" on an undefined value.

So what can we do?

Just change our server timer callback to give up and do not render anything when the transaction does not exist any more:

get '/test/:num' => sub
{
    my $self = shift;

    $self->render_later;

    my $timer;
    $timer = AE::timer(0.2, 0, sub
                       {
                           undef $timer;

                           # The transaction has been destroyed, no
                           # need to render anything any more
                           $self->tx // return;

                           $self->render(json => { value => 1234 });
                       });
};

That’s all.

Leave a Reply