August 2015 Archives

P6SGI: Revising and Reviewing

After my previous post I received quite a bit of really good, constructive feedback. Thank you all who responded to my request for comments! I would say the gist of the feedback was this:

  • Make the interface simpler for middleware and to a lesser extent, servers.
  • Consider always requiring the Promise interface.
  • Consider using Supply instead of Channel as it is non-blocking and likely to perform better.

After wrangling and playing around with various things and learning more about Perl 6 than I knew before, I have come up with what I think will be a stable interface that satisfies all of these suggestions.

First, since Promises are easy to do, I agree, let's just always require them. This means that the basic Hello World app now looks like this:

sub app(%env) {
    start {
        200, [ Content-Type => 'text/plain' ], [ 'Hello World' ]
    };
}

After much research and playing around with the Supply type, I have decided that it is generally superior to Channel. Therefore, we will use Supply to generate the message body, not a Channel. The main problem with Channel is that it will block the server on receive. You can get around this, but it is either a headache or requires a dedicated thread. I originally chose Channel over Supply, though, because Supply is flexible enough to cause trouble if the application developer is sloppy. The benefits, however, far outweigh the risks. Besides, since when has Perl ever shied away from letting sloppy developers shoot themselves in the foot?

Finally, I have unified the interface so that middleware and servers can process the application output without much effort, but in a way that allows the application flexibility to respond in just about any way it pleases. This is possible because of the way Perl 6 coercions work. Let's examine the definition of an application and then explore a couple examples.

  • The application must return a Promise or it must return an object that coerces into a Promise. In Perl, an object coerces to another if it provides a method with the name of the coercion to perform. Therefore, an application could return a Supply as it can be turned into a Promise automatically.

  • The Promise must be kept with a Capture that contains three positional arguments, or it must provide something that coerces into such a Capture, such as a 3-element List or Array.

  • The first argument of the Capture is the status code, which must be an Int or (you guessed it) something that coerces into an Int. That means it could be Enum or some other kind of object representing each of the possible HTTP status codes.

  • The second argument of the Capture is the list of headers. As before, this is a list of Pairs mapping header names to header values. Again, it could be any object that coerces to such a list.

  • The third argument of the Capture is a Supply that emits Str and Blob values, or any object that coerces into such a Supply. Both List and Array coerce into such a Supply safely, so we can just return an Array or List in simple cases.

This means that we have the simplicity for this to work:

sub app(%env) {
    start {
        my $n = %env.Int;
        my $acc = 1.FatRat;

        200,
        [ Content-Type => 'text/plain' ],
        [ do for 1..$n { "{$acc *= $_}\n" } ]
    };
}

and the flexibility to make something that outputs it's first byte faster like this:

sub app(%env) {
    start {
        my $n = %env.Int;
        200,
        [ Content-Type => 'text/plain' ],
        Supply.on-demand(-> $content {
            my $acc = 1.FatRat;
            for 1..$n {
                $content.emit("{$acc *= $_}\n");
            }
            $content.done;
        });
    };
}

and still have middleware that processes each like this:

sub add-one(%env) {
    callsame().then(-> $p {
        my ($s, @h, Supply(Any) $body) = $p.result;
        $s, @h, $body.map(* + 1);
    });
}
&app.wrap(&add-one);

I think that's pretty awesome.

Cheers.

Edit: The folks in #perl6 corrected a misunderstanding I had about the start { } deprecation. I have adjusted the code here accordingly.

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.

About Sterling Hanenkamp

user-pic Perl hacker by day, Perl 6 module contributor by night.