A Erlang style pure Scheme Webserver and further

guenchi1 pts1 comments

Igropyr — a fault-tolerant web server in pure Chez Scheme

01 · Fault tolerance

Let It Crash

Every request runs in a supervised worker pool.<br>Handlers don't defend — they crash, and the system recovers.

Crashes heal themselves

A crashed worker is replaced instantly ; the task is<br>retried on a fresh worker, up to 3 times, before the client<br>sees an error.

A worker stuck longer than 30 s — even a CPU-spinning<br>loop — is killed and replaced : preemptive scheduling<br>means nothing can freeze the server.

A half-sent request parks only its own reader process<br>and is reaped by timeout. Other connections never notice.

Write the happy path. The supervisor owns the sad one.

(app-get app "/crash"<br>(lambda (req res)<br>;; the worker dies; the supervisor retries on a<br>;; fresh worker -- the pool refills itself<br>(raise 'handler-crashed)))

(app-get app "/stuck"<br>(lambda (req res)<br>;; a hot loop cannot freeze the system: preemption<br>;; keeps serving, the ticker kills this worker<br>(let loop ((n 0)) (loop (+ n 1)))))

02 &middot; Live systems

Hot code swapping

Replace the handler — or a single route — on a running<br>server. The listener, open connections and the worker pool stay up;<br>in-flight requests finish on the old code.

Deploy without a restart

Routes live in a mutable registry behind the pool. Re-registering<br>a path replaces it atomically for the next request;<br>http-swap! replaces the whole handler the same way.

Combined with graceful shutdown (http-shutdown!<br>drains in-flight work) and SO_REUSEPORT multi-process<br>listening, zero-downtime operation is the default, not a project.

(app-get app "/version"<br>(lambda (req res) (send-text! res "v1")))

;; hit /upgrade on the LIVE server:<br>(app-get app "/upgrade"<br>(lambda (req res)<br>(app-get app "/version" ; re-register =<br>(lambda (req res) ; hot replace<br>(send-text! res "v2 (hot swapped)")))<br>(send-text! res "upgraded")))

03 &middot; The remote retry ring

Faults speak a protocol

When retries are exhausted or a stuck worker is killed,<br>Igropyr doesn't just throw a 500 — it can tell the client exactly<br>what happened, on a connection that stays open.

Killed first, told after

The on-failure hook answers a structured fault<br>after the stuck worker is dead — so when the client hears<br>stuck, there is no execution left in flight .<br>The state is definite.

crash — retries exhausted; resubmit with changed<br>parameters, or compensate.

stuck — killed mid-flight; resubmit carrying state,<br>or roll back.

Keep-alive survives the fault, so the client resubmits on the<br>same connection and gets a fresh retry round. Shorten<br>stuck-ms and a user who once stared at a spinner<br>for 30 s now rings through several informed retries in the<br>same time — failures become invisible at the UI.

(app-listen app 8080<br>`((stuck-ms . 3000) ; fail fast<br>(check-ms . 1000)<br>(on-failure . ,(make-fault-handler))))

;; the client receives, connection kept alive:<br>;; {"fault":"crash","attempts":4,"retryable":true}<br>;; {"fault":"stuck","elapsed-ms":3012,...}<br>;; unset? the plain 500 remains. zero breakage.

04 &middot; Web programming with continuations

Dialogues are processes

A multi-request dialogue — a wizard, a booking, a<br>transfer — runs as one green process . Its local bindings are<br>the conversation state, including things a session store can never<br>hold: an open database transaction , spanning rounds.

Control flow is program text

"The user is at the confirm step" means the process is parked<br>at that line . A step order the code cannot express cannot<br>happen — no state machine to get wrong, no replay to defend<br>against.

The gone guarantee: death for any reason —<br>crash, TTL, completion — unregisters the process, and a later<br>resume answers gone. Dead process = dropped<br>connection = the database itself rolled back:<br>gone proves nothing committed. Together with<br>the fault codes above, the client always knows the definite<br>server state — a complete remote transaction ring.

(conversation-start!<br>(lambda (req suspend!)<br>(let ((tx (begin-tx!))) ; live, across requests<br>(guard (e (#t (rollback! tx) (raise e)))<br>(let ((req2 (suspend! confirm-page)))<br>(commit! tx)<br>done))))<br>req)

(conversation-resume! id req) ; => reply | 'gone<br>;; 'gone means: rolled back. guaranteed.

Foundations

What it stands on

Pure Chez Scheme

Every line is Scheme — R6RS libraries in .sc, no C<br>shim. libuv, zlib and the crypto for MySQL auth are reached<br>through Chez's FFI directly. Whole-program compilation folds the<br>framework and your app into one optimized binary.

Erlang-style actors

Green processes with spawn / send / receive,<br>link and monitor, a process registry,<br>gen-server and PubSub. One OS thread, preemptive<br>scheduling, pure message passing — no shared state, no locks.

Async on libuv

One event loop feeds thousands of parked processes. DNS, file<br>reads and database round-trips park the calling process,<br>never the thread. Non-blocking HTTP/WebSocket clients and<br>Redis/MySQL drivers included.

120k+<br>req/s, keep-alive, laptop

failed requests under ab -c 500

≤35s<br>full...

worker stuck fault process server crash

Related Articles