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.
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 →