Links, monitors, and supervisors
Let it crash, detect it, and recover at a higher level: links, monitors, and supervisors.
---- Monitors: tell me when it ends
- Links: fail together
- Supervisors: recovery as policy
- What you have learned
Processes fail. A worker hits a bad input, dereferences something it should not, or is asked to stop. The Erlang insight libxtc adopts is: do not try to prevent every failure inside the process; contain it, detect it, and recover at a higher level. Three mechanisms make that possible – links, monitors, and supervisors.
Monitors: tell me when it ends
A monitor is a one-way “notify me when that process ends” relation.
When the monitored process exits – cleanly, by xtc_exit_self, or via a
contained fault – libxtc delivers a DOWN message to the monitoring
process’s mailbox. The watcher stays alive regardless of how the target
ended; it just learns about it.
#include <stdio.h>
#include <stdint.h>
#include "xtc.h"
#include "xtc_loop.h"
#include "xtc_proc.h"
static void
worker(void *arg)
{
int code = (int)(intptr_t)arg;
printf("worker: exiting with reason %d\n", code);
xtc_exit_self(code); /* end deliberately with a reason */
}
static void
parent(void *arg)
{
xtc_loop_t *loop = arg; /* our loop, handed in at spawn */
xtc_pid_t child;
uint64_t ref;
void *raw;
size_t sz;
/* Spawn AND monitor atomically: there is no window in which the
* child could exit before the monitor is armed (which would race a
* plain spawn-then-monitor). */
if (xtc_proc_spawn_monitor(loop, worker,
(void *)(intptr_t)42, NULL, &child, &ref) != XTC_OK)
return;
/* Wait for the DOWN. It arrives as an ordinary mailbox message. */
if (xtc_recv(&raw, &sz, 1000LL * 1000 * 1000) != XTC_OK)
return;
{
xtc_pid_t who;
int reason;
if (xtc_down_decode(raw, sz, &who, &reason) == XTC_OK)
printf("parent: child ended, reason %d\n", reason);
else
printf("parent: unexpected message\n");
}
xtc_free(raw);
}
int
main(void)
{
xtc_loop_t *loop;
if (xtc_loop_init(&loop) != XTC_OK)
return 1;
if (xtc_proc_spawn(loop, parent, loop, NULL, NULL) != XTC_OK)
return 1;
(void)xtc_loop_run(loop);
(void)xtc_loop_fini(loop);
return 0;
}
Tested source: docs/_includes/snippets/04_monitor.c
worker: exiting with reason 42
parent: child ended, reason 42
Two details matter:
- Spawn and monitor atomically.
xtc_proc_spawn_monitorarms the monitor before the child can run, so there is no window where a fast-exiting child ends before you were watching. (A plainxtc_proc_spawnfollowed byxtc_monitorhas that race; the atomic form closes it.) - A DOWN is an ordinary message. It lands in the mailbox like any
other;
xtc_down_decodetells you whether a received message is a DOWN and unpacks the target pid and reason. For embedders that need to distinguish an app exit code1from signal1,xtc_down_decode_exfills a struct with the kind, signal, and exit code in separate fields.
Links: fail together
A link is a bidirectional relation: if either linked process dies
abnormally, the other is sent an exit signal too (by default, it dies as
well). Links are how you tie together a set of processes that only make
sense as a unit – if one collapses, the whole group should unwind
rather than limp on half-alive. xtc_link / xtc_unlink manage links;
xtc_proc_spawn_link spawns-and-links atomically, the same
race-free pattern as the monitor form.
Monitor or link? Use a monitor when the watcher should survive and react to the target’s death (a supervisor watching workers). Use a link when two processes share a fate and neither is meaningful without the other (a connection process and its dedicated codec process). Monitors are observational; links are structural.
Supervisors: recovery as policy
A supervisor is a process whose only job is to start a set of child processes, monitor them, and restart them according to a strategy when they die. You describe the children and the policy; the supervisor runs the “let it crash, then recover” loop for you.
flowchart TD
root(["root supervisor<br/>one_for_all"]) --> db["db supervisor<br/>one_for_one"]
root --> net["listener process"]
db --> w1["worker 1"]
db --> w2["worker 2"]
db --> w3["worker 3"]
w2 -. crashes .-> db
db -. restarts just w2 .-> w2
classDef sup fill:#e8f0ff,stroke:#3b6;
class root,db sup;
When worker 2 crashes, its one_for_one supervisor restarts only that
child. Had the supervisor been one_for_all, all three workers would
restart together. A crash the db supervisor cannot contain (its
restart budget is exhausted) escalates to the root supervisor.
libxtc’s xtc_sup_* API
(xtc_supervisor(3))
offers the classic OTP strategies:
- one-for-one – restart just the child that died.
- one-for-all – if any child dies, restart them all (for children whose states are interdependent).
- rest-for-one – restart the dead child and everything started after it.
- simple-one-for-one – a dynamic pool of identical children.
A restart budget (max restarts within a window) prevents a
crash-restart-crash storm: exceed it and the supervisor itself gives up
and escalates to its supervisor. This is the tree that makes BEAM
systems self-heal, and
examples/03_supervised_app.c
shows a whole application built as one – walked through in the
Examples section. That example also
demonstrates the three tools a supervised worker needs so a restart
cannot corrupt or leak: an
xtc_scope
whose finalizer frees the worker’s scratch resource on every exit path
(including the restart-kill), an
xtc_uncancelable
region around a two-step update so a kill cannot split it, and
xtc_trace_causal_enable
so a fault dump shows how the worker got to where it crashed.
Defensive error handling everywhere. The alternative to supervision is to make every function check and recover from every error locally – the code becomes mostly error handling, and a state that gets corrupted anyway (the case you did not foresee) has no clean recovery. “Let it crash” says: write the happy path clearly, contain the failure to one process, and put the recovery logic in one place – the supervisor – where it can restart from a known-good state. It is less code and it recovers from the unforeseen, not just the foreseen.
What you have learned
- A monitor notifies you (via a DOWN message) when a process ends; the watcher survives.
- A link binds two processes to a shared fate.
- A supervisor restarts children by policy, with a restart budget that escalates rather than thrashes.
- Prefer the atomic
spawn_monitor/spawn_linkforms to avoid the spawn-then-relate race.
You now have the whole process model: spawn, message, and supervise. The last guide chapter is about the outside world – files, sockets, and blocking C libraries – without giving up the single-threaded-loop simplicity: Blocking work and I/O.
← Processes and messages · Next: Blocking work and I/O →