Skip to content

The book · 07

Resilience

book.md · 82 lines · 2 min read

Goal: let processes fail and restart safely (supervision, links, and monitors), all of it emerging from the structure of the code (the BEAM model, with no behaviours declared separately).

When a process dies, the sequence is fixed: its defers run, its @mm is freed, and an error describing the death is produced (a crash with a reason, or a clean exit). That error is what reaches the observer: it is error-as-value applied to process death.

You don’t need a new construct to “be told of the death without dying along with it”: it is the catch |e| of spawn:

@supervisor(max_restarts: 3, window: 10s)
spawn worker(data) catch |e| {
// 'worker' died, I (the supervisor) didn't. 'e' is the reason. I react without propagating.
log("worker fell: {{e}}")
}

Since death is an ordinary error, you discriminate it with match |e|:

@supervisor(strategy: rest_for_one)
spawn db(conn) match |e| {
Normal => log("finished the work")
Crashed(m) => restart()
Timeout => escalate()
}

The three strategies emerge from the structure

Section titled “The three strategies emerge from the structure”
  • one_for_one: independent processes, each with its own catch. With no annotation, it is the default (no restart limit):
spawn worker_a(da) catch |e| { ... }
spawn worker_b(db) catch |e| { ... } // if A falls, B doesn't know
  • one_for_all: a spawn-block, where the processes live and die together. If one falls, the coordinator kills the siblings and restarts the group:
spawn {
indexer(docs) catch |e| { log(e) }
notifier(subs) catch |e| { log(e) }
} catch |e| {
notify_ops("critical group died: {{e}}") // the whole group exhausted itself
}
  • rest_for_one: a pipeline with order; the only one that requires annotation (it is not inferable):
@supervisor(strategy: rest_for_one)
spawn {
db_connection() // falls → restarts all three
db_writer() // falls → restarts writer + reader
db_reader() // falls → restarts only reader
} catch |e| { ... }
Section titled “Links and monitors, without raw primitives”

BEAM has link/monitor/trap_exit as primitives; Makoto does not expose them raw. Coupling fate is the tree (processes in the same spawn-block share fate because the structure says so), and observing without coupling is the catch. Invisible death cascades go away: the supervision tree is the code.

The process function is clean, with no built-in retry policy; the @supervisor at the call site decides max_restarts/window. When it exhausts, the outer catch fires, with no automatic escalation.

Next: 08 · Memory