Asynchronous Interceptors in Pedestal

  • clojure
  • pedestal

One of the most unique features of the Pedestal library is its ability to serve responses asynchronously. In services with any sizeable measure of load, this is useful because it allows long-running requests to kick-off work, then immediately return control of the thread back to the web-server.

Since many web-servers handle only one request per thread, blocking or waiting for some external resource or call can unnecessarily limit your server’s throughput. The benefit of asynchronous request handling is that those costly operations can now occur in the background, without tying up your server’s limited thread pool. When the operation is completed, request handling resumes and a response is delivered to the patiently waiting client.

In Pedestal, async has been baked-in from the start, but starting with version 0.3.0, asynchrony is implemented via core.async channels. Unfortunately documentation on this feature is lacking, so I wanted to throw together a quick tutorial on the matter.

To start, generate a Pedestal service using the pedestal-micro template:

$ lein new pedestal-micro going-async
Generating fresh 'lein new' pedestal-micro project.

Inside src/going_async.clj you’ll find the basis for a Pedestal service, including a handler function hello-world. We’re going to turn hello-world into a fully asynchronous interceptor that performs some work in the background, before returning a response. (To be honest, we’re just going to chill for two seconds before responding…)

While Pedestal interprets simple functions like hello-world as Ring handlers, to go async Pedestal requires a proper interceptor. (Hopefully this will change to be more lenient at some point in the future. Just think if returning a channel signalled “I’m going async!”).

To define an async interceptor, we need to bring two namespaces into going-async: io.pedestal.interceptor and clojure.core.async. More specifically, we need the defbefore interceptor function, and a handful of core.async functions (go, <!, >!, chan, and timeout):

(ns going-async
  (:require ;; ...
            [io.pedestal.interceptor :refer [defbefore]]
            [clojure.core.async :refer [go <! >! chan timeout]])

With that, we have all of the pieces to take hello-world to the next level. In all of its glory:

(defbefore hello-world [context] ;; 1, 2
  (let [ch (chan 1)]             ;; 3
    (go                          ;; 4
      (<! (timeout 2000))        ;; 5
      (>! ch (assoc context      ;; 6
                    (ring-resp/response "Hello World!"))))
    ch))                         ;; 7

There’s a bit going on here, so I’ll break it down a little:

  1. Rather than a simple handler, we’re defining an interceptor that is invoked on incoming requests (as opposed to outbound responses).
  2. Raw, low-level interceptors work in terms of context maps, rather than individual :request or :response values.
  3. To start our function, we create and name a core.async channel that we will respond on later. It’s crucial this channel is the final return value of hello-world.
  4. The go macro is what takes us async, while giving the appearance of synchronous work (one of the big value props of core.async, in general).
  5. You can imagine doing some more serious work here, but for illustration we can just wait two seconds by “taking” from a timeout channel.
  6. Most importantly, we furnish our channel with an updated context that includes our response. Unlike simple handlers, interceptors modify requests/responses by modifying the original context map.
  7. Finally, we make sure to return our output channel and not the implicit channel go creates.

You can now run the server and receive a response in a slightly less than timely fashion:

$ lein run
Compiling going-async
INFO  o.e.jetty.server.ServerConnector - Started ServerConnector@83298d7{HTTP/1.1}{localhost:8080}
INFO  org.eclipse.jetty.server.Server - Started @3132ms

# In another tab...
$ curl -i localhost:8080/
# Wait a bit...
Hello World!

And that’s it. That’s the tutorial. Of course, there’s plenty more you can do with async than just keeping your users waiting: you could fetch the results of an expensive query before proceeding with response generation, enqueue work to happen later, or even ingest data from other channels.

What would you use asynchrony in request-handling for?

Like this post? Subscribe to my newsletter.

Get fresh content on Clojure, Architecture and Software Development, each and every week.