Can I make a fully non-blocking backend application with http-kit and core.async?
Asked Answered
S

1

37

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:

  1. Have a few super-fast OS threads handle all the CPU-intensive computing; these must never be waiting.
  2. Have a lot of "weak threads" handle the IO (database calls, web-service calls, sleeping, etc.); these are meant mostly to be waiting.
  3. 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.

Sampson answered 27/7, 2014 at 10:27 Comment(3)
how do you solved it at the end??...I've the same requirement, I wish build a full async app in clojure..but ring is orthogonal to the async patterns, pedestal seems promising but the documentation is poor and vertx is not idiomatic for clojure developers, also is not ring compatible, I try this github.com/ninjudd/ring-async but seems to be just an experiment...I'm curious about what technology do you choose at the end, thanks!..Jahvist
I've made some progress on that issue (however, I have not implemented such an app). The first thing to check is that all (or most) of the DB drivers, IO clients etc. are asynchronous themselves. Then you can use a library like core.async or manifold for the plumbing. As for HTTP routing / handling, you can create a Ring middleware that adapts to httpkit by adding a response channel to the request map, and adapt Ring middleware to be asynchronous. It'll be harder, you should check that you do have such performance requirements.Sampson
Update: there are now libraries for that, e.g Yada github.com/juxt/yadaSampson
E
5

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

Euphonic answered 28/7, 2014 at 4:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.