Like Ring, but async.
To use in your Leiningen project, add the following
;; For http-kit support
[async-ring "0.1.0"]
[http-kit "2.1.19"]
;; For jetty support
[async-ring "0.1.0"]
[ring/ring-jetty-adapter "1.3.1"]
Ring is a great foundation for building HTTP servers in Clojure. However, Ring fails to solve many problems that high-performance and transactional HTTP servers must solve:
Async Ring attempts to solve these problems by introducing a core.async based API that is backwards compatible with Ring and popular Ring servers, so that you don't need to rewrite your app to take advantage of these techniques.
Async Ring is 100% compatible with normal Ring. Want to use your Ring handler
in an Async Ring app? Just use sync->async-hander
. What about mounting an
Async Ring handler into a normal Ring add? async->sync-handler
. Maybe you'd
like to use the huge body of existing Ring middleware in your Async Ring app:
try sync->async-middleware
. Or maybe you'd like to use async middleware with
your synchronous Ring app: just use async->sync-middleware
.
Async Ring also includes a complete set of optimized ports of Ring middleware. These ports includes a ported test suite, so you can feel comfortable in the logic being executed.
Beauty is a simple concurrent router that lets you reuse your existing Ring routes
and handlers, be they Clout or Compojure, Hiccup or Selmer. You just need to pass
your existing app to the beauty-router
middleware, and then annotate any handlers
that you want to run concurrently with prioritization.
Async Ring comes with adapters for Jetty 7 and http-kit, so that you don't even
need to change your server code. Just use to-jetty
or to-httpkit
to mount
an async handler onto your existing routing hierarchy.
Async Ring uses a standard pattern in core.async, in which the response channel is tied to the request map. This way, it's easy to write sophisticated pipelines that route and process the request according to whatever rules are necessary for the job.
async-ring.middleware
contains ports of all the middleware found in Ring core.
Just post an issue to get your favorite middleware ported!
Let's first take a look at how to write "Hello World" in Async Ring with Http-Kit:
(require '[org.http-kit.server :as http-kit])
(require 'async-ring.adapters.http-kit)
(require 'async-ring.core)
(def async-ring-app
(async-ring.core/constant-response
{:body "all ok" :status 200 :headers {"Content-Type" "text/plain"}}))
(def server (http-kit/run-server (async-ring.adapters.http-kit/to-httpkit async-ring-app)
{:port 8080}))
In this example, we see how to use the constant-response
handler, which is
the simplest Async Ring handler available. It always returns the same response.
After we create the app, we use to-httpkit
to make it Http-Kit compatible,
and then we pass it to the Http-Kit server to start the application.
Now, we'll look at how we can run an existing traditional Ring app on Async Ring with Jetty.
(require '[compojure.core :refer (defroutes GET)])
(require '[async-ring.adapters.jetty :as jetty])
(require 'async-ring.core)
(defroutes traditional-ring-app
(GET "/" []
{:body "all ok" :status 200 :headers {"Content-Type" "text/plain"}}))
(def async-ring-app
(async-ring.core/sync->async-adapter traditional-ring-app
{:parallelism 10
:buffer-size 5}))
(def server (jetty/run-jetty-async (jetty/to-jetty async-ring-app)
{:port 8080
:join? false}))
Here, we first create a traditional Ring app. Then, we add an adapter to make it asynchronous, allowing up to 10 requests to be simultaneously routed and processed, and up to 5 requests to be buffered. Finally, we start the Async Ring app on Jetty.
Async Ring has a small but growing library of native ports of Ring middleware. By using a native port of the Ring middleware, you're able to get the best performance.
(require '[compojure.core :refer (defroutes GET)])
(require '[async-ring.middleware :refer (wrap-params)])
(require '[org.http-kit.server :as http-kit])
(require 'async-ring.adapters.http-kit)
(require 'async-ring.core)
(defroutes traditional-ring-app
(GET "/" [q]
{:body (str "got " q) :status 200 :headers {"Content-Type" "text/plain"}}))
(def async-ring-app
(-> (async-ring.core/sync->async-adapter traditional-ring-app
{:parallelism 5
:buffer-size 5}))
(wrap-params {:parallelism 10
:buffer-size 100}))
(def server (http-kit/run-server (async-ring.adapters.to-httpkit async-ring-app)
{:port 8080}))
Here, we can see a few things. First of all, it's easy to compose Async Ring
handlers using ->
, just like regular Ring. Secondly, we can see that it's
possible to control the buffering and parallelism at each stage in the async
pipeline--this allows you to make decisions such as devoting extra CPU cores
to encoding/decoding middleware, and limiting the concurrent number of requests
to a database-backed session store.
Ported middleware lives in async-ring.middleware
. The
second argument to the async-ported middleware is always the async options, such
as parallelism and the buffer size.
If you'd like to see your middlewares ported to Async Ring, just file an issue and I'll do that quickly.
Beauty is a concurrent routing API that adds quality of server (QoS) features to Ring. Quality of service allows you separate routes that access independent databases to ensure that slowness in one backend doesn't slow down other requests. QoS also allows you to dynamically decide to prioritize some requests over others, to ensure that high-priority requests are completed first, regardless of arrival order.
Let's first look at a simple example of using Beauty:
(require '[compojure.core :refer (defroutes GET ANY)])
(require '[org.http-kit.server :as http-kit])
(require 'async-ring.adapters.http-kit)
(require 'async-ring.core)
(defroutes beautified-traditional-ring-app
(GET "/" []
(beauty-route :main (handle-root)))
(GET "/health" []
(handle-health-check))
(ANY "/rest/endpoint" []
(beauty-route :endpoint (handle-endpoint)))
(ANY "/rest/endpoint/:id" [id]
(beauty-route :endpoint 8 (handle-endpoint-id id))))
(def server (http-kit/run-server (async-ring.adapters.to-httpkit
(beauty-router
beautified-traditional-ring-app
{:main {:parallelism 1}
:endpoint {:parallelism 5
:buffer-size 100}}))
{:port 8080}))
The first thing we added is the beauty-route
annotations to each route that
we want to run on a prioritized concurrent pool. Note that you can choose to
freely mix which routes are Beauty routes, and which routes are executed
single-threaded (/health
isn't executed on a Beauty pool).
Next, notice that beauty-route
takes an argument: the name of the pool that you want to execute
the request on. beauty-router
handles creating pools with bounded concurrency
and a bounded buffer of pending requests. In this example, we are using 2 pools:
:main
, which has a single worker and only services requests to /
, and :endpoint
,
which services all of the routes under /rest
. The :endpoint
pool has 5
concurrent workers, and it can queue up to 100 requests before it exhibits
backpressure.
Finally, notice that the final route (/rest/endpoint/:id
) uses the priority form of
beauty-route
: normally, all requests are handled at the standard priority, 5
.
In some cases, you may know that certain routes are usually faster to execute,
or that certain clients may have have a cookie that indicates they need better
service. In that case, you can specify the priority for a beauty-route
d task.
These priorities are used to determine which request will be handled from the
pool's buffer.
The Beauty Router should be flexible enough to solve most QoS problems; nevertheless, Pull Requests are welcome to improve the functionality!
At first glance, Async Ring and Pedestal seem very similar--they're both frameworks for building asynchronous HTTP applications using a slightly modified Ring API. In this section, we'll look at some of the differences between Pedestal and Async Ring.
<!
and >!
to block wherever you
want.Performance numbers are currently only preliminary.
I benchmarked returning the "constant" body via Async Ring, Traditional Ring, and using a callback. All tests were done on http-kit.
Traditional Ring | httpkit async | Async Ring | |
---|---|---|---|
Mean | 1.45 ms | 1.542 ms | 1.953 ms |
90th %ile | 2 ms | 2 ms | 2 ms |
Thus Async Ring adds 500 microseconds to each call, but doesn't impact outliers significantly. This is something I'd like to try to to improve; however, the latency cost is worth it if you need these other concurrency features.
Copyright © 2014 David Greenberg
Distributed under the Eclipse Public License either version 1.0
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close