cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
reactor.c
Go to the documentation of this file.
1
25#define _POSIX_C_SOURCE 200809L
27#include "reactor_rx.h"
28#include "async_debug.h"
31#include <unistd.h>
32#include <stdlib.h>
33#include <stdio.h>
34#include <string.h>
35#include <errno.h>
36#include <poll.h>
37#include <pthread.h>
38#include <stdatomic.h>
39#include <fcntl.h>
40#include <limits.h>
41#include <time.h>
42
43#ifdef __linux__
44#include <sys/syscall.h>
45#include <linux/io_uring.h>
46#if defined(__has_include)
47#if __has_include(<linux/time_types.h>)
48#include <linux/time_types.h>
49#endif
50#endif
51#include <sys/mman.h>
52#include <sys/epoll.h>
53#include <sys/socket.h>
54#include <sys/eventfd.h>
55/* malloc_trim(3): glibc-only <malloc.h>. The weak declaration below lets the
56 * binary link (and the trim degrade to a no-op) on Linux libcs without it,
57 * e.g. musl. */
58#include <malloc.h>
59
60#ifndef IORING_ENTER_EXT_ARG
61#define IORING_ENTER_EXT_ARG (1U << 3)
67};
68#endif
69
70#ifndef IORING_ASYNC_CANCEL_ALL
71#define IORING_ASYNC_CANCEL_ALL (1U << 0)
72#endif
73
74#ifndef IORING_ASYNC_CANCEL_FD
75#define IORING_ASYNC_CANCEL_FD (1U << 1)
76#endif
77
78#ifndef IORING_ASYNC_CANCEL_ANY
79#define IORING_ASYNC_CANCEL_ANY (1U << 2)
80#endif
81
82#ifndef IORING_ACCEPT_MULTISHOT
83#define IORING_ACCEPT_MULTISHOT (1U << 0)
84#endif
85
86#ifndef IORING_CQE_F_MORE
87#define IORING_CQE_F_MORE (1U << 1)
88#endif
89
90/* accept4-style flags for the SQE; on Linux these equal the O_* values. */
91#ifndef SOCK_CLOEXEC
92#define SOCK_CLOEXEC O_CLOEXEC
93#endif
94#ifndef SOCK_NONBLOCK
95#define SOCK_NONBLOCK O_NONBLOCK
96#endif
97
99
104static inline int sys_io_uring_setup(unsigned entries, struct io_uring_params *p) {
105 return (int)syscall(__NR_io_uring_setup, entries, p);
106}
107/* The kernel orders an SQE's publication before its CQE, but ThreadSanitizer
108 * cannot see through the io_uring rings, so a connection prepared by one
109 * thread and completed on the reactor thread looks like a race. Tell it:
110 * every SQ tail publication releases the ring, and the run thread acquires
111 * it before dispatching CQEs. No code is generated outside TSan builds. */
112#if defined(__SANITIZE_THREAD__)
113#define REACTOR_TSAN 1
114#elif defined(__has_feature)
115#if __has_feature(thread_sanitizer)
116#define REACTOR_TSAN 1
117#endif
118#endif
119#ifdef REACTOR_TSAN
120void __tsan_acquire(void *addr);
121void __tsan_release(void *addr);
122#define reactor_tsan_release(ring) __tsan_release((void *)(ring))
123#define reactor_tsan_acquire(ring) __tsan_acquire((void *)(ring))
124#else
125#define reactor_tsan_release(ring) ((void)(ring))
126#define reactor_tsan_acquire(ring) ((void)(ring))
127#endif
128
136static inline int sys_io_uring_enter(int ring_fd, unsigned to_submit, unsigned min_complete,
137 unsigned flags, sigset_t *sig) {
139}
148static inline int sys_io_uring_enter_timeout(int ring_fd, unsigned to_submit, unsigned min_complete,
149 unsigned flags, const struct __kernel_timespec *ts) {
150 struct io_uring_getevents_arg arg = {.ts = (uint64_t)(uintptr_t)ts};
152 flags | IORING_ENTER_EXT_ARG, &arg, sizeof(arg));
153}
154
166static void *mmap_ring(int fd, size_t sz, off_t off) {
167 void *p = mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_SHARED, fd, off);
168 return (p == MAP_FAILED) ? NULL : p;
169}
170
174static size_t sq_ring_size(struct io_uring_params *p) {
175 return p->sq_off.array + p->sq_entries * sizeof(uint32_t);
176}
177
181static size_t cq_ring_size(struct io_uring_params *p) {
182 return p->cq_off.cqes + p->cq_entries * sizeof(struct io_uring_cqe);
183}
184
185typedef struct {
186 int ring_fd;
187 /* Serializes SQ producers: the accept thread (reactor_add) and worker
188 * threads (rearm/del) submit concurrently; without this the SQE memcpy
189 * and tail advance race and one submission overwrites the other,
190 * silently dropping the connection's POLL_ADD. */
192 struct io_uring_sqe *sqes;
193 struct io_uring_cqe *cqes;
199 bool active;
200
201 // epoll fallback
202 int epoll_fd;
203 bool use_epoll;
204} reactor_impl_t;
205
206#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
207#include <sys/event.h>
208
209typedef struct {
210 int kq_fd;
211} reactor_impl_t;
212
213#else
214// Fallback
215typedef struct {
216} reactor_impl_t;
217#endif
218
219typedef struct {
220 int fd;
222 void *ctx;
223#ifdef __linux__
224 /* CLOCK_MONOTONIC timestamp taken in reactor_add_common right before the
225 * POLL_ADD submit/defer decision; approximates when the fd started
226 * waiting for readiness. Only set when CWIST_LATENCY_PROBE is enabled;
227 * zero means "armed before the probe was enabled" (defensive, see the
228 * dispatch loop). */
230#endif
231 /* Inline caller payload; ctx always points here. Zeroed on checkout so
232 * reuse across events cannot leak stale fields (calloc semantics
233 * without the heap). While the slot sits on the free list, ctx is
234 * borrowed as the next-free link. */
235 unsigned char payload[CWIST_REACTOR_PAYLOAD_SIZE];
236} reactor_event_ctx_t;
237
238/* Slot chunks grow on demand (see struct cwist_reactor below); 512 slots per
239 * chunk keeps a mostly-idle worker's footprint small (~40 KiB vs ~320 KiB at
240 * 4096) while busy reactors simply chain more chunks. */
241#define REACTOR_CHUNK_EVENTS 512
242
243typedef struct reactor_slot_chunk {
244 struct reactor_slot_chunk *next;
245 /* Slots follow in the same allocation. */
246 reactor_event_ctx_t slots[];
248
249#ifdef __linux__
250/* Persistent multishot-accept registration (issue #406). Unlike the
251 * one-shot ev_ctx slots this object lives across completions: one
252 * IORING_OP_ACCEPT+MULTISHOT keeps producing CQEs until it terminates
253 * (completion without IORING_CQE_F_MORE). Dispatched via bit 1 of
254 * user_data (bit 0 is the RX-uring tag); cwist_alloc alignment keeps both
255 * tag bits clear. */
256typedef struct reactor_accept_slot {
257 struct reactor_accept_slot *next;
258 int listen_fd;
260 void *ctx;
262#endif
263
264struct cwist_reactor {
265 reactor_impl_t impl;
266 bool running;
267 /* Foreign-thread completion stack (Treiber MPSC) plus the self-pipe that
268 * wakes a parked run thread: producers push a node and write a byte to
269 * wake_wr; the run thread drains the stack on the wake event (and once
270 * per loop iteration as a backstop). */
272 int wake_fd; /* Read side registered with the poller. */
273 int wake_wr; /* Write side (same fd as wake_fd for eventfd). */
274 /* Dynamically grown slot chunks: a fixed pool (formerly 4096 slots)
275 * capped every reactor at 4096 live connections, which is what shed
276 * requests en masse past ~500k concurrent connections. Chunks are never
277 * freed until destroy because slot pointers sit in in-flight CQEs. */
278 reactor_slot_chunk_t *chunks;
279 reactor_event_ctx_t *free_head; /* Free list threaded through slots. */
281 /* Armed one-shot timers, min-heap on deadline_ns. Run thread only. */
285#ifdef __linux__
286 /* Deferred SQE batching: submissions made by the reactor's own run thread
287 * while it dispatches a CQE batch (overwhelmingly connection re-arms, one
288 * per served request) are queued here and flushed with a single
289 * io_uring_enter after the batch, instead of paying one enter syscall per
290 * event. Cross-thread submissions (accept thread -> worker reactor) keep
291 * the immediate path so sleeping workers still wake. */
292 struct io_uring_sqe deferred_sqes[1024];
293 reactor_event_ctx_t *deferred_ctxs[1024];
295 /* SQEs parked in the SQ by queue_deferred, covered by the next wait
296 * enter's to_submit. Only touched by the run thread. */
298 /* Owned by the run thread: read only after reactor_on_run_thread(), so
299 * other threads never touch it. */
300 bool dispatching;
301 /* RX-uring completion handler for tagged (low-bit set) RECV SQEs;
302 * registered by the HTTP layer once per worker reactor. */
304 /* Persistent multishot-accept registrations (bit-1-tagged ACCEPT SQEs);
305 * freed on terminal completion or at destroy. */
307 /* RX-epoll: fds whose armed receive may already have bytes waiting (the
308 * last recv filled the buffer, or an edge arrived while unarmed). An
309 * edge-triggered registration will not report those again, so the run
310 * loop receives them before its next wait. Run thread only. */
311 int *rx_ready;
314 /* Per-request latency probe recorder (issue #166). Owner-thread only, so
315 * plain counters suffice. Two histograms: time from (re-)arm to dispatch
316 * (queue_delay) and callback runtime (svc). */
317#define LATENCY_PROBE_BUCKETS 18
318 struct {
323 uint64_t over_5ms; /* samples beyond 5000 us */
324 } probe[2];
325 /* CLOCK_MONOTONIC timestamp of the first event-free wake that started the
326 * current idle streak (0 = not idle). Run thread only. Drives the idle
327 * malloc_trim cadence; see reactor_idle_note_wake(). */
329#endif
330};
331
332/* The reactor this thread is running in cwist_reactor_run(), or NULL. A
333 * thread-local instead of a pthread_t owner field in the reactor: other
334 * threads would otherwise read that field while the run thread writes it. */
335static __thread cwist_reactor_t *t_run_reactor;
336
337static inline bool reactor_on_run_thread(const cwist_reactor_t *reactor) {
338 return t_run_reactor == reactor;
339}
340
341/* Microsecond bucket boundaries for the latency probe histograms: 18 buckets
342 * spanning [0,10) us up to [1e6,+inf) us. */
343#ifdef __linux__
345 10, 25, 50, 100, 250, 500, 1000, 2500, 5000,
346 10000, 25000, 50000, 100000, 250000, 500000, 1000000, 2500000};
347
353static bool latency_probe_enabled(void) {
354 static _Atomic int cached = -1;
356 if (v < 0) {
357 const char *s = getenv("CWIST_LATENCY_PROBE");
358 v = (s && strcmp(s, "1") == 0) ? 1 : 0;
360 }
361 return v == 1;
362}
363
376 int lo = 0, hi = LATENCY_PROBE_BUCKETS - 1;
377 while (lo < hi) { /* first bucket whose upper bound exceeds the sample */
378 int mid = (lo + hi) / 2;
380 hi = mid;
381 else
382 lo = mid + 1;
383 }
384 buckets[lo]++;
385 (*count)++;
386 *sum_us += sample_us;
387 if (sample_us > *max_us) *max_us = sample_us;
388 if (sample_us > 5000) (*over_5ms)++;
389}
390
391enum { LATENCY_PROBE_QUEUE = 0, LATENCY_PROBE_SVC = 1 };
392
402 uint64_t target = (count * pct_mille + 999) / 1000;
404 for (int i = 0; i < LATENCY_PROBE_BUCKETS; i++) {
405 cumulative += buckets[i];
406 if (cumulative >= target)
408 }
409 return UINT64_MAX;
410}
411
412static void latency_probe_dump(const cwist_reactor_t *reactor) {
413 if (!latency_probe_enabled()) return;
414 for (int phase = 0; phase < 2; phase++) {
415 const uint64_t *b = reactor->probe[phase].buckets;
416 uint64_t count = reactor->probe[phase].count;
417 if (count == 0) continue;
418 const char *name = phase == LATENCY_PROBE_QUEUE ? "queue_delay" : "svc";
419 fprintf(stderr, "[latency-probe] pid=%d %-12s count=%llu\n", (int)getpid(), name,
420 (unsigned long long)count);
422 "[latency-probe] pid=%d %-12s p50=%llu p90=%llu p99=%llu p999=%llu max=%llu us\n",
423 (int)getpid(), name, (unsigned long long)latency_probe_percentile(b, count, 500),
424 (unsigned long long)latency_probe_percentile(b, count, 900),
425 (unsigned long long)latency_probe_percentile(b, count, 990),
426 (unsigned long long)latency_probe_percentile(b, count, 999),
427 (unsigned long long)reactor->probe[phase].max_us);
428 fprintf(stderr, "[latency-probe] pid=%d %-12s mean=%llu us over_5ms=%llu\n", (int)getpid(),
429 name, (unsigned long long)(reactor->probe[phase].sum_us / count),
430 (unsigned long long)reactor->probe[phase].over_5ms);
431 for (int i = 0; i < LATENCY_PROBE_BUCKETS; i++) {
432 if (!b[i]) continue;
433 if (i + 1 < LATENCY_PROBE_BUCKETS)
434 fprintf(stderr, "[latency-probe] pid=%d %-12s [%-8u,%-8u) us : %llu\n",
435 (int)getpid(), name, i == 0 ? 0u : latency_probe_bounds_us[i - 1],
437 else
438 fprintf(stderr, "[latency-probe] pid=%d %-12s [1000000, +inf) us : %llu\n",
439 (int)getpid(), name, (unsigned long long)b[i]);
440 }
441 }
442}
443
444/* --- RX-uring receive path (issue #179) ------------------------------------
445 * CWIST_RX_URING: unset = auto (enabled whenever the reactor has a real
446 * io_uring ring), "1" = force attempt, "0" = force off (legacy POLL path).
447 * Cached after the first read like the other env knobs in this file. */
448static bool uring_submit(cwist_reactor_t *reactor, struct io_uring_sqe *out_sqe);
449
450bool cwist_rx_uring_env_enabled(void) {
451 static _Atomic int cached = -1;
453 if (v < 0) {
454 const char *s = getenv("CWIST_RX_URING");
455 v = (s && strcmp(s, "0") == 0) ? 0 : 1;
457 }
458 return v == 1;
459}
460
461/* CWIST_RX_EPOLL: unset = on, "0" = off (epoll reactors keep the one-shot
462 * POLL + recv path). Cached like CWIST_RX_URING. */
463static bool rx_epoll_env_enabled(void) {
464 static _Atomic int cached = -1;
466 if (v < 0) {
467 const char *s = getenv("CWIST_RX_EPOLL");
468 v = (s && strcmp(s, "0") == 0) ? 0 : 1;
470 }
471 return v == 1;
472}
473
474bool cwist_reactor_rx_supported(const cwist_reactor_t *reactor) {
475 if (!reactor) return false;
476 if (reactor->impl.use_epoll) return rx_epoll_env_enabled();
477 return cwist_rx_uring_env_enabled();
478}
479
480/* Per-fd epoll registration state, process-wide (an fd belongs to one
481 * reactor at a time). It serves two purposes:
482 *
483 * - A hint for the one-shot path: a re-arm of an fd that is already
484 * registered goes straight to EPOLL_CTL_MOD instead of failing an ADD with
485 * EEXIST first, which used to cost two epoll_ctl calls per keep-alive
486 * request.
487 * - The RX-epoll receive path (reactor_rx.h): the fd stays registered
488 * edge-triggered, with no EPOLLONESHOT, and the reactor does the recv() into
489 * the armed buffer itself. A steady keep-alive request then costs a recv
490 * and a send, with no epoll_ctl.
491 *
492 * state is the only field touched off the run thread (by cwist_reactor_add,
493 * _mod and _del, which take an fd out of RX mode); the rest belongs to the
494 * run thread of the reactor that armed the fd. Every state value is only a
495 * hint for epoll_ctl: a stale one costs one failed call (ENOENT/EEXIST) and
496 * the fallback, never a lost registration, except RX, which recv_arm trusts
497 * only for the same connection pointer (see cwist_reactor_fd_forget). */
498enum { EPFD_UNKNOWN = 0, EPFD_ONESHOT = 1, EPFD_RX = 2 };
499
500typedef struct {
501 _Atomic uint8_t state;
502 bool armed; /* A receive is armed: the next readable edge recv()s. */
503 bool queued; /* On the owning reactor's rx_ready list. */
504 bool maybe_more; /* Bytes may be waiting that no new edge will report. */
505 unsigned len;
506 void *buf;
507 void *conn;
509
510#define EPFD_CHUNK_BITS 10
511#define EPFD_CHUNK (1u << EPFD_CHUNK_BITS)
512#define EPFD_MAX_FDS (1u << 22)
514
516static epoll_fd_slot_t *epfd_slot(int fd, bool create) {
517 if (fd < 0 || (unsigned)fd >= EPFD_MAX_FDS) return NULL;
520 if (!c) {
521 if (!create) return NULL;
522 /* Chunks live as long as the process: reachable from the table and
523 * reused by every later fd in their range. */
525 if (!fresh) return NULL;
526 memset(fresh, 0, EPFD_CHUNK * sizeof(*fresh));
529 c = fresh;
530 } else {
531 cwist_free(fresh); /* Another thread installed it first; c holds that one. */
532 }
533 }
534 return &c[(unsigned)fd & (EPFD_CHUNK - 1)];
535}
536
537static void epfd_set_state(int fd, uint8_t state) {
540}
541
542static uint8_t epfd_get_state(int fd) {
543 epoll_fd_slot_t *slot = epfd_slot(fd, false);
545}
546
547void cwist_reactor_fd_forget(int fd) {
549}
550
552static bool epoll_register(int epfd, int fd, struct epoll_event *ev) {
553 if (epfd_get_state(fd) != EPFD_UNKNOWN) {
554 if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, ev) == 0) return true;
555 return errno == ENOENT && epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == 0;
556 }
557 if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == 0) return true;
558 return errno == EEXIST && epoll_ctl(epfd, EPOLL_CTL_MOD, fd, ev) == 0;
559}
560
562static bool rx_ready_push(cwist_reactor_t *r, int fd, epoll_fd_slot_t *slot) {
563 if (slot->queued) return true;
564 if (r->rx_ready_n == r->rx_ready_cap) {
565 uint32_t cap = r->rx_ready_cap ? r->rx_ready_cap * 2 : 64;
566 int *grown = cwist_alloc(cap * sizeof(*grown));
567 if (!grown) return false;
568 if (r->rx_ready_n) memcpy(grown, r->rx_ready, r->rx_ready_n * sizeof(*grown));
569 cwist_free(r->rx_ready);
570 r->rx_ready = grown;
571 r->rx_ready_cap = cap;
572 }
573 r->rx_ready[r->rx_ready_n++] = fd;
574 slot->queued = true;
575 return true;
576}
577
580static void rx_epoll_recv(cwist_reactor_t *r, int fd, epoll_fd_slot_t *slot) {
581 ssize_t n;
582 do {
583 n = recv(fd, slot->buf, slot->len, MSG_DONTWAIT);
584 } while (n < 0 && errno == EINTR);
585 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
586 slot->maybe_more = false;
587 return;
588 }
589 int res = n >= 0 ? (int)n : -errno;
590 /* A full buffer may have left bytes behind; a short read drained the
591 * socket, so anything newer raises a fresh edge. */
592 slot->maybe_more = n > 0 && (size_t)n == slot->len;
593 slot->armed = false;
594 /* rx_cb may close and free the connection, or re-arm (possibly queueing
595 * this fd again); the slot itself stays valid. */
596 if (r->rx_cb) r->rx_cb(slot->conn, res);
597}
598
600static void rx_epoll_event(cwist_reactor_t *r, int fd) {
601 epoll_fd_slot_t *slot = epfd_slot(fd, false);
602 if (!slot || atomic_load_explicit(&slot->state, memory_order_relaxed) != EPFD_RX) return;
603 if (!slot->armed) {
604 /* The connection is between receives (serving, or parked on a
605 * deferred response). Remember the edge for the next arm. */
606 slot->maybe_more = true;
607 return;
608 }
609 rx_epoll_recv(r, fd, slot);
610}
611
615 uint32_t n = r->rx_ready_n;
616 for (uint32_t i = 0; i < n; i++) {
617 int fd = r->rx_ready[i];
618 epoll_fd_slot_t *slot = epfd_slot(fd, false);
619 if (!slot) continue;
620 slot->queued = false;
621 if (atomic_load_explicit(&slot->state, memory_order_relaxed) != EPFD_RX || !slot->armed) {
622 continue;
623 }
624 rx_epoll_recv(r, fd, slot);
625 }
626 /* Keep what the callbacks queued during this pass. */
627 memmove(r->rx_ready, r->rx_ready + n, (r->rx_ready_n - n) * sizeof(*r->rx_ready));
628 r->rx_ready_n -= n;
629}
630
632static bool rx_epoll_arm(cwist_reactor_t *reactor, int fd, void *buf, unsigned len, void *conn) {
633 /* Off the run thread the slot would race the run thread's receive; the
634 * caller falls back to the one-shot POLL, whose callback arms from the
635 * run thread. */
636 if (!reactor_on_run_thread(reactor)) return false;
637 epoll_fd_slot_t *slot = epfd_slot(fd, true);
638 if (!slot) return false;
639 bool registered =
640 atomic_load_explicit(&slot->state, memory_order_relaxed) == EPFD_RX && slot->conn == conn;
641 slot->buf = buf;
642 slot->len = len;
643 slot->conn = conn;
644 if (!registered) {
645 /* First arm, or the fd went through the one-shot path since. MOD and
646 * ADD both report an fd that is already readable, so no edge is lost
647 * across the switch. */
648 struct epoll_event ev;
649 ev.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
650 ev.data.u64 = ((uint64_t)(unsigned)fd << 1) | 1u;
651 if (!epoll_register(reactor->impl.epoll_fd, fd, &ev)) return false;
653 slot->maybe_more = false;
654 slot->armed = true;
655 return true;
656 }
657 slot->armed = true;
658 if (slot->maybe_more && !rx_ready_push(reactor, fd, slot)) {
659 slot->armed = false;
660 return false;
661 }
662 return true;
663}
664
665void cwist_reactor_set_rx_cb(cwist_reactor_t *reactor, cwist_rx_cb_t cb) {
666 if (reactor) reactor->rx_cb = cb;
667}
668
669bool cwist_reactor_recv_arm(cwist_reactor_t *reactor, int fd, void *buf, unsigned len, void *conn,
671 if (!reactor || fd < 0 || !buf || len == 0 || !conn) return false;
672 if (!cwist_reactor_rx_supported(reactor)) return false;
673 if (reactor->impl.use_epoll) {
675 struct timespec ts;
677 *armed_ns = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
678 }
679 return rx_epoll_arm(reactor, fd, buf, len, conn);
680 }
681 /* Tag-bit discipline: ev_ctx pointers come from cwist_alloc (malloc
682 * backed, at least max_align_t aligned), so the low bit is always zero
683 * there and one here. Refuse a misaligned connection pointer rather
684 * than aliasing into the ev_ctx namespace. */
685 if (((uintptr_t)conn & 1u) != 0) return false;
686
687 struct io_uring_sqe sqe;
688 memset(&sqe, 0, sizeof(sqe));
689 sqe.opcode = IORING_OP_RECV;
690 sqe.fd = fd;
691 sqe.addr = (uint64_t)(uintptr_t)buf;
692 sqe.len = len;
693 sqe.user_data = (uint64_t)(uintptr_t)conn | 1u;
695 struct timespec ts;
697 *armed_ns = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
698 }
699
700 /* Run-thread re-arms defer into the batch flush; everything else
701 * submits immediately so remote workers wake. Data SQEs carry no ev_ctx
702 * slot, so deferred_ctxs[] holds NULL for them and flush_deferred knows
703 * the connection pointer rides in the tagged user_data. */
704 if (reactor_on_run_thread(reactor) && reactor->dispatching &&
705 reactor->deferred_n <
706 (uint32_t)(sizeof(reactor->deferred_sqes) / sizeof(reactor->deferred_sqes[0]))) {
707 uint32_t slot = reactor->deferred_n++;
708 reactor->deferred_sqes[slot] = sqe;
709 reactor->deferred_ctxs[slot] = NULL;
710 return true;
711 }
712 return uring_submit(reactor, &sqe);
713}
714
715void cwist_reactor_probe_record(cwist_reactor_t *reactor, bool queue, uint64_t sample_us) {
716 if (!reactor || !latency_probe_enabled()) return;
718 latency_probe_record(reactor->probe[phase].buckets, &reactor->probe[phase].count,
719 &reactor->probe[phase].sum_us, &reactor->probe[phase].max_us,
720 &reactor->probe[phase].over_5ms, sample_us);
721}
722
723/* --- Multishot accept (issue #406) ------------------------------------------
724 * CWIST_ACCEPT_URING: unset = auto (multishot accept whenever the reactor
725 * has a real io_uring ring), "0" = force off (one-shot POLL + accept4 loop).
726 * Cached after the first read like the other env knobs in this file. */
727static bool accept_uring_env_enabled(void) {
728 static _Atomic int cached = -1;
730 if (v < 0) {
731 const char *s = getenv("CWIST_ACCEPT_URING");
732 v = (s && strcmp(s, "0") == 0) ? 0 : 1;
734 }
735 return v == 1;
736}
737
739 reactor_accept_slot_t **pp = &reactor->accept_slots;
740 while (*pp) {
741 if (*pp == slot) {
742 *pp = slot->next;
743 return;
744 }
745 pp = &(*pp)->next;
746 }
747}
748
749bool cwist_reactor_accept_arm(cwist_reactor_t *reactor, int listen_fd, cwist_accept_cb_t cb,
750 void *ctx) {
751 if (!reactor || listen_fd < 0 || !cb) return false;
752 if (reactor->impl.use_epoll || !reactor->impl.active) return false;
753 if (!accept_uring_env_enabled()) return false;
754
756 if (!slot) return false;
757 slot->listen_fd = listen_fd;
758 slot->cb = cb;
759 slot->ctx = ctx;
760
761 struct io_uring_sqe sqe;
762 memset(&sqe, 0, sizeof(sqe));
763 sqe.opcode = IORING_OP_ACCEPT;
764 sqe.fd = listen_fd;
765 sqe.accept_flags = SOCK_CLOEXEC | SOCK_NONBLOCK;
767 sqe.user_data = (uint64_t)(uintptr_t)slot | 2u;
768
769 /* Link before submitting: a fast completion can dispatch on the run
770 * thread as soon as the SQE is visible, and destroy must find the slot. */
771 slot->next = reactor->accept_slots;
772 reactor->accept_slots = slot;
773 if (!uring_submit(reactor, &sqe)) {
774 reactor_accept_unlink(reactor, slot);
776 return false;
777 }
778 return true;
779}
780#endif
781
782static reactor_event_ctx_t *alloc_reactor_ctx(cwist_reactor_t *r, int fd, cwist_reactor_cb_t cb,
783 const void *payload, size_t payload_size) {
784 if (!r || payload_size > CWIST_REACTOR_PAYLOAD_SIZE) return NULL;
785 pthread_mutex_lock(&r->pool_lock);
786 reactor_event_ctx_t *ev_ctx = r->free_head;
787 if (ev_ctx) r->free_head = (reactor_event_ctx_t *)ev_ctx->ctx;
788 pthread_mutex_unlock(&r->pool_lock);
789
790 if (!ev_ctx) {
791 /* Free list empty: grow a chunk. Allocate and link it outside the
792 * lock so the ~40 KiB setup does not serialize every other submitter
793 * on this reactor; under the lock the chunk is pushed onto the free
794 * list wholesale and one slot popped. Chunks are never freed until
795 * destroy, so a race with another grower only means two chunks. */
797 cwist_alloc(sizeof(*chunk) + REACTOR_CHUNK_EVENTS * sizeof(reactor_event_ctx_t));
798 if (!chunk) return NULL;
799 for (uint32_t i = 0; i + 1 < REACTOR_CHUNK_EVENTS; i++) {
800 chunk->slots[i].ctx = &chunk->slots[i + 1];
801 }
802 chunk->slots[REACTOR_CHUNK_EVENTS - 1].ctx = NULL;
803
804 pthread_mutex_lock(&r->pool_lock);
805 chunk->next = r->chunks;
806 r->chunks = chunk;
807 chunk->slots[REACTOR_CHUNK_EVENTS - 1].ctx = r->free_head;
808 r->free_head = &chunk->slots[0];
809 ev_ctx = r->free_head;
810 r->free_head = (reactor_event_ctx_t *)ev_ctx->ctx;
811 pthread_mutex_unlock(&r->pool_lock);
812 }
813 if (!ev_ctx) return NULL;
814
815 memset(ev_ctx, 0, sizeof(*ev_ctx));
816 ev_ctx->fd = fd;
817 ev_ctx->cb = cb;
818 ev_ctx->ctx = ev_ctx->payload;
819 if (payload && payload_size > 0) {
820 memcpy(ev_ctx->payload, payload, payload_size);
821 }
822 return ev_ctx;
823}
824
825static void free_reactor_ctx(cwist_reactor_t *r, reactor_event_ctx_t *ev_ctx) {
826 if (!r || !ev_ctx) return;
827 pthread_mutex_lock(&r->pool_lock);
828 ev_ctx->ctx = r->free_head;
829 r->free_head = ev_ctx;
830 pthread_mutex_unlock(&r->pool_lock);
831}
832
834 if (!r || !node || !node->cb) return false;
836 do {
837 node->next = head;
838 } while (!atomic_compare_exchange_weak_explicit(&r->post_head, &head, node,
840 /* Wake only on the first post of a pending batch. A non-empty stack
841 * already has a wake pending; another write would be redundant. */
842 if (head == NULL && r->wake_wr >= 0) {
843 uint64_t one = 1;
844 ssize_t ign = write(r->wake_wr, &one, sizeof(one));
845 (void)ign; /* EAGAIN means the run thread is already awake. */
846 }
847 return true;
848}
849
850/* Pop the whole MPSC stack and run the callbacks oldest-first. Only ever
851 * called by the reactor's run thread (or destroy, after it has stopped). */
852static void reactor_drain_posts(cwist_reactor_t *r) {
856 while (list) {
858 list->next = rev;
859 rev = list;
860 list = next;
861 }
862 while (rev) {
864 rev->cb(rev->ctx);
865 rev = next;
866 }
867}
868
869/* run flag: cleared by cwist_reactor_stop() from any thread. */
870static bool reactor_running(cwist_reactor_t *r) {
872}
873
874/* ---- one-shot timers (min-heap, run thread only) ---- */
875
876/* Upper bound on any poll wait, timers or not: the shutdown flags are only
877 * re-checked between waits (see cwist_reactor_run). */
878#define REACTOR_IDLE_WAIT_NS 100000000ull
879
880static uint64_t reactor_now_ns(void) {
881 struct timespec ts;
883 return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
884}
885
886static void timer_heap_place(cwist_reactor_t *r, uint32_t i, cwist_reactor_timer_t *t) {
887 r->timer_heap[i] = t;
888 t->heap_slot = i + 1;
889}
890
891static void timer_heap_up(cwist_reactor_t *r, uint32_t i) {
892 cwist_reactor_timer_t *t = r->timer_heap[i];
893 while (i > 0) {
894 uint32_t parent = (i - 1) / 2;
895 if (r->timer_heap[parent]->deadline_ns <= t->deadline_ns) break;
896 timer_heap_place(r, i, r->timer_heap[parent]);
897 i = parent;
898 }
899 timer_heap_place(r, i, t);
900}
901
902static void timer_heap_down(cwist_reactor_t *r, uint32_t i) {
903 cwist_reactor_timer_t *t = r->timer_heap[i];
904 for (;;) {
905 uint32_t child = 2 * i + 1;
906 if (child >= r->timer_n) break;
907 if (child + 1 < r->timer_n &&
908 r->timer_heap[child + 1]->deadline_ns < r->timer_heap[child]->deadline_ns)
909 child++;
910 if (t->deadline_ns <= r->timer_heap[child]->deadline_ns) break;
911 timer_heap_place(r, i, r->timer_heap[child]);
912 i = child;
913 }
914 timer_heap_place(r, i, t);
915}
916
917static void timer_heap_remove(cwist_reactor_t *r, cwist_reactor_timer_t *t) {
918 uint32_t i = t->heap_slot - 1;
919 t->heap_slot = 0;
920 cwist_reactor_timer_t *last = r->timer_heap[--r->timer_n];
921 if (i == r->timer_n) return;
922 timer_heap_place(r, i, last);
923 if (i > 0 && r->timer_heap[(i - 1) / 2]->deadline_ns > last->deadline_ns)
924 timer_heap_up(r, i);
925 else
926 timer_heap_down(r, i);
927}
928
929void cwist_reactor_timer_init(cwist_reactor_timer_t *timer, void (*cb)(void *ctx), void *ctx) {
930 if (!timer) return;
931 timer->deadline_ns = 0;
932 timer->heap_slot = 0;
933 timer->cb = cb;
934 timer->ctx = ctx;
935}
936
938 if (!r || !timer || !timer->cb) return false;
939 if (timer->heap_slot) timer_heap_remove(r, timer);
940 if (r->timer_n == r->timer_cap) {
941 uint32_t cap = r->timer_cap ? r->timer_cap * 2 : 64;
942 cwist_reactor_timer_t **heap = cwist_alloc(cap * sizeof(*heap));
943 if (!heap) return false;
944 if (r->timer_n) memcpy(heap, r->timer_heap, r->timer_n * sizeof(*heap));
945 cwist_free(r->timer_heap);
946 r->timer_heap = heap;
947 r->timer_cap = cap;
948 }
949 timer->deadline_ns = reactor_now_ns() + delay_us * 1000ull;
950 r->timer_n++;
951 timer_heap_place(r, r->timer_n - 1, timer);
952 timer_heap_up(r, r->timer_n - 1);
953 return true;
954}
955
957 if (!r || !timer || !timer->heap_slot) return;
958 timer_heap_remove(r, timer);
959}
960
962 return timer && timer->heap_slot != 0;
963}
964
965/* Fire every timer whose deadline has passed. At most the timers armed on
966 * entry run, so a callback that re-arms itself with a zero delay waits for
967 * the next round instead of spinning here. */
968static void reactor_run_timers(cwist_reactor_t *r) {
969 if (r->timer_n == 0) return;
970 uint64_t now = reactor_now_ns();
971 uint32_t budget = r->timer_n;
972 while (budget-- > 0 && r->timer_n > 0 && r->timer_heap[0]->deadline_ns <= now) {
973 cwist_reactor_timer_t *t = r->timer_heap[0];
974 timer_heap_remove(r, t);
975 t->cb(t->ctx);
976 }
977}
978
979/* Poll wait for this round: the time to the earliest timer, capped at the
980 * idle wait. */
981static uint64_t reactor_wait_ns(const cwist_reactor_t *r) {
982 if (r->timer_n == 0) return REACTOR_IDLE_WAIT_NS;
983 uint64_t now = reactor_now_ns();
984 uint64_t deadline = r->timer_heap[0]->deadline_ns;
985 if (deadline <= now) return 0;
986 uint64_t wait = deadline - now;
988}
989
990static void reactor_wake_cb(int fd, void *ctx) {
991 cwist_reactor_t *r = *(cwist_reactor_t *const *)ctx;
992 uint64_t buf[8];
993 while (read(fd, buf, sizeof(buf)) > 0) {
994 }
995 reactor_drain_posts(r);
996 /* One-shot slots are recycled after firing: re-arm for the next post. */
997 if (!cwist_reactor_add(r, fd, reactor_wake_cb, &r, sizeof(r))) {
998 int wr = r->wake_wr;
999 r->wake_fd = -1;
1000 r->wake_wr = -1;
1001 close(fd);
1002 if (wr >= 0 && wr != fd) close(wr);
1003 }
1004}
1005
1008 if (!r) return NULL;
1009 memset(r, 0, sizeof(cwist_reactor_t));
1010 __atomic_store_n(&r->running, false, __ATOMIC_RELEASE);
1011 pthread_mutex_init(&r->pool_lock, NULL);
1012#ifdef __linux__
1013 pthread_mutex_init(&r->impl.sq_lock, NULL);
1014#endif
1015
1016#ifdef __linux__
1017 r->impl.use_epoll = false;
1018 /* CWIST_REACTOR_BACKEND=epoll forces the epoll path for A/B measurement;
1019 * any other value (or unset) keeps io_uring as the default. */
1020 {
1021 const char *backend = getenv("CWIST_REACTOR_BACKEND");
1022 if (backend && strcmp(backend, "epoll") == 0) r->impl.use_epoll = true;
1023 }
1024 struct io_uring_params p;
1025 memset(&p, 0, sizeof(p));
1026 int fd = r->impl.use_epoll ? -1 : sys_io_uring_setup(4096, &p);
1027 if (fd >= 0) {
1028 r->impl.ring_fd = fd;
1029 r->impl.sq_ring_sz = sq_ring_size(&p);
1030 r->impl.cq_ring_sz = cq_ring_size(&p);
1031 r->impl.sqes_sz = p.sq_entries * sizeof(struct io_uring_sqe);
1032
1033 void *sq_ptr = mmap_ring(fd, r->impl.sq_ring_sz, IORING_OFF_SQ_RING);
1034 void *cq_ptr = mmap_ring(fd, r->impl.cq_ring_sz, IORING_OFF_CQ_RING);
1035 void *sqes_ptr = mmap_ring(fd, r->impl.sqes_sz, IORING_OFF_SQES);
1036
1037 if (sq_ptr && cq_ptr && sqes_ptr) {
1038 r->impl.sq_head = (uint32_t *)((char *)sq_ptr + p.sq_off.head);
1039 r->impl.sq_tail = (uint32_t *)((char *)sq_ptr + p.sq_off.tail);
1040 r->impl.sq_ring_mask = (uint32_t *)((char *)sq_ptr + p.sq_off.ring_mask);
1041 r->impl.sq_array = (uint32_t *)((char *)sq_ptr + p.sq_off.array);
1042 r->impl.sqes = sqes_ptr;
1043 r->impl.sq_entries = p.sq_entries;
1044
1045 r->impl.cq_head = (uint32_t *)((char *)cq_ptr + p.cq_off.head);
1046 r->impl.cq_tail = (uint32_t *)((char *)cq_ptr + p.cq_off.tail);
1047 r->impl.cq_ring_mask = (uint32_t *)((char *)cq_ptr + p.cq_off.ring_mask);
1048 r->impl.cqes = (struct io_uring_cqe *)((char *)cq_ptr + p.cq_off.cqes);
1049 r->impl.cq_entries = p.cq_entries;
1050 r->impl.active = true;
1051
1052 /* Identity SQE mapping is fixed for the ring's lifetime; fill it
1053 * once here instead of rewriting sq_array on every submission. */
1054 for (uint32_t i = 0; i < p.sq_entries; i++) r->impl.sq_array[i] = i;
1055 } else {
1056 if (sq_ptr) munmap(sq_ptr, r->impl.sq_ring_sz);
1057 if (cq_ptr) munmap(cq_ptr, r->impl.cq_ring_sz);
1058 if (sqes_ptr) munmap(sqes_ptr, r->impl.sqes_sz);
1059 close(fd);
1060 r->impl.use_epoll = true;
1061 }
1062 } else {
1063 r->impl.use_epoll = true;
1064 }
1065
1066 if (r->impl.use_epoll) {
1067 r->impl.epoll_fd = epoll_create1(0);
1068 if (r->impl.epoll_fd < 0) {
1069 pthread_mutex_destroy(&r->impl.sq_lock);
1070 pthread_mutex_destroy(&r->pool_lock);
1071 cwist_free(r);
1072 return NULL;
1073 }
1074 }
1075#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
1076 r->impl.kq_fd = kqueue();
1077 if (r->impl.kq_fd < 0) {
1078 pthread_mutex_destroy(&r->pool_lock);
1079 cwist_free(r);
1080 return NULL;
1081 }
1082#endif
1083 atomic_init(&r->post_head, NULL);
1084#ifdef __linux__
1085 r->wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
1086 r->wake_wr = r->wake_fd;
1087#else
1088 r->wake_fd = -1;
1089 r->wake_wr = -1;
1090 {
1091 int pfds[2];
1092 if (pipe(pfds) == 0) {
1093 for (int i = 0; i < 2; i++) {
1094 int fl = fcntl(pfds[i], F_GETFL, 0);
1095 if (fl >= 0) fcntl(pfds[i], F_SETFL, fl | O_NONBLOCK);
1096 }
1097 r->wake_fd = pfds[0];
1098 r->wake_wr = pfds[1];
1099 }
1100 }
1101#endif
1102 /* Poll the wake fd on every backend. IORING_REGISTER_EVENTFD notifies
1103 * an external eventfd when CQEs arrive; writes to it do not create CQEs
1104 * or interrupt io_uring_enter. The one-shot poll is re-armed by
1105 * reactor_wake_cb, including when posts arrive during a dispatch. */
1106 if (r->wake_fd >= 0 && !cwist_reactor_add(r, r->wake_fd, reactor_wake_cb, &r, sizeof(r))) {
1107 /* Wake best-effort: the run loop still drains the stack each round. */
1108 close(r->wake_fd);
1109 if (r->wake_wr != r->wake_fd) close(r->wake_wr);
1110 r->wake_fd = -1;
1111 r->wake_wr = -1;
1112 }
1113 return r;
1114}
1115
1117 if (!reactor) return;
1118#ifdef __linux__
1119 latency_probe_dump(reactor);
1120#endif
1121 /* Run any completions posted after the run thread parked for good. */
1122 reactor_drain_posts(reactor);
1123 if (reactor->wake_fd >= 0) close(reactor->wake_fd);
1124 if (reactor->wake_wr >= 0 && reactor->wake_wr != reactor->wake_fd) close(reactor->wake_wr);
1125#ifdef __linux__
1126 if (!reactor->impl.use_epoll) {
1127 /* Teardown absorbed from io_uring_backend.c: unmap all three rings. */
1128 if (reactor->impl.sqes) munmap(reactor->impl.sqes, reactor->impl.sqes_sz);
1129 if (reactor->impl.cqes) {
1130 munmap((char *)reactor->impl.cqes -
1131 (reactor->impl.cq_ring_sz -
1132 reactor->impl.cq_entries * sizeof(struct io_uring_cqe)),
1133 reactor->impl.cq_ring_sz);
1134 }
1135 if (reactor->impl.sq_array) {
1136 munmap((char *)reactor->impl.sq_array -
1137 (reactor->impl.sq_ring_sz - reactor->impl.sq_entries * sizeof(uint32_t)),
1138 reactor->impl.sq_ring_sz);
1139 }
1140 close(reactor->impl.ring_fd);
1141 } else {
1142 close(reactor->impl.epoll_fd);
1143 cwist_free(reactor->rx_ready);
1144 }
1145#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
1146 close(reactor->impl.kq_fd);
1147#endif
1148 pthread_mutex_destroy(&reactor->pool_lock);
1149#ifdef __linux__
1150 pthread_mutex_destroy(&reactor->impl.sq_lock);
1151 /* Multishot accepts terminated with the ring; free their registrations. */
1152 reactor_accept_slot_t *as = reactor->accept_slots;
1153 while (as) {
1154 reactor_accept_slot_t *next = as->next;
1155 cwist_free(as);
1156 as = next;
1157 }
1158#endif
1159 cwist_free(reactor->timer_heap);
1160 reactor_slot_chunk_t *chunk = reactor->chunks;
1161 while (chunk) {
1162 reactor_slot_chunk_t *next = chunk->next;
1164 chunk = next;
1165 }
1166 cwist_free(reactor);
1167}
1168
1169#ifdef __linux__
1170/* Submit one SQE immediately: the reactor never batches or defers submission,
1171 * so a woken worker always sees the event on its next wait. */
1172static bool uring_submit(cwist_reactor_t *reactor, struct io_uring_sqe *out_sqe) {
1173 bool ok = false;
1174 pthread_mutex_lock(&reactor->impl.sq_lock);
1175 uint32_t tail = *reactor->impl.sq_tail;
1176 uint32_t head = __atomic_load_n(reactor->impl.sq_head, __ATOMIC_ACQUIRE);
1177 if (tail - head < reactor->impl.sq_entries) {
1178 uint32_t index = tail & *reactor->impl.sq_ring_mask;
1179 struct io_uring_sqe *sqe = &reactor->impl.sqes[index];
1180 memcpy(sqe, out_sqe, sizeof(*sqe));
1181 reactor_tsan_release(&reactor->impl);
1182 __atomic_store_n(reactor->impl.sq_tail, tail + 1, __ATOMIC_RELEASE);
1183 if (sys_io_uring_enter(reactor->impl.ring_fd, 1, 0, 0, NULL) < 0) {
1184 if (cwist_async_debug_enabled()) {
1185 static _Atomic long dbg_enter_fail;
1186 long n = atomic_fetch_add(&dbg_enter_fail, 1) + 1;
1187 if (n <= 5 || n % 10000 == 0)
1189 "[reactor] uring enter failed fd=%d errno=%d(%s) sq=%u/%u total=%ld\n",
1190 out_sqe->fd, errno, strerror(errno), tail - head,
1191 reactor->impl.sq_entries, n);
1192 }
1193 __atomic_store_n(reactor->impl.sq_tail, tail, __ATOMIC_RELEASE);
1194 } else {
1195 ok = true;
1196 }
1197 } else if (cwist_async_debug_enabled()) {
1198 static _Atomic long dbg_sq_full;
1199 long n = atomic_fetch_add(&dbg_sq_full, 1) + 1;
1200 if (n <= 5 || n % 10000 == 0)
1201 fprintf(stderr, "[reactor] SQ full fd=%d depth=%u/%u total=%ld\n", out_sqe->fd,
1202 tail - head, reactor->impl.sq_entries, n);
1203 }
1204 pthread_mutex_unlock(&reactor->impl.sq_lock);
1205 return ok;
1206}
1207#endif
1208
1209#ifdef __linux__
1210/* Flush the deferred SQE queue with a single io_uring_enter for the whole
1211 * batch. On batch failure (SQ momentarily full from concurrent cross-thread
1212 * submissions) fall back to per-SQE submits with a short retry; a final
1213 * failure closes the connection and recycles its slot, matching every
1214 * caller's add-failure path. */
1215static bool uring_submit_batch(cwist_reactor_t *reactor, struct io_uring_sqe *batch, uint32_t n) {
1216 bool ok = false;
1217 pthread_mutex_lock(&reactor->impl.sq_lock);
1218 uint32_t tail = *reactor->impl.sq_tail;
1219 uint32_t head = __atomic_load_n(reactor->impl.sq_head, __ATOMIC_ACQUIRE);
1220 if (tail - head + n <= reactor->impl.sq_entries) {
1221 for (uint32_t i = 0; i < n; i++) {
1222 uint32_t index = (tail + i) & *reactor->impl.sq_ring_mask;
1223 memcpy(&reactor->impl.sqes[index], &batch[i], sizeof(batch[i]));
1224 }
1225 reactor_tsan_release(&reactor->impl);
1226 __atomic_store_n(reactor->impl.sq_tail, tail + n, __ATOMIC_RELEASE);
1227 if (sys_io_uring_enter(reactor->impl.ring_fd, n, 0, 0, NULL) >= 0) {
1228 ok = true;
1229 } else {
1230 __atomic_store_n(reactor->impl.sq_tail, tail, __ATOMIC_RELEASE);
1231 }
1232 }
1233 pthread_mutex_unlock(&reactor->impl.sq_lock);
1234 return ok;
1235}
1236
1237static void flush_deferred(cwist_reactor_t *reactor) {
1238 uint32_t n = reactor->deferred_n;
1239 reactor->deferred_n = 0;
1240 if (n == 0) return;
1241 if (uring_submit_batch(reactor, reactor->deferred_sqes, n)) return;
1242 for (uint32_t i = 0; i < n; i++) {
1243 struct io_uring_sqe *sqe = &reactor->deferred_sqes[i];
1244 reactor_event_ctx_t *ev_ctx = reactor->deferred_ctxs[i];
1245 int attempt;
1246 for (attempt = 0; attempt < 100; attempt++) {
1247 if (uring_submit(reactor, sqe)) break;
1248 struct timespec ts = {.tv_sec = 0, .tv_nsec = 1000 * 1000};
1249 nanosleep(&ts, NULL); /* SQ drains without our help; wait it out */
1250 }
1251 if (attempt == 100) {
1252 if (cwist_async_debug_enabled()) {
1253 fprintf(stderr, "[reactor] deferred submit failed fd=%d; closing\n", (int)sqe->fd);
1254 }
1255 if (ev_ctx) {
1256 close((int)sqe->fd);
1257 free_reactor_ctx(reactor, ev_ctx);
1258 } else if (reactor->rx_cb) {
1259 /* Tagged RECV SQE: the connection owns the fd and shell, so
1260 * report the cancellation and let its handler clean up. */
1261 reactor->rx_cb((void *)(sqe->user_data & ~(uint64_t)1u), -ECANCELED);
1262 }
1263 }
1264 }
1265}
1266
1267/* Copy the deferred SQEs into the SQ without an io_uring_enter: the run
1268 * loop's next wait enter carries to_submit, so a dispatch round costs one
1269 * enter total instead of wait-enter + flush-enter. On SQ contention fall
1270 * back to the immediate flush so re-arms never stall. */
1271static void queue_deferred(cwist_reactor_t *reactor) {
1272 uint32_t n = reactor->deferred_n;
1273 if (n == 0) return;
1274 pthread_mutex_lock(&reactor->impl.sq_lock);
1275 uint32_t tail = *reactor->impl.sq_tail;
1276 uint32_t head = __atomic_load_n(reactor->impl.sq_head, __ATOMIC_ACQUIRE);
1277 if (tail - head + n <= reactor->impl.sq_entries) {
1278 for (uint32_t i = 0; i < n; i++) {
1279 uint32_t index = (tail + i) & *reactor->impl.sq_ring_mask;
1280 memcpy(&reactor->impl.sqes[index], &reactor->deferred_sqes[i],
1281 sizeof(reactor->deferred_sqes[i]));
1282 }
1283 reactor_tsan_release(&reactor->impl);
1284 __atomic_store_n(reactor->impl.sq_tail, tail + n, __ATOMIC_RELEASE);
1285 reactor->deferred_n = 0;
1286 reactor->sq_unsubmitted += n;
1287 }
1288 pthread_mutex_unlock(&reactor->impl.sq_lock);
1289 if (reactor->deferred_n) flush_deferred(reactor);
1290}
1291#endif
1292
1293static bool reactor_add_common(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb,
1294 const void *payload, size_t payload_size, bool for_write) {
1295 if (!reactor || fd < 0) return false;
1296 reactor_event_ctx_t *ev_ctx = alloc_reactor_ctx(reactor, fd, cb, payload, payload_size);
1297 if (!ev_ctx) return false;
1298
1299#ifdef __linux__
1300 if (latency_probe_enabled()) {
1301 struct timespec ts;
1303 ev_ctx->armed_ns = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
1304 }
1305 if (!reactor->impl.use_epoll) {
1306 /* One-shot POLL_ADD: multishot (IORING_POLL_ADD_MULTI) was rejected
1307 * because the slot is recycled after firing, so a persistent poll
1308 * would re-dispatch into a recycled slot. */
1309 struct io_uring_sqe sqe;
1310 memset(&sqe, 0, sizeof(sqe));
1311 sqe.opcode = IORING_OP_POLL_ADD;
1312 sqe.fd = fd;
1313 sqe.poll_events = for_write ? POLLOUT : POLLIN;
1314 sqe.user_data = (uint64_t)ev_ctx;
1315 /* Run-thread re-arms defer to the batch flush; everything else
1316 * submits immediately so remote workers wake from their wait. */
1317 if (reactor_on_run_thread(reactor) && reactor->dispatching &&
1318 reactor->deferred_n <
1319 (uint32_t)(sizeof(reactor->deferred_sqes) / sizeof(reactor->deferred_sqes[0]))) {
1320 uint32_t slot = reactor->deferred_n++;
1321 reactor->deferred_sqes[slot] = sqe;
1322 reactor->deferred_ctxs[slot] = ev_ctx;
1323 return true;
1324 }
1325 if (uring_submit(reactor, &sqe)) {
1326 return true;
1327 }
1328 free_reactor_ctx(reactor, ev_ctx);
1329 return false;
1330 } else {
1331 struct epoll_event ev;
1332 ev.events = (for_write ? EPOLLOUT : EPOLLIN) | EPOLLET | EPOLLONESHOT;
1333 ev.data.ptr = ev_ctx;
1334 if (epoll_register(reactor->impl.epoll_fd, fd, &ev)) {
1335 /* Also takes the fd out of RX mode: the one-shot registration
1336 * replaced the edge-triggered one. */
1338 return true;
1339 }
1340 free_reactor_ctx(reactor, ev_ctx);
1341 return false;
1342 }
1343#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
1344 struct kevent change;
1346 0, ev_ctx);
1347 if (kevent(reactor->impl.kq_fd, &change, 1, NULL, 0, NULL) == 0) {
1348 return true;
1349 }
1350 free_reactor_ctx(reactor, ev_ctx);
1351 return false;
1352#endif
1353 free_reactor_ctx(reactor, ev_ctx);
1354 return false;
1355}
1356
1357bool cwist_reactor_add(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload,
1358 size_t payload_size) {
1359 return reactor_add_common(reactor, fd, cb, payload, payload_size, false);
1360}
1361
1363 const void *payload, size_t payload_size) {
1364 return reactor_add_common(reactor, fd, cb, payload, payload_size, true);
1365}
1366
1367bool cwist_reactor_mod(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload,
1368 size_t payload_size) {
1369 if (!reactor || fd < 0) return false;
1370 reactor_event_ctx_t *ev_ctx = alloc_reactor_ctx(reactor, fd, cb, payload, payload_size);
1371 if (!ev_ctx) return false;
1372
1373#ifdef __linux__
1374 if (!reactor->impl.use_epoll) {
1375 struct io_uring_sqe sqe;
1376 memset(&sqe, 0, sizeof(sqe));
1377 sqe.opcode = IORING_OP_POLL_ADD;
1378 sqe.fd = fd;
1379 sqe.poll_events = POLLIN;
1380 sqe.user_data = (uint64_t)ev_ctx;
1381 if (uring_submit(reactor, &sqe)) {
1382 return true;
1383 }
1384 free_reactor_ctx(reactor, ev_ctx);
1385 return false;
1386 } else {
1387 struct epoll_event ev;
1388 ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
1389 ev.data.ptr = ev_ctx;
1390 if (epoll_ctl(reactor->impl.epoll_fd, EPOLL_CTL_MOD, fd, &ev) == 0) {
1392 return true;
1393 }
1394 free_reactor_ctx(reactor, ev_ctx);
1395 return false;
1396 }
1397#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
1398 struct kevent change;
1400 if (kevent(reactor->impl.kq_fd, &change, 1, NULL, 0, NULL) == 0) {
1401 return true;
1402 }
1403 free_reactor_ctx(reactor, ev_ctx);
1404 return false;
1405#endif
1406 free_reactor_ctx(reactor, ev_ctx);
1407 return false;
1408}
1409
1410bool cwist_reactor_del(cwist_reactor_t *reactor, int fd) {
1411 if (!reactor || fd < 0) return false;
1412
1413#ifdef __linux__
1414 if (!reactor->impl.use_epoll) {
1415 struct io_uring_sqe sqe;
1416 memset(&sqe, 0, sizeof(sqe));
1417 sqe.opcode = IORING_OP_ASYNC_CANCEL;
1418 sqe.addr = (unsigned long)fd;
1420 sqe.user_data = 0;
1421 return uring_submit(reactor, &sqe);
1422 } else {
1423 struct epoll_event ev;
1425 return epoll_ctl(reactor->impl.epoll_fd, EPOLL_CTL_DEL, fd, &ev) == 0;
1426 }
1427#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
1428 struct kevent change;
1429 EV_SET(&change, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
1430 return kevent(reactor->impl.kq_fd, &change, 1, NULL, 0, NULL) == 0;
1431#else
1432 return false;
1433#endif
1434}
1435
1436/* CWIST_REACTOR_DRAIN_CHUNK: cooperative-queuing knob (see the comments at
1437 * its call sites in cwist_reactor_run()). Values > 0 bound how many
1438 * connection callbacks run before foreign-thread posts are drained, on
1439 * every backend (io_uring CQE batch, epoll event array, kqueue event
1440 * array). 0 disables the mid-batch drain (legacy behavior). Unset or
1441 * invalid defaults to 64 so a foreign-thread completion does not queue
1442 * behind a full batch by default. Cached after the first read like the
1443 * other env knobs in this file -- the racy recompute is benign (same result
1444 * every time). */
1445static uint32_t reactor_drain_chunk(void) {
1446 static _Atomic int cached = -1;
1448 if (v < 0) {
1449 const char *s = getenv("CWIST_REACTOR_DRAIN_CHUNK");
1450 if (s) {
1451 long parsed = strtol(s, NULL, 10);
1452 v = (parsed >= 0 && parsed < INT_MAX) ? (int)parsed : 64;
1453 } else {
1454 v = 64;
1455 }
1457 }
1458 return (uint32_t)v;
1459}
1460
1461#ifdef __linux__
1462/* CQ grace: how long a reactor loop peeks its completion ring (pure
1463 * userspace, no syscall) before committing to the kernel wait. Completions
1464 * that land while a dispatch batch is being processed -- the common shape
1465 * under keep-alive load, where the peer's next request often arrives within
1466 * microseconds of the response -- would otherwise force a fresh wake each;
1467 * the grace folds them into the current one. 0 disables the spin.
1468 * CWIST_REACTOR_CQ_GRACE_NS overrides the 20 us default. */
1469static uint64_t reactor_cq_grace_ns(void) {
1470 static _Atomic long cached = -1;
1472 if (v < 0) {
1473 const char *s = getenv("CWIST_REACTOR_CQ_GRACE_NS");
1474 if (s && *s) {
1475 char *end = NULL;
1476 long parsed = strtol(s, &end, 10);
1477 v = (end != s && parsed >= 0 && parsed < 1000000000L) ? parsed : 20000;
1478 } else {
1479 v = 20000;
1480 }
1482 }
1483 return (uint64_t)v;
1484}
1485
1486/* Round budget: an io_uring dispatch round drains every CQE in the ring, and
1487 * a busy round can hold hundreds of connections' callbacks. Callbacks re-arm
1488 * their connections into the deferred SQE array, which only reaches the
1489 * kernel when the round ends -- so a request that arrived right after its
1490 * connection was re-armed waits out the whole batch, which is where the
1491 * arm-to-dispatch tail comes from (issue #166). The budget bounds that wait
1492 * without breaking the round: past it, parked re-arms are flushed to the
1493 * kernel at the next check instead of at round end (one extra enter per
1494 * check, only while a round is long). The round still runs to completion,
1495 * so the stop-at-round-boundary contract is unchanged. The check rides its
1496 * own cadence (every 64 callbacks), independent of CWIST_REACTOR_DRAIN_CHUNK.
1497 * 0 disables the bound. CWIST_REACTOR_ROUND_BUDGET_US overrides the 2000 us
1498 * default. */
1499static uint64_t reactor_round_budget_us(void) {
1500 static _Atomic long cached = -1;
1502 if (v < 0) {
1503 const char *s = getenv("CWIST_REACTOR_ROUND_BUDGET_US");
1504 if (s && *s) {
1505 char *end = NULL;
1506 long parsed = strtol(s, &end, 10);
1507 v = (end != s && parsed >= 0 && parsed < 3600000000L) ? parsed : 2000;
1508 } else {
1509 v = 2000;
1510 }
1512 }
1513 return (uint64_t)v;
1514}
1515
1516/* Idle malloc trim (measured ~1.2 MiB median per worker returned to the OS on
1517 * an idle HTTPS worker after a load round): glibc never gives back the free
1518 * top of the main arena unless asked. CWIST_MALLOC_TRIM_IDLE_SEC bounds how
1519 * many seconds of cumulative event-free idleness a reactor may accumulate
1520 * before it calls malloc_trim(0) on a wake that saw zero events -- a mostly
1521 * idle deployment sheds arena top memory without any hot-path cost. The knob
1522 * is cached after the first read like the other env knobs in this file; 0
1523 * disables the trim. */
1525 static _Atomic int cached = -1;
1527 if (v < 0) {
1528 const char *s = getenv("CWIST_MALLOC_TRIM_IDLE_SEC");
1529 if (s && *s) {
1530 char *end = NULL;
1531 long parsed = strtol(s, &end, 10);
1532 v = (end != s && parsed >= 0 && parsed < 86400) ? (int)parsed : 30;
1533 } else {
1534 v = 30;
1535 }
1537 }
1538 return (uint32_t)v;
1539}
1540
1541/* Weak: musl and other non-glibc Linux libcs have no malloc_trim. */
1542extern int malloc_trim(size_t __pad) __attribute__((weak));
1543
1544/* A wake that dispatched events (or ran posted work) ends the idle streak. */
1545static void reactor_idle_note_eventful(cwist_reactor_t *r) { r->idle_since_ns = 0; }
1546
1547/* Bookkeeping for a wake that saw zero events. Starts an idle streak on the
1548 * first such wake; once the streak has lasted CWIST_MALLOC_TRIM_IDLE_SEC
1549 * cumulative seconds, returns the arena top to the OS and re-anchors the
1550 * streak. Runs on the run thread only, after the wait returned empty. */
1553 if (sec == 0 || &malloc_trim == NULL) {
1554 r->idle_since_ns = 0;
1555 return;
1556 }
1557 struct timespec ts;
1559 uint64_t now = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
1560 if (r->idle_since_ns == 0) {
1561 r->idle_since_ns = now;
1562 return;
1563 }
1564 if (now - r->idle_since_ns >= (uint64_t)sec * 1000000000ull) {
1565 malloc_trim(0);
1566 r->idle_since_ns = now;
1567 }
1568}
1569
1570#if defined(__x86_64__) || defined(__i386__)
1571#define reactor_cpu_relax() __builtin_ia32_pause()
1572#elif defined(__aarch64__)
1573#define reactor_cpu_relax() __asm__ __volatile__("yield")
1574#else
1575#define reactor_cpu_relax() ((void)0)
1576#endif
1577#endif
1578
1580 if (!reactor) return;
1581 __atomic_store_n(&reactor->running, true, __ATOMIC_RELEASE);
1582 t_run_reactor = reactor;
1583
1584#ifdef __linux__
1585 if (!reactor->impl.use_epoll) {
1587 while (reactor_running(reactor)) {
1588 reactor_drain_posts(reactor);
1589 reactor_run_timers(reactor);
1590 /* Submit the re-arms queued by the previous dispatch batch under
1591 * the SQ lock, then wait in a separate call. The submit enter
1592 * MUST hold the lock: uring_submit/uring_submit_batch roll the
1593 * SQ tail back when their own enter fails, and a concurrent
1594 * unlocked enter here could consume that SQE first, leaving
1595 * sq_tail behind sq_head. tail < head reads as a permanently
1596 * full SQ to every later submit (tail - head underflows past
1597 * sq_entries), so the reactor goes deaf: no re-arm, no accept,
1598 * no completions - the server stops answering while every thread
1599 * looks idle. With all SQ consumers under the lock, a failed
1600 * enter means the SQE was definitely not consumed and the
1601 * rollback is exact.
1602 *
1603 * The wait is bounded (like the epoll path's 100 ms poll)
1604 * because the shutdown handler runs with SA_RESTART: an
1605 * unbounded enter would be restarted after SIGTERM and hang an
1606 * idle reactor forever. It passes to_submit=0, so it consumes
1607 * nothing and needs no lock. */
1608 /* CQ grace: completions that land while the previous batch is
1609 * dispatching would otherwise force a fresh wake. After the
1610 * locked submit enter below, peek the CQ ring from userspace for
1611 * a bounded window; completions caught in time are dispatched in
1612 * the current round and the wait enter is skipped. Re-arms are
1613 * safe either way: the submit enter above already flushed them.
1614 * The deadline bounds the spin; on expiry (or when disabled) the
1615 * code falls through to the bounded wait below. */
1616 bool cq_pending = false;
1617 if (cq_grace_ns > 0) {
1618 uint32_t ph =
1619 __atomic_load_n(reactor->impl.cq_head, __ATOMIC_ACQUIRE);
1620 if (ph == *reactor->impl.cq_tail) {
1621 struct timespec gs;
1623 uint64_t g_end = (uint64_t)gs.tv_sec * 1000000000ull +
1624 (uint64_t)gs.tv_nsec + cq_grace_ns;
1625 uint32_t spins = 0;
1626 while (ph == *reactor->impl.cq_tail) {
1628 if (((++spins) & 31u) == 0) {
1630 uint64_t now = (uint64_t)gs.tv_sec * 1000000000ull +
1631 (uint64_t)gs.tv_nsec;
1632 if (now >= g_end) break;
1633 }
1634 ph = __atomic_load_n(reactor->impl.cq_head, __ATOMIC_ACQUIRE);
1635 }
1636 cq_pending = (ph != *reactor->impl.cq_tail);
1637 }
1638 }
1639
1640 /* Bounded by the idle wait, shortened to the next timer. */
1641 const uint64_t wait_ns = reactor_wait_ns(reactor);
1642 const struct __kernel_timespec idle_ts = {
1643 .tv_sec = (long long)(wait_ns / 1000000000ull),
1644 .tv_nsec = (long long)(wait_ns % 1000000000ull)};
1645 uint32_t to_submit = reactor->sq_unsubmitted;
1646 reactor->sq_unsubmitted = 0;
1647 if (to_submit > 0) {
1648 pthread_mutex_lock(&reactor->impl.sq_lock);
1649 int sret = sys_io_uring_enter(reactor->impl.ring_fd, to_submit, 0, 0, NULL);
1650 pthread_mutex_unlock(&reactor->impl.sq_lock);
1651 if (sret < 0) {
1652 /* Submission state is unknown on EINTR; anything else
1653 * failed before consuming. Either way the SQEs are
1654 * still in the SQ (the kernel caps resubmission at what
1655 * is actually there), so restore the count and let the
1656 * next round retry. */
1657 reactor->sq_unsubmitted += to_submit;
1658 }
1659 }
1660 if (!cq_pending) {
1661 int ret = sys_io_uring_enter_timeout(reactor->impl.ring_fd, 0, 1,
1663 /* EINTR and ETIME both leave the CQ untouched, so the
1664 * head/tail check below still decides eventfulness. */
1665 if (ret < 0 && errno != EINTR && errno != ETIME) break;
1666 }
1667 uint32_t head = __atomic_load_n(reactor->impl.cq_head, __ATOMIC_ACQUIRE);
1668 uint32_t tail = *reactor->impl.cq_tail;
1669 if (head == tail) {
1670 /* Zero-completion wake: pure idle. Feeds the idle malloc_trim
1671 * cadence; see reactor_idle_note_wake(). */
1672 reactor_idle_note_wake(reactor);
1673 continue;
1674 }
1676 reactor_tsan_acquire(&reactor->impl);
1677 reactor->dispatching = true;
1680 if (round_budget_us) {
1681 struct timespec ts;
1683 round_start_ns = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
1684 }
1685 /* Cooperative queuing: a busy round can carry hundreds of ready
1686 * connections in one CQE batch (the ring is 4096 deep). Foreign-
1687 * thread completions (cwist_async_defer, background jobs) queue
1688 * onto post_head via cwist_reactor_post() and used to wait for
1689 * reactor_drain_posts() at the *top* of the next round -- i.e.
1690 * behind this entire batch, even if the post arrived while we
1691 * were only a few callbacks in. CWIST_REACTOR_DRAIN_CHUNK bounds
1692 * how many connection callbacks run before posts are drained, so
1693 * a foreign-thread completion's own tail latency stops scaling
1694 * with how many *other* connections happened to be ready in the
1695 * same wake. The default (64) applies the bound out of the box;
1696 * 0 restores the legacy single-drain-at-end behavior. */
1697 uint32_t drain_chunk = reactor_drain_chunk();
1700 while (head != tail) {
1701 struct io_uring_cqe *cqe = &reactor->impl.cqes[head & *reactor->impl.cq_ring_mask];
1702 uint64_t user_data = cqe->user_data;
1703 if (user_data & 1u) {
1704 /* Tagged data-path SQE (RX-uring RECV): the connection
1705 * pointer rides in the remaining bits. Dispatch on any
1706 * res, including negative ones: -EAGAIN is the POLL
1707 * fallback signal and 0 is EOF. Latency probe samples
1708 * are recorded by the handler itself (it owns the arm
1709 * timestamp), not by the ev_ctx wrapper below. */
1710 if (reactor->rx_cb) {
1711 reactor->rx_cb((void *)(user_data & ~(uint64_t)1u), cqe->res);
1712 }
1713 } else if (user_data & 2u) {
1714 /* Multishot-accept slot (issue #406): cqe->res is the
1715 * accepted fd, already CLOEXEC|NONBLOCK. A completion
1716 * without IORING_CQE_F_MORE ends the registration: notify
1717 * once with the terminal status (-errno, or -ECANCELED on
1718 * an error-free end) so the caller can resume the
1719 * one-shot POLL accept loop, then free the slot. */
1721 (reactor_accept_slot_t *)(uintptr_t)(user_data & ~(uint64_t)3u);
1722 int res = cqe->res;
1723 if (res >= 0) slot->cb(slot->ctx, res);
1724 if (!(cqe->flags & IORING_CQE_F_MORE)) {
1725 reactor_accept_unlink(reactor, slot);
1726 slot->cb(slot->ctx, res < 0 ? res : -ECANCELED);
1728 }
1729 } else {
1730 reactor_event_ctx_t *ev_ctx = (reactor_event_ctx_t *)user_data;
1731 if (ev_ctx) {
1732 if (cqe->res >= 0) {
1733 if (latency_probe_enabled()) {
1734 struct timespec ts;
1736 uint64_t t0 =
1737 (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
1738 /* armed_ns == 0 means the slot was armed before
1739 * the probe was enabled; fold it into the first
1740 * bucket rather than producing a garbage delay. */
1741 uint64_t delay_ns = ev_ctx->armed_ns ? t0 - ev_ctx->armed_ns : 0;
1742 ev_ctx->cb(ev_ctx->fd, ev_ctx->ctx);
1744 uint64_t t1 =
1745 (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
1746 latency_probe_record(reactor->probe[LATENCY_PROBE_QUEUE].buckets,
1747 &reactor->probe[LATENCY_PROBE_QUEUE].count,
1748 &reactor->probe[LATENCY_PROBE_QUEUE].sum_us,
1749 &reactor->probe[LATENCY_PROBE_QUEUE].max_us,
1750 &reactor->probe[LATENCY_PROBE_QUEUE].over_5ms,
1751 delay_ns / 1000);
1752 latency_probe_record(reactor->probe[LATENCY_PROBE_SVC].buckets,
1753 &reactor->probe[LATENCY_PROBE_SVC].count,
1754 &reactor->probe[LATENCY_PROBE_SVC].sum_us,
1755 &reactor->probe[LATENCY_PROBE_SVC].max_us,
1756 &reactor->probe[LATENCY_PROBE_SVC].over_5ms,
1757 (t1 - t0) / 1000);
1758 } else {
1759 ev_ctx->cb(ev_ctx->fd, ev_ctx->ctx);
1760 }
1761 }
1762 free_reactor_ctx(reactor, ev_ctx);
1763 }
1764 }
1765 head++;
1766 if (drain_chunk && ++since_drain >= drain_chunk && head != tail) {
1767 since_drain = 0;
1768 __atomic_store_n(reactor->impl.cq_head, head, __ATOMIC_RELEASE);
1769 reactor_drain_posts(reactor);
1770 }
1771 if (round_budget_us && head != tail &&
1772 (++since_budget_check & 63u) == 0) {
1773 /* The budget check rides its own cadence (every 64
1774 * callbacks), independent of CWIST_REACTOR_DRAIN_CHUNK,
1775 * so disabling the post drain does not disable the
1776 * round bound. */
1777 struct timespec ts;
1779 uint64_t now_ns =
1780 (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
1781 if (now_ns - round_start_ns >= round_budget_us * 1000ull) {
1782 /* Long round: flush the re-arms parked so far instead
1783 * of holding them to round end. The batch itself
1784 * still runs to completion. */
1785 flush_deferred(reactor);
1786 }
1787 }
1788 }
1789 reactor->dispatching = false;
1790 __atomic_store_n(reactor->impl.cq_head, head, __ATOMIC_RELEASE);
1791 queue_deferred(reactor);
1792 }
1793 } else {
1794 struct epoll_event events[1024];
1795 while (reactor_running(reactor)) {
1796 reactor_drain_posts(reactor);
1797 reactor_run_timers(reactor);
1798 /* Round to the nearest millisecond, with a 1 ms minimum so a
1799 * sub-millisecond remainder never degenerates into a busy poll:
1800 * the remainder is deliberately absorbed by that final 1 ms wait
1801 * instead of rounding every wait up (which let a timer land up to
1802 * ~1 ms late). An early epoll_wait return cannot dispatch a timer
1803 * early: reactor_run_timers() re-checks each deadline against the
1804 * clock, and the only armed timers (webrtc conn/SCTP ticks, see
1805 * src/net/webrtc/webrtc.c) re-check their own expiry or are
1806 * harmless when ticked slightly early. An already-due timer waits
1807 * at most this 1 ms before firing. */
1808 int wait_ms = (int)((reactor_wait_ns(reactor) + 500000ull) / 1000000ull);
1809 if (wait_ms < 1) wait_ms = 1;
1810 /* Queued RX receives must not wait behind a blocking poll. */
1811 if (reactor->rx_ready_n > 0) wait_ms = 0;
1812 int n = epoll_wait(reactor->impl.epoll_fd, events, 1024, wait_ms);
1813 if (n < 0) {
1814 if (errno == EINTR) continue;
1815 break;
1816 }
1817 if (n == 0) {
1818 /* Event-free idle wake: feeds the idle malloc_trim cadence;
1819 * see reactor_idle_note_wake(). */
1820 reactor_idle_note_wake(reactor);
1821 } else {
1823 }
1824 /* Cooperative queuing, same cadence as the io_uring branch (see
1825 * the comment there): epoll_wait hands back up to 1024 events in
1826 * one array, and this loop used to run them all with no drain
1827 * point in between -- so a foreign-thread completion posted via
1828 * cwist_reactor_post() mid-batch waited behind every remaining
1829 * event's handler, even though the post itself costs nothing.
1830 * CWIST_REACTOR_DRAIN_CHUNK bounds that wait to ~chunk callbacks
1831 * so a foreign-thread completion's tail latency stops scaling
1832 * with the same-wake batch size. 0 restores the legacy
1833 * single-drain-at-top-of-round behavior. Not drained after the
1834 * last event: the top-of-round drain covers it, mirroring the
1835 * io_uring branch's head != tail guard. */
1836 uint32_t drain_chunk = reactor_drain_chunk();
1838 for (int i = 0; i < n; i++) {
1839 /* Low bit set: an RX-mode fd (fd << 1 | 1); ev_ctx slots are
1840 * at least 8-byte aligned and never carry it. */
1841 if (events[i].data.u64 & 1u) {
1842 rx_epoll_event(reactor, (int)(events[i].data.u64 >> 1));
1843 if (drain_chunk && ++since_drain >= drain_chunk && i + 1 < n) {
1844 since_drain = 0;
1845 reactor_drain_posts(reactor);
1846 }
1847 continue;
1848 }
1849 reactor_event_ctx_t *ev_ctx = (reactor_event_ctx_t *)events[i].data.ptr;
1850 if (ev_ctx) {
1851 ev_ctx->cb(ev_ctx->fd, ev_ctx->ctx);
1852 free_reactor_ctx(reactor, ev_ctx);
1853 if (drain_chunk && ++since_drain >= drain_chunk && i + 1 < n) {
1854 since_drain = 0;
1855 reactor_drain_posts(reactor);
1856 }
1857 }
1858 }
1859 if (reactor->rx_ready_n > 0) rx_epoll_drain_ready(reactor);
1860 }
1861 }
1862#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
1863 struct kevent events[1024];
1864 /* Bounded like the epoll and io_uring waits: a shutdown requested
1865 * without a signal (cwist_shutdown_request() from another thread) only
1866 * clears g_cwist_running and closes the listen socket, which wakes
1867 * no kevent, so the flag must be re-checked periodically. */
1868 while (reactor_running(reactor)) {
1869 reactor_drain_posts(reactor);
1870 reactor_run_timers(reactor);
1871 const uint64_t wait_ns = reactor_wait_ns(reactor);
1872 const struct timespec idle_ts = {.tv_sec = (time_t)(wait_ns / 1000000000ull),
1873 .tv_nsec = (long)(wait_ns % 1000000000ull)};
1874 int n = kevent(reactor->impl.kq_fd, NULL, 0, events, 1024, &idle_ts);
1875 if (n < 0) {
1876 if (errno == EINTR) continue;
1877 break;
1878 }
1879 /* Cooperative queuing, same cadence as the io_uring and epoll
1880 * branches: kevent hands back up to 1024 events in one array and a
1881 * foreign-thread completion posted via cwist_reactor_post() mid-batch
1882 * must not wait behind every remaining event's handler.
1883 * CWIST_REACTOR_DRAIN_CHUNK bounds that wait to ~chunk callbacks; 0
1884 * restores the legacy single-drain-at-top-of-round behavior. Not
1885 * drained after the last event: the top-of-round drain covers it. */
1886 uint32_t drain_chunk = reactor_drain_chunk();
1888 for (int i = 0; i < n; i++) {
1889 reactor_event_ctx_t *ev_ctx = (reactor_event_ctx_t *)events[i].udata;
1890 if (ev_ctx) {
1891 ev_ctx->cb(ev_ctx->fd, ev_ctx->ctx);
1892 free_reactor_ctx(reactor, ev_ctx);
1893 if (drain_chunk && ++since_drain >= drain_chunk && i + 1 < n) {
1894 since_drain = 0;
1895 reactor_drain_posts(reactor);
1896 }
1897 }
1898 }
1899 }
1900#endif
1901 t_run_reactor = NULL;
1902}
1903
1905 if (!reactor) return;
1906 /* Called from foreign threads (shutdown paths, embedders). */
1907 __atomic_store_n(&reactor->running, false, __ATOMIC_RELEASE);
1908 /* A run thread parked in io_uring_enter(GETEVENTS) on an idle SQPOLL
1909 * ring sleeps until a CQE arrives: the kernel ignores the enter
1910 * timeout for SQPOLL rings, so with no pending SQEs the wait never
1911 * returns and the pthread_join() in the pool destroy path hangs the
1912 * process (observed as a worker child surviving SIGTERM until SIGKILL,
1913 * wedging the supervisor's shutdown waitpid). Nudge the registered
1914 * wake fd so the run thread re-checks its shutdown flags. */
1915 if (reactor->wake_wr >= 0) {
1916 uint64_t one = 1;
1917 ssize_t ign = write(reactor->wake_wr, &one, sizeof(one));
1918 (void)ign; /* EAGAIN: a wake is already pending, which is fine. */
1919 }
1920}
__attribute__((constructor))
Definition alloc.c:64
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
#define MSG_DONTWAIT
Definition http3.c:82
#define SOCK_CLOEXEC
Definition orm_socket.c:442
size_t batch
Definition otel.c:54
size_t head
Definition otel.c:57
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
bool cwist_reactor_add(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload, size_t payload_size)
Definition reactor.c:1357
void cwist_reactor_timer_init(cwist_reactor_timer_t *timer, void(*cb)(void *ctx), void *ctx)
Definition reactor.c:929
bool cwist_reactor_del(cwist_reactor_t *reactor, int fd)
Definition reactor.c:1410
#define REACTOR_IDLE_WAIT_NS
Definition reactor.c:878
#define REACTOR_CHUNK_EVENTS
Definition reactor.c:241
void cwist_reactor_destroy(cwist_reactor_t *reactor)
Definition reactor.c:1116
void cwist_reactor_timer_cancel(cwist_reactor_t *r, cwist_reactor_timer_t *timer)
Definition reactor.c:956
struct reactor_slot_chunk reactor_slot_chunk_t
void cwist_reactor_stop(cwist_reactor_t *reactor)
Definition reactor.c:1904
cwist_reactor_t * cwist_reactor_create(void)
Definition reactor.c:1006
void cwist_reactor_run(cwist_reactor_t *reactor)
Definition reactor.c:1579
bool cwist_reactor_mod(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload, size_t payload_size)
Definition reactor.c:1367
bool cwist_reactor_timer_armed(const cwist_reactor_timer_t *timer)
Definition reactor.c:961
bool cwist_reactor_timer_arm(cwist_reactor_t *r, cwist_reactor_timer_t *timer, uint64_t delay_us)
Definition reactor.c:937
bool cwist_reactor_add_out(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload, size_t payload_size)
Definition reactor.c:1362
#define CWIST_REACTOR_PAYLOAD_SIZE
Definition reactor.h:19
struct cwist_reactor cwist_reactor_t
Definition reactor.h:14
void(* cwist_reactor_cb_t)(int fd, void *ctx)
Definition reactor.h:12
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
Unified Graceful Shutdown support for CWIST.
atomic_int g_cwist_running
Global running flag. Checked by all server event loops.
Definition shutdown.c:17
Definition reactor.h:46
struct cwist_reactor_post * next
Definition reactor.h:47
void(* cb)(void *ctx)
Definition reactor.h:48
uint64_t deadline_ns
Definition reactor.h:63
uint32_t heap_slot
Definition reactor.h:64
void(* cb)(void *ctx)
Definition reactor.h:65