cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
reactor_rx.h
Go to the documentation of this file.
1/* Internal RX-uring receive path (issue #179).
2 *
3 * The C1M async HTTP server normally waits for readability with a one-shot
4 * POLL_ADD and then drains the fd with recv(). When the reactor runs a real
5 * io_uring ring, the receive wait can instead be an IORING_OP_RECV whose
6 * completion IS the readiness signal: one SQE replaces the POLL+recv pair
7 * and rides the existing deferred-SQE batch, so no extra io_uring_enter is
8 * paid beyond what the run loop already spends.
9 *
10 * SQE namespace: the run loop dispatches on cqe->user_data, historically an
11 * ev_ctx pointer. RECV SQEs tag the low bit of user_data and carry the
12 * connection pointer in the remaining bits:
13 *
14 * user_data = (uint64_t)conn | 1
15 *
16 * ev_ctx pointers come from cwist_alloc (malloc-backed, so at least
17 * max_align_t aligned) and can never carry the tag bit; reactor.c asserts
18 * this when arming. Multishot-accept slots (see the bottom of this header)
19 * tag bit 1 instead, so the run loop can tell all three apart.
20 *
21 * RX-epoll: on an epoll reactor (no io_uring ring, e.g. under a seccomp
22 * profile that blocks it) the same contract is served without SQEs. The fd
23 * stays registered edge-triggered (no EPOLLONESHOT) with user_data
24 * fd << 1 | 1, and on readiness the reactor itself recv()s into the armed
25 * buffer and reports the result through the same completion handler. A
26 * steady keep-alive request then costs one recv and one send, where the
27 * one-shot path also paid an epoll_ctl re-arm. Epoll arms are accepted on
28 * the run thread only; elsewhere cwist_reactor_recv_arm returns false and the
29 * caller's POLL fallback re-arms from the run thread. Any cwist_reactor_add,
30 * _mod or _del of the fd takes it back to the one-shot path. -EAGAIN is
31 * never reported: the receive stays armed until bytes, EOF or an error.
32 *
33 * This header is private to the build (src/sys/io); it is not installed.
34 * Everything compiles to no-ops on non-Linux so callers need no ifdefs
35 * beyond cheap inline checks. */
36#ifndef CWIST_SYS_IO_REACTOR_RX_H
37#define CWIST_SYS_IO_REACTOR_RX_H
38
40#include <stdbool.h>
41#include <stdint.h>
42
43#ifdef __cplusplus
44extern "C" {
45#endif
46
47#ifdef __linux__
48
49/* CWIST_RX_URING env gate: unset = auto (enabled), "1" = force attempt,
50 * "0" = force off (legacy POLL path). Cached after the first read. This is
51 * only the knob; per-reactor usability still requires an io_uring ring (see
52 * cwist_reactor_rx_supported). */
53bool cwist_rx_uring_env_enabled(void);
54
55/* True iff this reactor can take the RX path: an io_uring reactor with
56 * CWIST_RX_URING allowing it, or an epoll reactor with CWIST_RX_EPOLL
57 * allowing it. */
58bool cwist_reactor_rx_supported(const cwist_reactor_t *reactor);
59
60/* Drop the epoll registration state recorded for fd. Call on a freshly
61 * accepted fd: its number may have belonged to a closed socket, and the
62 * stale state would otherwise cost the first re-arm a failed epoll_ctl (and
63 * must never let an RX arm for a new connection skip registration). */
64void cwist_reactor_fd_forget(int fd);
65
66/* Completion handler for tagged RECV SQEs. conn is the untagged connection
67 * pointer; res is the CQE result: byte count, 0 (EOF), -EAGAIN, or another
68 * negative errno. Invoked on the reactor owner thread while dispatching. */
69typedef void (*cwist_rx_cb_t)(void *conn, int res);
70
71/* Register the completion handler (idempotent; http.c sets it once per
72 * worker reactor right after cwist_reactor_create). */
73void cwist_reactor_set_rx_cb(cwist_reactor_t *reactor, cwist_rx_cb_t cb);
74
75/* Arm one IORING_OP_RECV on the reactor ring. The SQE references
76 * [buf, buf+len) and must stay valid until the completion arrives, so the
77 * caller must not move or consume that buffer while the SQE is in flight.
78 * At most one RECV per connection at a time. When the latency probe is
79 * enabled, *armed_ns (if non-NULL) receives the arm timestamp so the caller
80 * can compute queue delay. Run-thread calls during dispatch defer into the
81 * existing SQE batch; other threads submit immediately so parked workers
82 * wake. Returns false when unsupported or on submission failure; the caller
83 * then falls back to the legacy POLL path. */
84bool cwist_reactor_recv_arm(cwist_reactor_t *reactor, int fd, void *buf, unsigned len, void *conn,
86
87/* Record one latency-probe sample (queue=true for queue_delay, false for
88 * svc) on the reactor's probe histograms. No-op unless the probe is
89 * enabled; used by the RX path, whose dispatch bypasses the ev_ctx probe
90 * wrapper in cwist_reactor_run. */
91void cwist_reactor_probe_record(cwist_reactor_t *reactor, bool queue, uint64_t sample_us);
92
93/* --- Multishot accept (issue #406) -----------------------------------------
94 * The listen socket's accept can ride one IORING_OP_ACCEPT with
95 * IORING_ACCEPT_MULTISHOT instead of a one-shot POLL + accept4 drain loop:
96 * each completion IS an accepted connection (SOCK_CLOEXEC|SOCK_NONBLOCK
97 * already set). Accept slots tag bit 1 of user_data (the RX path uses bit
98 * 0); both live outside the ev_ctx namespace, whose slots are malloc-aligned.
99 *
100 * The slot is persistent, unlike the one-shot ev_ctx slots: it survives
101 * across completions until the multishot accept terminates (completion
102 * without IORING_CQE_F_MORE) or the reactor is destroyed. */
103
104/* Completion handler for multishot accepts. fd >= 0 is a freshly accepted
105 * socket (CLOEXEC+NONBLOCK set). A negative fd is -errno and is delivered
106 * exactly once, when the accept terminates: unsupported opcode on an old
107 * kernel (-EINVAL), listener error, or teardown. The caller should resume
108 * the one-shot POLL accept loop; the reactor has already freed the slot when
109 * the callback returns. Runs on the reactor owner thread while dispatching. */
110typedef void (*cwist_accept_cb_t)(void *ctx, int fd);
111
112/* Arm one multishot accept on the reactor ring. At most one per listen fd.
113 * Returns false when this reactor has no io_uring ring (epoll fallback) or
114 * CWIST_ACCEPT_URING=0; the caller then uses the one-shot POLL path. Arm
115 * before cwist_reactor_run() or from the run thread. */
116bool cwist_reactor_accept_arm(cwist_reactor_t *reactor, int listen_fd, cwist_accept_cb_t cb,
117 void *ctx);
118
119#else /* !__linux__ */
120
121static inline bool cwist_rx_uring_env_enabled(void) {
122 return false;
123}
124static inline bool cwist_reactor_rx_supported(const cwist_reactor_t *reactor) {
125 (void)reactor;
126 return false;
127}
128static inline void cwist_reactor_fd_forget(int fd) {
129 (void)fd;
130}
131typedef void (*cwist_rx_cb_t)(void *conn, int res);
132static inline void cwist_reactor_set_rx_cb(cwist_reactor_t *reactor, cwist_rx_cb_t cb) {
133 (void)reactor;
134 (void)cb;
135}
136static inline bool cwist_reactor_recv_arm(cwist_reactor_t *reactor, int fd, void *buf, unsigned len,
137 void *conn, uint64_t *armed_ns) {
138 (void)reactor;
139 (void)fd;
140 (void)buf;
141 (void)len;
142 (void)conn;
143 (void)armed_ns;
144 return false;
145}
146static inline void cwist_reactor_probe_record(cwist_reactor_t *reactor, bool queue,
148 (void)reactor;
149 (void)queue;
151}
152typedef void (*cwist_accept_cb_t)(void *ctx, int fd);
153static inline bool cwist_reactor_accept_arm(cwist_reactor_t *reactor, int listen_fd,
154 cwist_accept_cb_t cb, void *ctx) {
155 (void)reactor;
157 (void)cb;
158 (void)ctx;
159 return false;
160}
161
162#endif /* __linux__ */
163
164#ifdef __cplusplus
165}
166#endif
167
168#endif /* CWIST_SYS_IO_REACTOR_RX_H */
size_t cap
Definition otel.c:56
struct cwist_reactor cwist_reactor_t
Definition reactor.h:14
void(* cwist_accept_cb_t)(void *ctx, int fd)
Definition reactor_rx.h:152
void(* cwist_rx_cb_t)(void *conn, int res)
Definition reactor_rx.h:131