xtc_res(3)

---

xtc_res(3)

resource accountant with hard caps and high-water alerts

XTC_RES(3) Library Functions Manual XTC_RES(3)

xtc_res_init, xtc_res_acquire, xtc_res_release, xtc_res_used, xtc_res_set_cap, xtc_res_high, xtc_res_rejects, xtc_res_set_alert, xtc_res_set_alert_fn, xtc_res_attach_mctx, xtc_res_attach_net — resource accountant with hard caps and high-water alerts

#include <xtc.h>
#include <xtc_res.h>

int
xtc_res_init(xtc_res_t *r, const xtc_res_caps_t *caps);

int
xtc_res_acquire(xtc_res_t *r, xtc_res_kind_t k, int64_t n);

void
xtc_res_release(xtc_res_t *r, xtc_res_kind_t k, int64_t n);

int64_t
xtc_res_used(const xtc_res_t *r, xtc_res_kind_t k);

void
xtc_res_set_cap(xtc_res_t *r, xtc_res_kind_t k, int64_t cap);

int64_t
xtc_res_high(const xtc_res_t *r, xtc_res_kind_t k);

int64_t
xtc_res_rejects(const xtc_res_t *r, xtc_res_kind_t k);

int
xtc_res_set_alert(xtc_res_t *r, xtc_res_kind_t k, double pct);

int
xtc_res_set_alert_fn(xtc_res_t *r, void (*fn)(xtc_res_kind_t, int64_t, int64_t, void *), void *user);

int
xtc_res_attach_mctx(xtc_res_t *r, struct xtc_mctx *m);

int
xtc_res_attach_net(xtc_res_t *r);

xtc_res tracks resource consumption against operator-configured hard caps. An xtc_res_t is caller-allocated (typically embedded in an executor or loop) and initialised with (); it owns no OS resources, so there is no teardown call. Tracked kinds (xtc_res_kind_t):

Live xtc_task_t allocations.
Live () objects.
In-flight messages across all channels.
Open file descriptors attributable to xtc (see WHAT IS METERED).
Bytes in xtc-tracked allocations (see WHAT IS METERED).
Cross-loop inbox messages in flight (per loop, not global).

Caps are supplied as an xtc_res_caps_t; XTC_RES_CAPS_DEFAULT gives reasonable values for a multi-loop executor on a workstation. A cap of zero means ‘no cap’ (unbounded; debug only). The current cap is not readable back through an accessor: it is the value the caller supplied, and () changes it at runtime. xtc_res_set_cap() returns no value, so an invalid argument (a NULL r, a bad kind, or a negative cap) is ignored and the existing cap is kept. Releases before 1.50 stored a negative cap, which then read as ‘no cap’ and silently made the kind unbounded. A negative value in the xtc_res_caps_t given to xtc_res_init() still means no cap.

() charges n units and returns XTC_OK, or XTC_E_RESOURCE if the request would exceed the cap, or XTC_E_INVAL for a bad kind, a negative n, or a NULL r. A request that cannot be represented (n greater than INT64_MAX minus the current usage) is rejected with XTC_E_RESOURCE and counted as a reject, rather than being allowed to overflow the counter: the headroom is tested before the addition, so neither the hard-cap invariant nor the non-negative accounting invariant can be broken by a huge request. () gives capacity back, never fails, and clamps at zero on underflow. Both are atomic and lock-free; safe under concurrent calls.

: () arms a high-water alert for one kind at pct, a FRACTION of the cap in the open interval (0.0, 1.0 --) so 0.8 means 80 percent, NOT an absolute threshold. It returns XTC_E_INVAL for a pct of 0, of 1.0, outside the interval, or NaN, and for a NULL r or a bad kind. Releases before 1.50 accepted 0 (which silently disabled the alert) and 1.0 (which fires only once acquires are already being refused). () installs the callback (fn receiving (kind, used, cap, user)); pass NULL to disable. The callback fires once when usage crosses the threshold upward and re-arms when usage drops back below it, so a hovering workload does not flood. Alerts are per-kind and independent.

() reports current usage, () the high-water mark, and () the number of acquires rejected because they would have exceeded the cap.

A cap bounds only what is charged against it, and libxtc charges exactly the following; nothing else is metered:

, XTC_RES_INBOX_MSGS
Every task spawned on a loop ((), (), and their variants), against that loop's own accountant (()); a spawn from another thread also charges one inbox message.
, XTC_RES_CHAN_SLOTS
Channels created with a non-NULL accountant, and their in-flight messages.
xtc_slab(3) chunks of a cache whose options name an accountant, and xtc_mctx(3) chunks (payload plus the fixed per-chunk header) of a context attached with xtc_res_attach_mctx(). Not (), fiber stacks, process or mailbox structures, or any other internal allocation.
Descriptors returned by (), (), (), (), () and () while an accountant is attached with xtc_res_attach_net(), released by xtc_net_close(). Inbound connections are charged when accepted with xtc_net_accept(3). Not descriptors from a raw accept(2), files, pipes, the I/O backend's own descriptors, or xtc_xproc(3) control sockets.

Metering that needs an attach is off by default: until it is called nothing is charged, at no cost. Releases before 1.50 charged XTC_RES_MEM_BYTES for slab chunks only and XTC_RES_FDS for nothing, while this page described both as general.

() charges every chunk of context m to r. The context's current footprint is charged at attach time; if that alone exceeds the cap the call returns XTC_E_RESOURCE and changes nothing. Afterwards an allocation that would pass the cap is refused: (), () and () return NULL without touching the heap, and the reject is counted. (), () and () give the bytes back. Passing a NULL r detaches, releasing the current footprint; attaching elsewhere moves it. A child context created while its parent is attached inherits the accountant; one created earlier does not. Attach at setup time: it must not race an allocation or free on the same context. Returns XTC_OK, XTC_E_RESOURCE, or XTC_E_INVAL for a NULL m.

() is process-wide. While r is attached every creating xtc_net call charges one XTC_RES_FDS unit it creates the socket; past the cap it returns XTC_E_RESOURCE and creates nothing (a refused dial sends nothing). () refunds the unit to whichever accountant the descriptor was charged to, so a later detach (r = NULL) does not unbalance descriptors already open. Close metered descriptors with xtc_net_close(): one closed with a raw close(2) keeps its unit until the kernel reuses that descriptor number for another metered socket. Always returns XTC_OK.

This is the centerpiece of xtc's ‘predictable behaviour under stress’ discipline: every resource that grows under load must be charged to a res accountant for its cap to hold. The list above is what the library charges on its own; an application charges anything else explicitly with (), as in the example below.

xtc_res_t res;
xtc_res_caps_t caps = XTC_RES_CAPS_DEFAULT;
caps.mem_bytes = 100 * 1024 * 1024;       /* 100 MiB */
caps.tasks     = 10000;
caps.fds       = 4096;

xtc_res_init(&res, &caps);
xtc_res_set_alert(&res, XTC_RES_MEM_BYTES, 0.8);   /* 80% of the cap */

/* Charge memory before allocating. */
if (xtc_res_acquire(&res, XTC_RES_MEM_BYTES, sz) != XTC_OK)
    return XTC_E_RESOURCE;
ptr = xtc_malloc(sz);
if (ptr == NULL) {
    xtc_res_release(&res, XTC_RES_MEM_BYTES, sz);
    return XTC_E_NOMEM;
}

/* When done: */
xtc_free(ptr);
xtc_res_release(&res, XTC_RES_MEM_BYTES, sz);

xtc_log(3), xtc_mctx(3), xtc_net(3), xtc_slab(3), xtc(7)

Appeared in xtc 0.1.

September 23, 2026 Debian

View the mdoc source