A recent commit to SicilianButtercup (the current development branch toward the next release of Perl Catalyst) added the basics needed to allow one to work with an event loop, such as to permit one to use a nonblocking server like Twiggy. The goal of this change is so that you an write non blocking / event code within Catalyst so that you can better support mass conncurrency and long lived connections, or perhaps even advanced web technologies such as Web Sockets. How does this change work?
PSGI enabled Catalyst already returns a coderef using the delayed response form. So the basics were always in place. Here's the current code for this:
sub build_psgi_app { my ($self, $app, @args) = @_; return sub { my ($env) = @_; return sub { my ($respond) = @_; confess("Did not get a response callback for writer, cannot continiue") unless $respond; $app->handle_request(env => $env, response_cb => $respond); }; }; }
Remember, when using the delayed response form of a PSGI coderef, you are returning a coderef instead of the classic tuple [STATUS, \@HEADERS, \@BODY]. This coderef is invoked by your PSGI specified application container per request with a single argument "$respond" which itself is a coderef. "$respond" expects to get the tuple [STATUS, \@HEADERS, \@BODY] or [STATUS, \@HEADERS]. If the second form it returns a $writer object that has two methods "write" and "close". The request is not considered final UNTIL $write->close is called.
For the current stable Catalyst, we pull the expected body from $c->request->body which is either a fully formed scalar OR a filehandle. However in both cases when we finalize the body (as we do when completing the request) we return it all in one go (or as chunks if there is a filehandle, but we still block until the full filehandle is read) and then call $writer->close. This is why you cannot use an event loop to handle the writing for you since we forcibly close the writer before the loop can do anything with it.
In SicilianButtercup this have been changed. We no longer close the $writer object, but instead associate a DEMOLISH method on Catalyst::Response that does $writer->close for us when the request goes out of scope. So this means if you close over $request in say a callback coderef and link that callback to an event loop, the $writer object stays open until you either manually close it yourself in the callback, or allow the callback itself to go out of scope.
What it looks like right now is something like this (using AnyEvent):
sub anyevent :Local :Args(0) { my ($self, $c) = @_; my $res = $c->res; my $cb = sub { my $message = shift; $res->write("Finishing: $message\n"); $res->_writer->close; }; my $watcher; $watcher = AnyEvent->timer( after => 5, cb => sub { $cb->(scalar localtime); undef $watcher; # cancel circular-ref }); }
So you can see we close over $res which keeps it in scope and allows us to use an event loop to handle the response in a deferred and nonblocking manner (assuming you are running this under an event loop, such as you get with Twiggy).
The problem we have is that for cases when $c is closed over, for good or ill, the response never gets closed since the $response object doesn't go out of scope (it is an attribute on $c of course) and this means the application get broken. For the most part closing ovr $c is bad, since it usually means you have a memory leak. However there are some semi legitimate reasons to close over $c. For example in Test::Catalyst there is an exported testing method "ctx_request" which closes over $c so that it can return it for testing purposes. I suspect there might be other similar cases.
We could probably fix Test::Catalyst, but there is some concern that the overall approach to the way we close the $writer object by allowing $response to go out of scope is a bit off. Thoughts I have to solve this include:
- Adding some power to Catalyst that would make it easier to get the context object in requests when you want them for testing or for other reasons. This would remove the desire to close over the context and let us follow the current path
- Find a different way to close $writer that does not rely on having the context / response object go out of scope.
Thoughts from the community? I would consider this a blocking issue preventing us from sending event loop enabled Catalyst to CPAN, so if you are one of the people that would like Catalyst to have these features lets group up and find a path forward.
Comments
You can follow this conversation by subscribing to the comment feed for this post.