xtc_app(3)

---

xtc_app(3)

OTP-style application container

XTC_APP(3) Library Functions Manual XTC_APP(3)

xtc_app_create, xtc_app_destroy, xtc_app_start, xtc_app_run, xtc_app_stop, xtc_app_shutdown, xtc_app_drain_on_signal, xtc_app_is_shutdown_msg, xtc_app_loop, xtc_app_exec, xtc_app_registry — OTP-style application container

#include <xtc.h>
#include <xtc_app.h>

int
xtc_app_create(const xtc_app_opts_t *opts, xtc_app_t **out);

void
xtc_app_destroy(xtc_app_t *app);

int
xtc_app_start(xtc_app_t *app, const xtc_child_spec_t *children, int n_children);

int
xtc_app_run(xtc_app_t *app);

int
xtc_app_stop(xtc_app_t *app);

int
xtc_app_shutdown(xtc_app_t *app, int64_t drain_ns, int64_t force_ns, xtc_app_drain_report_t *report);

int
xtc_app_drain_on_signal(xtc_app_t *app, int64_t drain_ns, int64_t force_ns, xtc_app_drain_report_t *report);

int
xtc_app_is_shutdown_msg(const void *msg, size_t len);

xtc_loop_t *
xtc_app_loop(const xtc_app_t *app);

xtc_exec_t *
xtc_app_exec(const xtc_app_t *app);

xtc_reg_t *
xtc_app_registry(const xtc_app_t *app);

xtc_app is the L4 application container -- a root supervisor plus a process registry plus lifecycle plumbing. Models OTP's ‘application’ concept.

An app owns:

  • A loop (created if not supplied).
  • A root supervisor with the configured children.
  • A process registry where children can find each other by name (see xtc_reg(3)).

Typical usage:

xtc_app_opts_t opts = XTC_APP_OPTS_DEFAULT;
opts.name = "my_service";
opts.sup.strategy = XTC_SUP_ONE_FOR_ONE;

xtc_app_t *app;
xtc_app_create(&opts, &app);

xtc_child_spec_t kids[] = {
    { .name = "listener",  .fn = listener_proc, .arg = NULL,
      .policy = XTC_RESTART_PERMANENT },
    { .name = "metrics",   .fn = metrics_proc,  .arg = NULL,
      .policy = XTC_RESTART_TRANSIENT },
};
xtc_app_start(app, kids, 2);
xtc_app_run(app);              /* blocks until app stops */
xtc_app_destroy(app);

() returns the application's event loop and () its process registry. () returns the underlying xtc_exec(3) executor the application runs its loop on.

Setting opts.n_loops above 1 makes the app own an N-loop executor; the root supervisor runs on loop 0 and children are placed by xtc_child_spec_t.loop.

() is NOT a drain. It asks the root supervisor to stop and returns at once; the supervisor then ()s every child with reason 1 and does not wait for their cleanup. A child masked with () never observes that kill, and () then never returns.

xtc_app_shutdown() is the bounded, reported drain:

  1. It sends the message XTC_APP_SHUTDOWN_MSG ("$xtc_shutdown"), sizeof XTC_APP_SHUTDOWN_MSG bytes with the NUL, to every static child (the children given to ()). A child recognizes it with () and finishes by returning from its body.
  2. It waits up to drain_ns for every child to return or exit. A PERMANENT child that returns during the drain is parked, not restarted.
  3. It stops the supervisor (no more restarts) and force-cancels each remaining child with xtc_exit_pid(pid, XTC_APP_SHUTDOWN_REASON).
  4. It waits up to force_ns for every child to leave the proc table (() returns XTC_E_NOTFOUND). That is the cleanup-complete signal: the at-exit hooks and scope finalizers have returned. Neither “not alive” nor XTC_KILL_DELIVERED means that.
  5. It stops the app's loop or executor, so xtc_app_run() returns, and fills *report.

It always returns within about drain_ns + force_ns plus a few milliseconds, even when a child is masked forever: that child is reported as a survivor instead of being waited for. A survivor on an owned loop is freed with the app; on a borrowed loop (opts.loop) it stays parked there.

The report is size-versioned: set report->size to sizeof(xtc_app_drain_report_t) first (XTC_APP_DRAIN_REPORT_INIT does that). The library writes only the first size bytes, so fields added in a later minor release do not break an older caller.

typedef struct xtc_app_drain_report {
    size_t    size;           /* IN: sizeof the struct */
    int       n_children;     /* static children */
    int       n_drained;      /* finished within drain_ns */
    int       n_forced;       /* force-cancelled, then left the table */
    int       n_survivors;    /* still in the proc table */
    uint64_t  forced_mask;    /* bit i = child i forced (i < 64) */
    uint64_t  survivor_mask;  /* bit i = child i survived (i < 64) */
    int64_t   elapsed_ns;
} xtc_app_drain_report_t;

Call () from a plain thread, or, on a single-loop app, from a proc that is not one of the app's supervised children.

Multi-loop limit: the supervisor stops the executor the moment it exits (step 3), so a child force-cancelled on an app with n_loops above 1 can be frozen before its cleanup completes. It is then reported as a survivor, never falsely as forced, and the call still returns on time. Children that drain cooperatively in step 2 are not affected. Children added later with () are not drained or reported; the supervisor stop in step 3 kills them.

The library installs no signal handler by default. () is the opt-in: it installs a SIGTERM and SIGINT handler that only writes one byte to a non-blocking self-pipe, and spawns a watcher proc on the app's loop that waits on the pipe with () and starts a thread that runs xtc_app_shutdown(app, drain_ns, force_ns, report); xtc_app_run() joins that thread before it returns. No work runs in the handler. report (may be NULL) must stay valid until xtc_app_run() returns. Call it after xtc_app_start() and before xtc_app_run(), on an app that owns its loop. () restores the previous dispositions. One app per process may be armed at a time.

xtc_app_shutdown() returns XTC_OK when every child left the proc table, XTC_E_AGAIN when at least one survived, and XTC_E_INVAL for a NULL app, an app that is not inside xtc_app_run() (nothing could drain), a negative deadline, a report whose size is too small, a second call, a call from a supervised child, or a call from any proc on a multi-loop app.

xtc_app_drain_on_signal() returns XTC_OK, XTC_E_INVAL (bad arguments, a borrowed loop, or another app already armed), XTC_E_INTERNAL if the pipe could not be created, or XTC_E_NOSYS on Windows.

xtc_app_is_shutdown_msg() returns 1 for the drain request and 0 otherwise.

docs/_includes/snippets/12_graceful_drain.c starts workers, sends itself SIGTERM, drains and exits 0.

xtc_supervisor(3), xtc_proc(3), xtc_inspect(3), xtc_reg(3), xtc_loop(3), xtc(7)

Appeared in xtc 0.1.

September 23, 2026 Debian

View the mdoc source