Rack is an interface for Ruby components accepting and responding to HTTP requests synchronously. Though there is not an official Rack spec for asynchronous responses, Rack servers such as Rainbows! and Thin provide an asynchronous interface for responses that may happen at an unknown time in the future, such as in a web chat room.
The synchronous Rack interface is defined in its SPEC file. The essential part is an object defined by the application which is given to a Rack server. This object must implement call(env)
, where env
contains information about the HTTP request. The method returns and array of [status, headers, body]
which the Rack server sends along to the client.
class SynchronousApp
def call(env)
[200, {'Content-Type' => 'text/plain'}, ['Hello, World!']]
end
end
With this synchronous interface, the entire body must be prepared immediately, or be otherwise quickly available, for example by reading from a file. A response that must be waited upon will tie up the process or thread executing the HTTP request. In a multi-process Rack server such as Unicorn, this will block the entire worker process, making in unavailable to server other requests.
Asynchronous responses
Some Rack servers provide an alternate interface that allows the request to be suspended, unblocking the worker. At some later time, the request may be resumed, and the response sent to the client.
While there is not yet an async interface in the Rack specification, several Rack servers have implemented James Tucker’s async scheme. Rather than returning [status,
headers, body]
, the app returns a status of -1
, or throws the symbol :async
. The server provides env['async.callback']
which the app saves and later calls with the usual [status, headers, body]
to send the response.
Note: returning a status of -1
is illegal as far as Rack::Lint
is concerned. throw :async
is not flagged as an error.
class AsyncApp
def call(env)
Thread.new do
sleep 5 # simulate waiting for some event
= [200, {'Content-Type' => 'text/plain'}, ['Hello, World!']]
response ['async.callback'].call response
envend
[-1, {}, []] # or throw :async
end
end
In the example above, the request is suspended, nothing is sent back to the client, the connection remains open, and the client waits for a response. The app returns the special status, and the worker process is able to handle more HTTP requests (i.e. it is not blocked). Later, inside the thread, the full response is prepared and sent to the client.
Deferred or streaming response bodies
The example shows a one-shot request, wait, response cycle. But it is possible to have multiple wait, response segments to allow the headers to be sent to the client immediately, or the body to trickle in slowly without blocking the worker process. Async Rack servers, when env['async.callback']
is called, send the status and headers to the client and then begin iterating through each part of the body with #each
. After the last body part the server must decide if the connection to the client should be closed (entire body has been provided) or if it should remain open (body parts will be provided later). The details of this decision are implementation-specific. For now, assume the connection is not closed. To send additional body parts, env['async.callback']
may not be called a second time since the status code and headers have already been sent to the client and can not be changed. The app takes advantage of the server’s iteration through the body with #each
: the server calls body.each(&block)
, and the trick is to save &block
for later use. This turns the iteration inside-out: rather than the sever iterating through a body, the app takes control to send each part of the body itself.
class DeferredBody
def each(&block)
# normally we'd yield each part of the body, but since
# it isn't available yet, we save &block for later
@server_block = &block
end
def send(data)
# calling the saved &block has the same effect as
# if we had yielded to it
@server_block.call data
end
end
class AsyncApp
def call(env)
Thread.new do
sleep 5 # simulate waiting for some event
= DeferredBody.new
body = [200, {'Content-Type' => 'text/plain'}, body]
response ['async.callback'].call response
env
# at this point, the server may send the status and headers,
# but the body was empty
.send 'Hello, '
bodysleep 5
.send 'World'
bodyend
[-1, {}, []] # or throw :async
end
end
Note that the above won’t quite work because we haven’t signaled to the server that the body will be deferred and streamed in part by part.
Signaling deferred body with Thin and Rainbows::EventMachine
Thin is a single-threaded EventMachine based Rack server as is Rainbows! when used with Rainbows::EventMachine. The first async example above won’t actually work within EventMachine. The call to env['async.callback']
must be done from the reactor thread (for reasons I don’t quite understand):
class AsyncApp
def call(env)
Thread.new do
sleep 5 # simulate waiting for some event
= [200, {'Content-Type' => 'text/plain'}, ['Hello, World!']]
response EM::next_tick { env['async.callback'].call response }
end
[-1, {}, []] # or throw :async
end
end
In order to properly defer the body so it may be sent part by part rather than all at once, these servers expect the body
object to respond to #callback
and #errback
which the server calls to register handlers that, when called, will terminate the request, closing the connection (unless it is persistent/pipelining, but irrelevant details). It is up to the app to invoke the callbacks after the entire body has been sent or after an error has occurred. The callback interface is that of EventMachine::Deferrable
which may simply be included in deferred body classes. The app may then call body.succeed
after all the body has been sent. For example, from Thin’s examples/async_app.ru
:
class DeferrableBody
include EventMachine::Deferrable
def call(body)
.each do |chunk|
body@body_callback.call(chunk)
end
end
def each &blk
@body_callback = blk
end
end
class AsyncApp
def call(env)
= DeferrableBody.new
body EventMachine::next_tick {
['async.callback'].call [200, {'Content-Type' => 'text/plain'}, body]
env}
EventMachine::add_timer(1) {
.call ["Cheers then!"]
body.succeed # calls Thin's callback which closes the request
body}
end
end
Mizuno
Mizuno is a Rack server for JRuby. It uses the embeddable Jetty Java servlet container. Mizuno takes a slightly different approach to env['async.callback']
than Thin and Rainbows!. Multiple calls to env['async.callback']
are allowed. The status and headers of the second and subsequent calls are ignored. A call with both empty headers and an empty body signals the end of the response.
class AsyncApp
def call(env)
Thread.new do
sleep 5 # simulate waiting for some event
# first call send the headers
['async.callback'].call [200, {'Content-Type' => 'text/plain'}, []]
env
# status of second call is ignored; only the body is sent
['async.callback'].call [404, {}, ['Hello, ']]
env
# additional body part is sent
['async.callback'].call [0, {}, ['World!']]
env
# empty headers and body signal end of response
['async.callback'].call [0, {}, []]
envend
throw :async
end
end