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.

Example:

  Signal.watch(Signal.Term, fn ->
    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#

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 to run when sig is delivered.

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

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