3
votes

I'm very new to Plack, Twiggy and AnyEvent and having a problem.

I have client facing application servers that fire requests to a backend game server. The game servers do a couple of things.
1. they do things to objects when a request arrives from an application server (for example, move, buy, delete, or launche objects).
2. they regularly update the positions of all 'movable' objects (update lat/lng).

To try and achieve this I've setup a very basic psgi file which does the following: AnyEvent is used to update the lat/lng of the objects every two seconds (repeatedly).

use AnyEvent;
my $cond = AnyEvent->condvar;   
my $w; $w = AnyEvent->timer ('after' => 0, 'interval' => 2, cb => sub {
    while ( my ($key, $o) = each %{$self->{'objhash'}})
    {
        $self->position->update($o) if($o->{type} eq 'movable');
    }
});
$cond->recv;

my $app = sub {
my $env = shift;
    my $resp = $b->handler_rpc($env); #deal with application server requests (e.g. delete object)
    return $resp;
};

handler_rpc simply deals requests that are kicked off from the application servers to do something to an object. For completeness the code on the App servers is as follows;

sub request_action
{
my $self   = shift;
my $vals   = shift;
my $y = $self->y;

use AnyEvent;
use AnyEvent::RPC;
use Data::SplitSerializer;
use Data::Dump qw(pp);

my $flattened = Data::SplitSerializer->new( path_style => 'DZIL' )->serialize($vals);
pp $flattened;

my $rpc = AnyEvent::RPC->new(
    host => '192.168.56.20',
    port => 5000,
    base => '/',
    type => 'REST', # or type => '+AnyEvent::RPC::Enc::REST',
    #debug => 10,
    timeout => 5,
);

my $cv = AE::cv;
$cv->begin; 

my $response;
$rpc->req(
    method => 'POST',
    call  => [ method => qw(handler_rpc)], #Not used atm
    query => $flattened,
    cb    => sub { # ( response, code, error )
        if ($response = shift) {
            warn "we had this response " . Dumper($response);
            $response = $response->{response};
            $cv->end;
        } else {
            my ($code,$err) = @_;
            $response = { 'status' => 'UNKNOWN_ERROR',
                          'code'   => $code,
                          'error'  => $err };
            warn "we have an error $code, $err";
        }
    },
);
$cv->recv;
return  $response;
}

And on the game server side the code looks like this

sub handler_rpc {
my $self = shift;
my $vals = shift;

use Plack::Request;
my $req = Plack::Request->new($vals);

use Data::SplitSerializer;
use Data::Dump qw(pp);
use URI;
use URI::QueryParam;        

my $params = $req->parameters;

my $deep = Data::SplitSerializer->new( path_style => 'DZIL' )->deserialize($req->parameters);
pp $deep;   

#process the command
my $resp;
if( my $func = $self->can($deep->{cmd}) ) 
{
    warn "Calling method of " . $deep->{cmd};
    $resp = &$func( $self, $deep->{args} );
}                   

my $xs = new XML::Simple;
my $xml = $xs->XMLout($resp,
                  NoAttr => 1,
                  RootName=>'response', #TODO: put some key in here somewhere for security.
                 ); 
return [
    200, 
    ['Content-Type' => 'text/plain'],
    [ $xml ],
];  
}

The parts work individually. By that I mean, if I just worry about getting and processing RPC requests to the game server it all works fine.

If I just use AnyEvent to regularly update the object locations that works fine.

The problem occurs when I want to do both at the same time on the same set of objects.

It fails because either the $cond->recv blocks and we don't actually get to the $app so Twiggy doesn't process requests or if I move the $cond line, we start the app and twiggy waits for requests from the app servers but the AnyEvent events don't occur (or they occur once).

Twiggy is supposed to be non-blocking (which I think is useful here) and built with AnyEvent in mind, so I'm assuming there is a way to do what I want, but having looked around for a while, I'm not getting far.

Is there a way to do this using Twiggy, Plack and AnyEvent or am I heading the wrong direction? If this is somewhat right how do I fix it?
Is there another way I could approach this altogether that would be better?

As I said, I'm quite new to this so sorry if I'm not provided essential information. Please do ask and I'll update with anything I've missed.

Thanks.

1

1 Answers

1
votes

I'd like to point out that it is probably best to separate some "background" processing from handling HTTP requests. It would be best if HTTP server/application did not have to worry about having some action run every two seconds. For example this could happen in separate processes, but would require keeping game state in some kind of DB (instead of just having a hash in memory - as I understand your code now).

Having that in mind let's move to your specific issue. You can't have $w->recv before your app, because that will block. If you just remove it then your AnyEvent watcher will immediately go out of scope when app is run and no timer events will fire. Solution is not to call $w->recv at all (there is no need to block - event loop will be called by Twiggy anyway) but insstead just make sure that something is referencing $w to keep it from being garbage collected.

Only thing that persists when your psgi file is run is reference to an application. If that application closes over your $w then it will still be referenced and timer will work.

Simplest solution to do that is to have $w appear inside $app. Just like that:

$app = sub {
    my $env = shift;
    $w;
    ... rest of app code ...
};