Communication using HTTP between client and server is a simple problem of halted computation.
A client computes a request, transmits and halts, waiting for a server response. On receipt, the server computes a response, transmits and halts, waiting for the next client request.
This much is well known.
Racket's magnificent stateful Web server does three things on the server side:
- it reifies a Racket continuation, capturing where the server computation has halted.
- it externalizes the continuation, creating a URL-representation that uniquely maps to the Racket continuation
- it disseminates the externalized continuation to interested clients, typically via HTTP response, but alternately via SMTP or any other protocol.
Then, it waits.
Later, when presented with an externalized continuation, a quick inverse mapping occurs, the underlying Racket continuation is invoked, and the server processes the new client request.
Rinse and repeat.
The problem with this approach is twofold
- the reified Racket continuations live in server memory. And there's no safe way to garbage collect, as the continuations could be invoked at any time. There are strategies to reclaim memory, but some load level will noticeably decrease the performance of your application. And its not possible to figure out what that load level is prior to finishing your application. This is a problem.
- Again, the reified Racket continuations live in server memory and cannot be moved. So there's no way to scale an application to more than one server. It's a necessarily one machine system. This makes problem #1 worse.
Racket's yet more magnificent stateless Web server does exactly the same three things:
- to reify, it rewrites the entire call stack into a format known as A-Normal Form (ANF).
- to externalize, the ANF'd stack is encoded for transmission over HTTP.
- and then it's sent over to the client (dissemination).
Later, when presented with encoded stack, the stateless server performs an inverse transform to reconstruct the call stack, at which point the server keeps going.
So we've lost the invocation step and substituted a reconstruction.
But in exchange, we've eliminated continuations from server memory, and solved both enumerated problems above. Neat trick.
I provide a few lessons learned for the archives for the next person to attempt porting #lang racket to #lang web-server code.
First, the predicate serializable? from racket/serialize is invaluable. The #lang web-server code will not transform if there are non-serializable constructs in the dynamic extent of the invocation of send/suspend, such as a local binding or argument.
Second, invocations of native continuations reified with call/cc frequently throw errors related to continuation prompts, such as “attempt to cross a continuation barrier” or “no corresponding prompt tag in continuation”. In all cases, I was able to remedy the situation by enclosing the invocation in call-with-continuation-prompt. This may be an error in the system, but it is unclear at this time.
Third, the transformation does not allow parameters or dynamic-wind, because the internal data-structures representing them are not serializable, but continuation-marks can be used to reimplement the piece of the functionality you need.
Finally, thank you to the Racket team. I think the stateless Web language is important technology and must have required an enormous amount of work to implement.
Anecdotally, application speed seems at or better than the stateful code.
(This post was written by Zack Galler with minor edits before posting by Jay McCarthy.)