xtc_res(3)
---xtc_res(3)
resource accountant with hard caps and high-water alerts
| XTC_RES(3) | Library Functions Manual | XTC_RES(3) |
NAME
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
SYNOPSIS
#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);
DESCRIPTION
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
xtc_res_init();
it owns no OS resources, so there is no teardown call. Tracked kinds
(xtc_res_kind_t):
XTC_RES_TASKS- Live xtc_task_t allocations.
XTC_RES_CHANNELS- Live
xtc_chan_*() objects. XTC_RES_CHAN_SLOTS- In-flight messages across all channels.
XTC_RES_FDS- Open file descriptors attributable to xtc (see WHAT IS METERED).
XTC_RES_MEM_BYTES- Bytes in xtc-tracked allocations (see WHAT IS METERED).
XTC_RES_INBOX_MSGS- 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
xtc_res_set_cap()
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.
xtc_res_acquire()
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.
xtc_res_release()
gives capacity back, never fails, and clamps at zero on underflow. Both are
atomic and lock-free; safe under concurrent calls.
Alert
callbacks:
xtc_res_set_alert()
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).
xtc_res_set_alert_fn()
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.
xtc_res_used()
reports current usage,
xtc_res_high()
the high-water mark, and
xtc_res_rejects()
the number of acquires rejected because they would have exceeded the
cap.
WHAT IS METERED
A cap bounds only what is charged against it, and libxtc charges exactly the following; nothing else is metered:
XTC_RES_TASKS,XTC_RES_INBOX_MSGS- Every task spawned on a loop
(
xtc_async(),xtc_proc_spawn(), and their variants), against that loop's own accountant (xtc_loop_res()); a spawn from another thread also charges one inbox message. XTC_RES_CHANNELS,XTC_RES_CHAN_SLOTS- Channels created with a non-NULL accountant, and their in-flight messages.
XTC_RES_MEM_BYTES- 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(). Notxtc_malloc(), fiber stacks, process or mailbox structures, or any other internal allocation. XTC_RES_FDS- Descriptors returned by
xtc_net_listen(),xtc_net_dial(),xtc_net_accept(),xtc_net_unix_listen(),xtc_net_unix_dial() andxtc_net_udp_socket() while an accountant is attached withxtc_res_attach_net(), released byxtc_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.
xtc_res_attach_mctx()
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:
xtc_mctx_alloc(),
xtc_mctx_calloc()
and
xtc_mctx_strdup()
return NULL without touching the heap, and the
reject is counted.
xtc_mctx_free(),
xtc_mctx_reset()
and
xtc_mctx_destroy()
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.
xtc_res_attach_net()
is process-wide. While r is attached every creating
xtc_net call charges one XTC_RES_FDS unit
before
it creates the socket; past the cap it returns
XTC_E_RESOURCE and creates nothing (a refused dial
sends nothing).
xtc_net_close()
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
xtc_res_acquire(),
as in the example below.
EXAMPLES
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);
SEE ALSO
HISTORY
Appeared in xtc 0.1.
| September 23, 2026 | Debian |