I'm wondering if it's possible to put together a fully non-blocking Clojure backend web application with http-kit.
(Actually any Ring-compatible http server would be fine by me; I'm mentioning http-kit because it claims to have an event-driven, non-blocking model).
EDIT: TL;DR
This question is a symptom of some misconceptions I had about the nature of non-blocking/asynchronous/event-driven systems. In case you're in the same place as I was, here are some clarifications.
Making an event-driven system with the performance benefits of it being non-blocking (like in Node.js) is possible only if all (say, most) of your IO is handled in a non-blocking way from the ground up. This means that all your DB drivers, HTTP servers and clients, Web services etc. have to offer an asynchronous interface in the first place. In particular:
- if your database driver offers a synchronous interface, there's no way to make it non-blocking. (Your thread is blocked, no way to retrieve it). If you want non blocking, you need to use something else.
- High-level coordination utilities like core.async cannot make a system non-blocking. They can help you manage non-blocking code, but don't enable it.
- If your IO drivers are synchronous, you can use core.async to have the design benefits of asynchrony, but you won't get the performance benefits of it. Your threads will still be wasting time waiting for each response.
Now, specifically:
- http-kit as an HTTP server offers a non-blocking, asynchronous interface. See below.
- However, many Ring middlewares, since they're essentially synchronous, won't be compatible with this approach. Basically, any Ring middleware that updates the returned response will not be useable.
If I got it right (and I'm not an expert, so please tell me if I'm working on wrong assumptions), the principles of such a non-blocking model for a web application are the following:
- Have a few super-fast OS threads handle all the CPU-intensive computing; these must never be waiting.
- Have a lot of "weak threads" handle the IO (database calls, web-service calls, sleeping, etc.); these are meant mostly to be waiting.
- This is beneficial because the waiting time spent on the handling of a request is typically 2 (disk access) to 5 (web services calls) orders of magnitude higher than the computing time.
From what I have seen, this model is supported by default on the Play Framework (Scala) and Node.js (JavaScript) platforms, with promise-based utilities for managing asynchrony programmatically.
Let's try to do this in a Ring-based clojure app, with Compojure routing. I have a route that constructs the response by calling the my-handle
function:
(defroutes my-routes
(GET "/my/url" req (my-handle req))
)
(def my-app (noir.util.middleware/app-handler [my-routes]))
(defn start-my-server! []
(http-kit/run-server my-app))
It seems the commonly accepted way of managing asynchrony in Clojure applications is CSP-based, with the use of the core.async library, with which I'm totally fine. So if I wanted to embrace the non-blocking principles listed above, I'd implement my-handle
this way :
(require '[clojure.core.async :as a])
(defn my-handle [req]
(a/<!!
(a/go ; `go` makes channels calls asynchronous, so I'm not really waiting here
(let [my-db-resource (a/thread (fetch-my-db-resource)) ; `thread` will delegate the waiting to "weaker" threads
my-web-resource (a/thread (fetch-my-web-resource))]
(construct-my-response (a/<! my-db-resource)
(a/<! my-web-resource)))
)))
The CPU-intensive construct-my-response
task is performed in a go
-block whereas the waiting for external resources is done in thread
-blocks, as suggested by Tim Baldridge in this video on core.async (38'55'')
But that is not enough to make my application non-blocking. Whatever thread goes through my route and will call the my-handle
function, will be waiting for the response to be constructed, right?
Would it be beneficial (as I believe) to make this HTTP handling non-blocking as well, if so how can I achieve it?
EDIT
As codemomentum pointed out, the missing ingredient for a non-blocking handling of the request is to use http-kit channels. In conjunction with core.async, the above code would become something like this :
(defn my-handle! [req]
(http-kit/with-channel req channel
(a/go
(let [my-db-resource (a/thread (fetch-my-db-resource))
my-web-resource (a/thread (fetch-my-web-resource))
response (construct-my-response (a/<! my-db-resource)
(a/<! my-web-resource))]
(send! channel response)
(close channel))
)))
This lets you embrace an asynchronous model indeed.
The problem with this is that it is pretty much incompatible with Ring middleware. A Ring middleware uses a function call for obtaining the response, which makes it essentially synchronous. More generally speaking, it seems event-driven handling is not compatible with a pure functional programming interface, because triggering events means having side-effects.
I'd be glad to know if there is a Clojure library that addresses this.