We have a library that uses AnyEvent already. It uses AnyEvent internally and in the end it returns a value (synchronously - not with a callback). Is there any way I can use this library with Mojolicious?
It does something like:
#!/usr/bin/perl
use strict;
use warnings;
use AnyEvent;
use Mojolicious::Lite;
# To the caller, getData is a synchronous sub that returns a value.
# The fact that it uses AnyEvent is an internal implementation detail of
# getData
sub getData {
my $cv = AnyEvent->condvar;
my $w = AnyEvent->timer (after => 5, cb => sub {
# Perform many async operations, represented here by a single timer,
# calculating a final result that is sent:
$cv->send(42);
});
my $result = $cv->recv;
# postProcess($result);
return $result;
}
get '/' => sub {
my ($c) = @_;
$c->render(text => "Data is: " . getData());
};
app->start;
When I run morbo app.pl
and try to get '/'
from two browser tabs at the same time I get this error:
AnyEvent::CondVar: recursive blocking wait attempted at /bla/bla/app.pl line 16.
What I think is going on is that morbo uses EV internally and so when it dispatches to handle the first get '/'
, $cv->recv
ends up getting called, returning to the EV event loop. EV now tries to handle the second get '/'
and $cv->resv
is called again, triggering the error.
I know I can refactor the $cv
out of getData()
to make an asynchronous version, but in reality the real "getData" is called in many places and turning all calls to "getData" into async code is not feasible.
So my question is: Is there any way I can call the exact getData()
above reliably while using morbo
/Mojolicious? I'd like get '/'
to block until it is done.
Edit: The AnyEvent's WHAT TO DO IN A MODULE section explicity says:
Never call ->recv on a condition variable unless you know that the ->send method has been called on it already. This is because it will stall the whole program, and the whole point of using events is to stay interactive.
getData()
above violates that. Now I understand the reason for that part of the AnyEvent documentation :-)
You can avoid this problem by ensuring Mojolicious and AnyEvent do not use the same main loop, either by setting the env var MOJO_REACTOR=Mojo::Reactor::Poll
, or using AnyEvent::Loop before anything else so that AnyEvent uses its pure-perl loop (preferred, since it is not being used as the main loop). Unfortunately there is no way to cause it to use a separately instantiated loop otherwise, such as how Mojo::UserAgent blocking requests function.
Note that commonly, you want multiple loop consumers to share the main loop; this is an odd case, where you want the inner consumer to block.
A more "async" long term solution might be to simply allow the operations to share the main loop, as Mojolicious is designed for non-blocking response operation. You can do this by first ensuring that both do in fact share the EV main loop (such as by setting MOJO_REACTOR=Mojo::Reactor::EV
), then changing your function, or creating a new version of the function, to return a promise that the function will asynchronously populate with the result, and chaining any further functionality that relies on that result off of that promise. Of course your function is more complicated than the single timer you use here but it still may be worth consideration - it will allow the application to continue to serve other requests while waiting on the result of the AnyEvent operation.
sub getData_p {
my $p = Mojo::Promise->new;
my $w; # keep a strong reference to $w for AnyEvent's reasons
$w = AnyEvent->timer(after => 5, cb => sub { $p->done(42); undef $w });
return $p;
}
get '/' => sub {
my ($c) = @_;
my $tx = $c->render_later->tx; # keep strong reference to $tx
getData_p()->then(sub { $c->render(text => "Data is: $_[0]") })
->catch(sub { $c->reply->exception($_[0]); undef $tx });
};