P6SGI: Perl 6 Web Service Gateway Interface
So, I have been meaning to start a Perl 6 blog for a couple of months. At that point, though, this site was having issues and I have this perverse desire to write blog software every time I think about blogging and so things got put off for a bit. I am now starting this here and I want to get write off and start with what I think is my most important Perl 6 contribution thus far and one I want to get your feedback on: P6SGI!
For those that need instant gratification, here is a P6SGI application:
# Perl 6
sub app(%env) {
(200, [ 'Content-Type' => 'text/plain' ], [ 'Hello World!' ])
}
That looks quite a bit like its PSGI cousin. That's not the real power of P6SGI, though. For that, you will have to bear with me for a short bit.
I am a web application developer by day and a fan (mostly) of PSGI, the Perl Web Service Gateway Interface. PSGI is a standard that defines something akin to CGI, but is modernized and implementation agnostic. It was developed by Tatsuhiko Miyagawa after seeing the success of similar standards for Python (WSGI) and Ruby (Rack). The nice thing about PSGI is that it requires nothing but the Perl language itself to build a web application, which means it is really simple to use. The bad thing, though, is that when you need to use the advanced PSGI features, you can end up with a callback that calls a callback that calls a callback: it is hard to read and not ideal. That is not so much a weakness of PSGI, but a weakness of what is built in to Perl.
While I have been learning Perl 6 over the past few months, I have learned that Perl 6 has a number of very useful, built-in types that could actually fix these problems. Looking at early app server implementations, I found that each had implemented support for PSGI-style apps like the one above, but none had delved into the deferred or streaming aspects of the standard. I decided, I wanted to see what could be done with this and began experimenting.
Let us start with the simplest of these, the deferred PSGI application:
# Perl 5
sub app {
my $env = shift;
return sub {
my $res = shift;
my $content = some_long_running_process($env);
$res->([ 200, [ 'Content-Type' => 'text/plain' ], [ $content ]);
}
}
What is going on? If your application returns a code reference, then the server is supposed to call that reference and supply to it another callback that can be called by your application when it has finished running some long running task.
This is verbose and confusing. I did not want to use a callback that calls another callback to implement this in Perl 6. Fortunately, Perl 6 has a tool that is perfect and exactly suited to solving this problem, a Promise:
# Perl 6
sub app(%env) {
start {
my $content = some-long-running-process(%env);
(200, [ 'Content-Type' => 'text/plain' ], [ $content ])
};
}
This is the equivalent to the above in P6SGI, but is much easier to read. What is going on? First, start is a routine that starts an asynchronous process, this is kind of like a fork in Perl 5. Perl 6 is responsible to run that block and it returns a Promise object. The server can then wait for the Promise to be kept or broken. Perl 6 FTW.
That takes care of deferred responses, but what about streaming? Often, you have a large file you need to return or one that returns in bits over a long period of time. For this, you want a streaming response.
In PSGI, a streaming response looks pretty similar to a deferred response, like this:
# Perl 5
sub app {
my $env = shift;
return sub {
my $res = shift;
my $out = $res->([ 200, [ 'Content-Type' => 'text/plain' ] ]);
while (my $content = more_data($env)) {
$out->write($content);
}
$out->close;
}
}
Again, we have a callback that calls a callback, but if we call that callback with only the status and header bits, we get back a writer object, which is kind of like another callback. We can then use the write
and close
methods on that writer to send our content back. This works, but again requires a complicated bit of reasoning to understand the callback-callback-callback parts.
Perl 6 comes to our rescue again with a built-in type that seems tailor made to fix this problem, Channels:
# Perl 6
sub app(%env) {
my $stream = Channel.new;
start {
while my $content = more_data(%env) {
$stream.send($content);
}
$stream.close;
};
(200, [ 'Content-Type' => 'text/plain' ], $stream)
}
A Channel is an object capable of encapsulating an asynchronous stream of data to a single recipient. If the content return is a Channel object, the server can receive data from the Channel as it arrives and send it on. There are no callbacks, just clean reactive code.
If you need to defer and stream, you can do that too by combining a Promise with a result returning a Channel, which is probably what I will normally do since it is shorter and cleaner to write:
# Perl 6
sub app(%env) {
start {
my Channel $stream = long-process-fills-a-channel(%env);
(200, [ 'Content-Type' => 'text/plain' ], $stream);
}
}
Now, you have a Promise to stream through a Channel.
Another smaller difference in P6SGI from PSGI is the encoding of data. Most developers do not like dealing with encoding at all. When applications interact with other applications over a socket, though, they may not have that luxury. Fortunately, Perl 6 simplifies this by making a strong distinction between encoded and decoded data.
All of the P6SGI application examples here deal with strings and send them to the server without encoding them into blobs first. This puts the burden of encoding on the server. P6SGI specifies how servers should do this. However, applications that want to make sure this is always done precisely can pass the server blobs instead of strings:
# Perl 6
sub app(%env) {
(200, [ 'Content-Type' => 'text/plain; charset=UTF-8' ], [
"Hello World".encode('UTF-8')
]);
}
Anyway, I have written all of this up on github in full detail and hope that you will give feedback. Nothing is set in stone at this point as I have not even yet gotten around to writing the reference implementation, but I am very happy with the natural improvements Perl 6 grants to PSGI through Promises, Channels, and better encoding tools.
Please, let me know what you think and, as always, patches welcome.
Cheers.
This post sparked some very interesting discussion in #perl6 which should be considered before implementing this proposed API:
http://irclog.perlgeek.de/perl6/2015-08-03#i_10994607
In summary, so far, the comments are that some would like a slightly higher level API for this, which I believe can be resolved in the reference implementation, similar to what Plack does. It has been suggested that apps always be written in terms of Promises, which will break the existing pre-P6SGI implementations, but that's likely to be just fine. And no one has yet come up with a good name for the PSGI reference implementation yet. It is named, but the name sucks.