stdlib/httpd -- Async Handlersstdlib/httpd ships two complementary execution models:
| Path | When to reach for it |
|---|---|
Blocking thread-pool (httpd-new / httpd-new-pool) |
CPU-bound handlers; small concurrency; minimal mental overhead. A worker thread is occupied for the lifetime of each request. |
Async fiber server (httpd-new-async / httpd-new-async-with-limit) |
Slow upstreams (DB, downstream HTTP, queues), websocket-style long-poll, or any handler that wants to suspend on something the reactor watches (an fd, a timer). Concurrency is bounded by reactor capacity, not by n_workers. |
Both paths use the same handler shape -- (fn [conn] :nil ...) -- and
the same Track M middleware library composes over either.
Cross-reference: reactor-guide.md for the
underlying event loop, and async-await-guide.md
for the language-level suspension model that runs on top of fibers.
httpd-new-async port handler creates:
tur/reactor (event loop).local-fiber-group bound to that reactor.READ events.When a connection arrives, the accept callback local-spawns a request
fiber whose body runs the non-blocking version of the request lifecycle:
O_NONBLOCK on the accepted fd.recv loop with EWOULDBLOCK -> local-park-fd READ.send loop with EWOULDBLOCK -> local-park-fd WRITE.The whole thing runs on one thread by default (resolved OQ3 -- one group, one reactor; shard later if measurement demands it).
(load "stdlib/httpd.tur")
(defn main [] : int
(let [handler (fn [c : ptr<void>] : nil
(httpd-resp-status! c 200)
(httpd-resp-body! c "hello async"))
h (httpd-new-async 8080 handler)]
(httpd-run-async h) ;; blocks until httpd-stop-async fires
(httpd-async-free h)
0))
Inside a handler running on the async server, three primitives let you explicitly suspend the current fiber and free the reactor thread to drive other in-flight requests:
| Primitive | Resumes when... |
|---|---|
(httpd-await-readable conn) |
The conn's fd has data ready to read. |
(httpd-await-writable conn) |
The conn's fd can be written to. |
(httpd-await-timer conn ms) |
ms milliseconds have elapsed. |
All three are no-ops when the conn is from the blocking thread-pool
path (the conn's fiber_group field is NULL), so handlers that use
them stay portable across both server types.
(defn slow-handler [c : ptr<void>] : nil
;; Suspend for ~50ms before responding. Under the blocking pool this
;; ties up a worker; under async it parks the fiber and frees the
;; reactor thread for other requests.
(httpd-await-timer c 50)
(httpd-resp-status! c 200)
(httpd-resp-body! c "ok"))
Need a channel? The underlying local-park-chan from
stdlib/reactor.tur is available directly; we did
not wrap it in an httpd-await-chan form because the handler already
has access to the fiber group via the conn.
httpd-new-async-with-limit port handler max-in-flight caps the number
of concurrent request fibers. When the cap is hit, the accept callback
sends a quick HTTP/1.0 503 Service Unavailable directly from the
reactor thread and closes the connection without spawning a fiber:
;; Cap at 100 in-flight requests; the 101st through Nth get 503.
(let [h (httpd-new-async-with-limit 8080 handler 100)]
(httpd-run-async h)
(httpd-async-free h))
(httpd-new-async port handler) is just sugar for the cap-0 case
(unlimited). In production you usually want a cap matched to your
upstream's tolerance.
The H7 middleware shape -- (fn [next] (fn [conn] ...)) -- composes
unchanged over async handlers. httpd-call is a plain Turmeric call
sitting on the fiber stack, so next can park transparently:
(let [base (fn [c : ptr<void>] : nil
(httpd-await-timer c 10)
(httpd-resp-body! c "ok"))
stack (compose-middleware base mw-log mw-cors)
h (httpd-new-async 8080 stack)]
(httpd-run-async h)
(httpd-async-free h))
The same applies to mw-basic-auth, mw-json-body, mw-cors-with,
mw-compress (from stdlib/httpd-compress.tur, depends on the
tur/zlib spice), and compose-middleware-of (the runtime form).
See httpd-middleware-guide.md for the
full middleware library.
;; Per-request timeout: race the work against a 5-second timer.
;; (Channels make the "first to fire" pattern straightforward.)
(defn handler-with-deadline [c : ptr<void>] : nil
(httpd-await-timer c 5000)
;; ... if the slow work didn't finish, set 504 ...
)
A handler can register an arbitrary fd with the same reactor (via
reactor-add-fd) and await its readiness without blocking the worker.
For the common case of "call a slow downstream HTTP service", build a
small client helper on top of tur/reactor; the handler is then plain
straight-line Turmeric.
fread, blocking DNS, etc.): the fiber will not
suspend; the entire reactor thread stalls. Either wrap the call in
a worker thread you await on, or use the blocking pool.| Defn | Purpose |
|---|---|
httpd-new-async port handler |
Async server, no in-flight cap. |
httpd-new-async-with-limit port handler max-in-flight |
Async server with cap. |
httpd-run-async h |
Drive the fiber pump; blocks until stopped. |
httpd-stop-async h |
Request a graceful shutdown. |
httpd-async-free h |
Tear down state. Call after run returns. |
httpd-async-port h |
Read the bound port (useful with port=0). |
httpd-await-readable conn |
Suspend on READ readiness. |
httpd-await-writable conn |
Suspend on WRITE readiness. |
httpd-await-timer conn ms |
Suspend for ms milliseconds. |