sturdy-bulkhead implements middleware to queue compute-intensive requests within a Ring API.
It prevents CPU starvation by offloading work to dedicated worker pools using core.async.
Add to deps.edn:
{:deps com.sturdystats/sturdy-bulkhead {:mvn/version "VERSION"}}
Initialize a worker pool when your application starts.
(require '[sturdy.bulkhead :as bulkhead])
(def my-pool
(bulkhead/start-pool! {:num-workers 4 ; 4 concurrent queries max
:queue-size 20})) ; queue up to 20 requests
Use the provided middleware to wrap computationally expensive routes.
(def app
(-> heavy-query-handler
(bulkhead/wrap-compute-bound my-pool {:timeout-ms 10000})))
If you use reitit, you can attach the middleware to specific routes using its vector syntax:
(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
["/api"
["/heavy" {:get {:middleware [[bulkhead/wrap-compute-bound my-pool {:timeout-ms 10000}]]
:handler heavy-query-handler}}]
["/light" {:get {:handler light-query-handler}}]])))
When shutting down your application, clean up the resources:
(bulkhead/stop-pool! my-pool)
It is critical to tune the worker pool relative to your web server's thread pool:
The sum of your num-workers and queue-size should be strictly less than your web server's maximum thread pool size.
Because wrap-compute-bound is synchronous middleware, it blocks the incoming Jetty request thread while waiting for the bulkhead worker to finish (or for the timeout to occur).
num-workers): This should be less than your total CPU cores. This ensures the OS and web server always have dedicated resources to process network I/O, health checks, and fast endpoints without CPU starvation.queue-size): Determine how many requests you are willing to hold in memory during a traffic burst. If the queue is full, the middleware immediately returns a 503 Service Unavailable, freeing the jetty thread. Note that you may configure :queue-size 0, which means requests will only be accepted if a worker is immediately available for direct handoff; otherwise they are rejected.num-workers = 30 and queue-size = 250, all of jetty's threads could be sleeping on responses from bulkhead. This would render the server unresponsive until the queue drains.:timeout-ms), the caller immediately receives a 504 Gateway Timeout. However, if the worker pool has already begun executing your handler, that handler will continue running to completion in the background, occupying a worker thread until it finishes. Timeouts only free the incoming web server thread, they do not interrupt or cancel the underlying worker pool task.This project uses deps.edn aliases for testing and validation.
# Run unit tests
clj -X:test
# Generate code coverage report (in target/coverage/index.html)
clj -X:coverage
# Scan dependencies for known vulnerabilities (clj-watson)
clj -X:vuln
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |