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).
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:
Now, specifically:
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:
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.
With the async approach you get to send the data to the client when its ready, instead of blocking a thread the whole time its being prepared.
For http-kit, you should be using an async handler described in the documentation. After delegating the request to an async handler in a proper way, you can implement it however you like using core.async or something else.
Async Handler documentation is here: http://http-kit.org/server.html#channel
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With