Links, monitors, and supervisors

Let it crash, detect it, and recover at a higher level: links, monitors, and supervisors.

---
  1. Monitors: tell me when it ends
  2. Links: fail together
  3. Supervisors: recovery as policy
  4. 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_monitor arms the monitor before the child can run, so there is no window where a fast-exiting child ends before you were watching. (A plain xtc_proc_spawn followed by xtc_monitor has that race; the atomic form closes it.)
  • A DOWN is an ordinary message. It lands in the mailbox like any other; xtc_down_decode tells you whether a received message is a DOWN and unpacks the target pid and reason. For embedders that need to distinguish an app exit code 1 from signal 1, xtc_down_decode_ex fills a struct with the kind, signal, and exit code in separate fields.

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_link forms 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