March Docs

Signal

Signal module: register deferred handlers for OS signals.

A watcher is a zero-argument function () -> () invoked after the signal is delivered, on a normal green-thread stack (deferred green-thread dispatch) — never in async-signal context, so the handler may allocate, log, spawn tasks, and touch shared state freely. There is at most ONE watcher per signal; registering a second replaces the first.

Delivery latency is best-effort (up to ~1s): the runtime flips an atomic pending flag in signal context and drains it from the scheduler / event loop. Repeated deliveries before a drain coalesce into a single handler call.

Term / Int (SIGTERM / SIGINT) special case: while a watcher is registered the first delivery runs the handler and suppresses the default graceful shutdown, giving the program a chance to clean up on its own terms. A second delivery escapes the watcher and forces graceful shutdown (the Ctrl-C-twice escape hatch). Hup / Usr1 / Usr2 have no default action while watched, and their default OS disposition is restored on unwatch.

In compiled programs the scheduler's preemption signal cannot be watched: Usr1 by default, or whichever signal MARCH_PREEMPT_SIGNAL selects (setting it to USR2 frees Usr1; a Linux RTMIN signal frees both).

Example:

Signal.watch(Signal.Term, fn -> do
  Log.info("shutting down gracefully")
  drain_connections()
end)

(Post-7.1 the short fn -> body zero-arg spelling works; fn () -> body and fn _ -> body are also accepted.)

Types

typeSigSig = Term | Int | Hup | Usr1 | Usr2#

The signals a program may watch. Only non-fatal, portable signals are exposed so the runtime's scheduler/sanitizer signal-stack arrangement is never disturbed.

Functions

fnraiseraise(s : Sig) : ()#

Send sig to the current process — the symmetric trigger for watch.

Signal.raise(Signal.Usr2)

Useful for self-scheduled work and for exercising watchers in tests. The delivery is asynchronous: a registered watcher runs from the next scheduler drain, not synchronously at the call site.

fnunwatchunwatch(s : Sig) : ()#

Remove the watcher for sig, restoring the signal's default disposition.

Signal.unwatch(Signal.Usr1)

Safe to call when no watcher is registered.

fnwatchwatch(s : Sig, handler : () -> ()) : ()#

Register handler (a zero-argument function) to run when sig is delivered. Returns () immediately.

Signal.watch(Signal.Usr1, fn -> reload_config())

The handler runs later, from the scheduler drain, on a normal stack. At most one watcher exists per signal; a second watch replaces the first. For Term/Int, registering a watcher suppresses the default shutdown on the first delivery (a second delivery forces it).