Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_reactor.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
28#include "nilorea/n_reactor.h"
29
30#include "nilorea/n_log.h"
31#include "nilorea/n_common.h"
32#include "nilorea/n_str.h"
33#include "nilorea/n_list.h"
34#include "nilorea/n_network.h"
35#include "nilorea/n_zlib.h"
36#include "nilorea/n_lz4.h"
37
38#include <stdlib.h>
39#include <string.h>
40#include <errno.h>
41#include <fcntl.h>
42
43#if N_REACTOR_AVAILABLE
44
45#include <unistd.h>
46#include <pthread.h>
47#include <sys/epoll.h>
48#include <sys/eventfd.h>
49#include <sys/socket.h>
50#include <arpa/inet.h> /* ntohl, htonl */
51#include <stdatomic.h>
52
53/* Default registered-fd capacity. The internal table grows past
54 * this on demand at register time, the hint just sizes the
55 * initial alloc. */
56#define N_REACTOR_DEFAULT_MAX_FDS 256
57
58/* Maximum events drained per `epoll_wait` call. Larger = fewer
59 * syscalls under load; smaller = lower per-event scheduling
60 * latency. 64 is a common pragmatic compromise. */
61#define N_REACTOR_BATCH_SIZE 64
62
63struct n_reactor {
64 int epoll_fd;
65 int stop_efd; /* eventfd written by n_reactor_stop */
66 int wake_efd; /* eventfd written by producers */
67 int stop_requested; /* set by stop_efd handler */
68
69 /* Registered NETWORKs. The wake-event handler scans this list to
70 * find connections that have new data in send_buf; the EPOLLOUT
71 * handler reaches NETWORK directly via data.ptr and doesn't need
72 * the list. Mutex protects insert / remove from any thread (the
73 * register / unregister API is called from the producer thread,
74 * the same thread that calls n_reactor_run, or from accept-pool
75 * threads). */
76 LIST* registered;
77 pthread_mutex_t registered_lock;
78
79 /* Stats, accessed via atomic loads/stores so n_reactor_get_stats
80 * is safe from any thread without a mutex. */
81 atomic_llong events_processed;
82 atomic_llong writes_partial;
83 atomic_llong reads_partial;
84 atomic_llong fds_registered;
85 atomic_llong fds_unregistered;
86 atomic_llong wake_signals;
87 /* Registered-list walk instrumentation. Counts how often the
88 * registered list is walked at wake time and how productive each
89 * walk is. */
90 atomic_llong wake_walks; /* # of times the registered list was walked at wake */
91 atomic_llong wake_walk_visits; /* sum of NETWORKs visited across all wake walks */
92 atomic_llong wake_walk_drains; /* # of NETWORKs that had pending data during walks */
93
94 /* n_reactor_notify_send adds the NETWORK to `dirty_pending`
95 * (CAS-guarded via NETWORK.in_dirty_list) under dirty_lock; the
96 * wake handler walks only this list instead of the full
97 * registered list. */
98 LIST* dirty_pending; /* NETWORK* entries with pending sends */
99 pthread_mutex_t dirty_lock;
100};
101
102/* Internal helpers. */
103
104static int register_internal_efd(int epoll_fd, int efd, uint64_t tag) {
105 /* The two internal eventfds (stop, wake) are registered with
106 * data.u64 = a tag value the run loop uses to dispatch back
107 * to internal handling without a per-fd lookup. NETWORK fds
108 * use data.ptr. */
109 struct epoll_event ev;
110 ev.events = EPOLLIN;
111 ev.data.u64 = tag;
112 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, efd, &ev) != 0) {
113 n_log(LOG_ERR, "n_reactor: epoll_ctl ADD eventfd failed: %s",
114 strerror(errno));
115 return 0;
116 }
117 return 1;
118}
119
120/* Tag values for data.u64 on internally-registered eventfds. They
121 * are small enough to never collide with a real pointer (no
122 * legitimate `NETWORK *` lives in the bottom MB of address space);
123 * the run loop branches on `data.u64 < N_REACTOR_TAG_INTERNAL_MAX`
124 * to detect internal vs NETWORK fds. */
125#define N_REACTOR_TAG_STOP 1ULL
126#define N_REACTOR_TAG_WAKE 2ULL
127#define N_REACTOR_TAG_INTERNAL_MAX 16ULL
128
129/* Drain an eventfd. Reads in 8-byte units (eventfd's contract);
130 * keeps reading until EAGAIN. We don't care about the count, just
131 * the fact that something happened. */
132static long long drain_eventfd(int efd) {
133 long long total = 0;
134 uint64_t buf;
135 for (;;) {
136 ssize_t n = read(efd, &buf, sizeof(buf));
137 if (n == sizeof(buf)) {
138 total += (long long)buf;
139 continue;
140 }
141 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) break;
142 /* Either a partial read (impossible per eventfd's contract)
143 * or some other error. Either way, break, the wake event
144 * is "something happened", we don't lose state. */
145 break;
146 }
147 return total;
148}
149
150/* Set a file descriptor to non-blocking mode. Returns 1 on success. */
151static int set_nonblocking(int fd) {
152 int flags = fcntl(fd, F_GETFL, 0);
153 if (flags < 0) return 0;
154 if ((flags & O_NONBLOCK) == O_NONBLOCK) return 1;
155 if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) return 0;
156 return 1;
157}
158
159/* Free any in-flight recv accumulator state on the NETWORK. */
160static void reactor_recv_state_reset(NETWORK* netw) {
167}
168
169/* Free any in-flight send buffer on the NETWORK. */
170static void reactor_send_state_reset(NETWORK* netw) {
177}
178
179/* Update the EPOLL_CTL_MOD events for a registered NETWORK. ev_mask
180 * is the desired event set (already including EPOLLIN | EPOLLRDHUP |
181 * EPOLLET, caller adds or removes EPOLLOUT). */
182static int reactor_epoll_mod(n_reactor* r, NETWORK* netw, uint32_t ev_mask) {
183 struct epoll_event ev;
184 ev.events = ev_mask;
185 ev.data.ptr = netw;
186 return epoll_ctl(r->epoll_fd, EPOLL_CTL_MOD, netw->link.sock, &ev) == 0;
187}
188
189/* Pop the next N_STR from netw->send_buf (under sendbolt) and pre-frame
190 * it into a contiguous buffer ready to send: 4-byte state word + 4-byte
191 * payload length + payload bytes, with optional zlib/lz4 compression
192 * applied to the payload (same gating as netw_send_func: compress_mode
193 * != NONE && payload >= NETW_COMPRESS_THRESHOLD && ratio >= 10 %).
194 *
195 * On success populates netw->reactor_send_buf / _len / _off and returns
196 * 1. On nothing-to-send returns 0. On allocation failure returns -1
197 * (caller should treat as fatal).
198 */
199static int reactor_send_state_load_next(NETWORK* netw) {
200 if (netw->reactor_send_buf) return 1; /* already loaded */
201
202 pthread_mutex_lock(&netw->sendbolt);
204 pthread_mutex_unlock(&netw->sendbolt);
205 if (!msg) return 0;
206
207 /* Optional compression, same gating as netw_send_func, including
208 * the per-NETWORK compress_mode choice (NONE / ZLIB / LZ4) and
209 * the threshold + ratio macros (NETW_COMPRESS_THRESHOLD,
210 * NETW_COMPRESS_MIN_RATIO) shared with the thread engine via the
211 * public n_network.h. */
212 uint32_t pkt_state = NETW_RUN;
214 msg->written >= (size_t)NETW_COMPRESS_THRESHOLD) {
215 N_STR* zipped = NULL;
216 uint32_t flag_bit = 0;
218 zipped = zip4_nstr(msg);
219 flag_bit = NETW_COMPRESSED_LZ4;
220 } else {
221 zipped = zip_nstr(msg);
222 flag_bit = NETW_COMPRESSED_ZLIB;
223 }
224 if (zipped && zipped->written > 0 &&
225 zipped->written * 100u <=
226 msg->written * (100u - NETW_COMPRESS_MIN_RATIO)) {
227 /* Saved at least NETW_COMPRESS_MIN_RATIO %, swap. */
228 free_nstr(&msg);
229 msg = zipped;
230 pkt_state |= flag_bit;
231 } else if (zipped) {
232 free_nstr(&zipped);
233 }
234 }
235
236 if (msg->written > UINT32_MAX) {
237 n_log(LOG_ERR, "n_reactor: payload too large (%zu > UINT32_MAX); dropping",
238 msg->written);
239 free_nstr(&msg);
240 return 0;
241 }
242 size_t total = 8 + msg->written;
243 char* buf = NULL;
244 Malloc(buf, char, total);
245 if (!buf) {
246 free_nstr(&msg);
247 return -1;
248 }
249 uint32_t state_be = htonl(pkt_state);
250 uint32_t length_be = htonl((uint32_t)msg->written);
251 memcpy(buf, &state_be, 4);
252 memcpy(buf + 4, &length_be, 4);
253 memcpy(buf + 8, msg->data, msg->written);
254 free_nstr(&msg);
255
256 netw->reactor_send_buf = buf;
257 netw->reactor_send_len = total;
259 return 1;
260}
261
262/* Drain as much of netw->reactor_send_buf as the kernel will accept,
263 * loop to load the next queued frame on completion. Returns:
264 * 1, drained fully (send_buf empty, no in-flight). Caller disarms
265 * EPOLLOUT.
266 * 0, back-pressure (EAGAIN). Caller arms EPOLLOUT.
267 * -1, hard error / connection closed by peer. Caller unregisters
268 * and flags NETW_ERROR.
269 */
270static int reactor_drain_writes(NETWORK* netw, n_reactor* reactor) {
271 for (;;) {
272 if (!netw->reactor_send_buf) {
273 int r = reactor_send_state_load_next(netw);
274 if (r < 0) return -1;
275 if (r == 0) return 1; /* nothing to send */
276 }
277 size_t remaining = netw->reactor_send_len - netw->reactor_send_off;
278 if (remaining == 0) {
279 /* Buffer fully drained, free and try the next queued msg. */
281 netw->reactor_send_buf = NULL;
284 continue;
285 }
286 /* TLS-over-reactor: route through the NETWORK's
287 * single-attempt send pointer (raw send() on cleartext,
288 * SSL_write on crypto sockets). EINTR is retried inside the
289 * call; partial-write resume rides the same buffer+offset
290 * thanks to SSL_MODE_ENABLE_PARTIAL_WRITE. */
292 uint32_t attempt = (remaining > UINT32_MAX) ? UINT32_MAX : (uint32_t)remaining;
293 ssize_t sent = netw->send_data_once((void*)netw,
295 attempt);
296 if (sent > 0) {
297 netw->reactor_send_off += (size_t)sent;
298 continue;
299 }
300 if (sent == NETW_IO_WANT_WRITE) {
301 atomic_fetch_add(&reactor->writes_partial, 1);
302 return 0;
303 }
304 if (sent == NETW_IO_WANT_READ) {
305 /* TLS renegotiation: the write needs inbound bytes first.
306 * Flag it so the readable path retries the drain; the
307 * caller's EPOLLOUT arming is harmless (edge-triggered,
308 * fires at most once while we wait). */
310 atomic_fetch_add(&reactor->writes_partial, 1);
311 return 0;
312 }
313 /* NETW_SOCKET_DISCONNECTED / NETW_SOCKET_ERROR (already logged
314 * inside the once-helper). */
315 return -1;
316 }
317}
318
319/* Push a fully-received frame onto the NETWORK's recv_buf. Mirrors
320 * the thread-mode netw_recv_func tail: optionally decompresses
321 * based on the frame's state-word flags, then list_push under the
322 * recvbolt mutex. Frees pkt_payload on failure. */
323static void reactor_recv_dispatch_frame(NETWORK* netw,
324 uint32_t pkt_state,
325 uint32_t pkt_length,
326 char* pkt_payload) {
327 /* Wrap the raw bytes in an N_STR, same shape the thread-mode
328 * recv path produces. */
329 N_STR* msg = NULL;
330 Malloc(msg, N_STR, 1);
331 if (!msg) {
332 Free(pkt_payload);
333 return;
334 }
335 msg->data = pkt_payload;
336 msg->length = (size_t)pkt_length + 1;
337 msg->written = (size_t)pkt_length;
338 /* Ensure NUL-terminator for downstream consumers that treat
339 * the buffer as a C string (decompression's unzip*_nstr
340 * inspects strptr->data[written] in some paths). The buffer
341 * was sized to pkt_length+1 in the caller. */
342 msg->data[pkt_length] = '\0';
343
344 /* Decompression: same logic as netw_recv_func. */
345 int want_zlib = (pkt_state & NETW_COMPRESSED_ZLIB) != 0;
346 int want_lz4 = (pkt_state & NETW_COMPRESSED_LZ4) != 0;
347 if (want_zlib || want_lz4) {
348 N_STR* plain = want_lz4 ? unzip4_nstr(msg) : unzip_nstr(msg);
349 if (plain) {
350 free_nstr(&msg);
351 msg = plain;
352 } else {
354 "n_reactor: failed to decompress payload "
355 "(%" PRIu32 " bytes, codec=%s); dropping",
356 pkt_length, want_lz4 ? "lz4" : "zlib");
357 free_nstr(&msg);
358 return;
359 }
360 }
361
362 pthread_mutex_lock(&netw->recvbolt);
363 if (list_push(netw->recv_buf, msg, free_nstr_ptr) == FALSE) {
364 pthread_mutex_unlock(&netw->recvbolt);
365 n_log(LOG_ERR, "n_reactor: recv_buf list_push failed; dropping frame");
366 free_nstr(&msg);
367 return;
368 }
369 pthread_mutex_unlock(&netw->recvbolt);
370}
371
372/* Sweep the registered list for NETWORKs whose game thread has set
373 * NETW_EXIT_ASKED, drain pending sends best-effort,
374 * shutdown(SHUT_WR) so the peer observes EOF, unregister from the
375 * epoll set, and signal the close ack so the game thread can close
376 * the fd.
377 *
378 * Snapshots pointers under the lock, then processes them with the
379 * lock released, `n_reactor_unregister` reacquires the same lock,
380 * so we mustn't hold it across the call. The snapshot batch caps at
381 * N_REACTOR_BATCH_SIZE; any additional EXIT_ASKED entries are caught
382 * on the next sweep (the sweep runs at the end of every wake batch
383 * + every heartbeat tick, so the lag is bounded). */
384static void reactor_sweep_exit_asked(n_reactor* reactor) {
385 NETWORK* batch[N_REACTOR_BATCH_SIZE];
386 int count = 0;
387
388 pthread_mutex_lock(&reactor->registered_lock);
389 LIST_NODE* node = reactor->registered->start;
390 while (node && count < N_REACTOR_BATCH_SIZE) {
391 NETWORK* n = (NETWORK*)node->ptr;
392 node = node->next;
393 if (!n || !netw_atomic_read_reactor_mode(n)) continue;
394 uint32_t st = 0;
395 int thr_st = 0;
396 netw_get_state(n, &st, &thr_st);
397 if (st & NETW_EXIT_ASKED) {
398 batch[count++] = n;
399 }
400 }
401 pthread_mutex_unlock(&reactor->registered_lock);
402
403 for (int i = 0; i < count; i++) {
404 NETWORK* n = batch[i];
405 /* Best-effort drain, peer is going away so partial is fine.
406 * Swallow the return value: error / EAGAIN both lead to the
407 * same teardown path next. */
408 if (n->reactor_send_buf || (n->send_buf && n->send_buf->nb_items > 0)) {
409 (void)reactor_drain_writes(n, reactor);
410 }
411 /* Half-close the write side so the peer's read side observes
412 * EOF immediately. The fd close itself happens later in the
413 * caller's netw_close after `reactor_close_acked` flips.
414 * n_reactor_unregister publishes that ack with a release store
415 * as its final action, so nothing may touch `n` afterwards: the
416 * game thread's acquire-load can observe the ack and free the
417 * NETWORK the instant unregister returns. */
418 shutdown(n->link.sock, SHUT_WR);
419 n_reactor_unregister(reactor, n);
420 }
421}
422
423/* Drain the socket non-blockingly into the NETWORK's recv accumulator.
424 * Parses as many complete frames as the bytes allow; pushes each onto
425 * recv_buf. Edge-triggered: keeps reading until EAGAIN. Returns 1 on
426 * normal progress, 0 if the connection should be torn down (peer
427 * closed cleanly, or a hard error). */
428static int reactor_handle_readable(NETWORK* netw, n_reactor* reactor) {
429 char chunk[4096];
430 int eof = 0;
431
432 /* TLS-over-reactor: route through the NETWORK's
433 * single-attempt recv pointer (raw recv() on cleartext, SSL_read
434 * on crypto sockets). Looping until NETW_IO_WANT_READ both drains
435 * the edge-triggered readiness AND empties OpenSSL's internal
436 * plaintext buffer (SSL_read reports WANT_READ only once that
437 * buffer is dry), so no SSL_pending() poll is needed. */
439
440 for (;;) {
441 ssize_t got = netw->recv_data_once((void*)netw, chunk, (uint32_t)sizeof(chunk));
442 if (got == NETW_SOCKET_DISCONNECTED) {
443 eof = 1;
444 break;
445 }
446 if (got == NETW_IO_WANT_READ) break; /* drained */
447 if (got == NETW_IO_WANT_WRITE) {
448 /* TLS renegotiation: the read needs outbound room first.
449 * Flag it so the run loop arms EPOLLOUT and re-runs this
450 * drain after the socket turns writable. */
452 break;
453 }
454 if (got <= 0) {
455 /* NETW_SOCKET_ERROR: already logged inside the helper. */
456 return 0;
457 }
458
459 /* Feed `got` bytes through the state machine. */
460 const char* p = chunk;
461 size_t rem = (size_t)got;
462 while (rem > 0) {
463 switch (netw->reactor_read_phase) {
464 case 0: /* STATE word */
465 case 1: /* LENGTH word */
466 {
467 size_t need = 4 - (size_t)netw->reactor_read_hdr_have;
468 size_t take = (rem < need) ? rem : need;
470 p, take);
471 netw->reactor_read_hdr_have += (int)take;
472 p += take;
473 rem -= take;
474 if (netw->reactor_read_hdr_have < 4) {
475 /* Need more bytes for this header word. */
476 atomic_fetch_add(&reactor->reads_partial, 1);
477 break;
478 }
479 /* Header word complete. */
480 uint32_t word;
481 memcpy(&word, netw->reactor_read_hdr_buf, sizeof(word));
482 word = ntohl(word);
484 if (netw->reactor_read_phase == 0) {
486 if (word == NETW_EXIT_ASKED) {
487 /* Peer requested clean shutdown. Treat as EOF. */
488 eof = 1;
489 rem = 0;
490 break;
491 }
493 } else {
495 /* Allocate the payload buffer. +1 for the NUL
496 * the dispatch helper writes after fill. */
497 if (word == 0) {
498 /* Zero-byte payload: dispatch immediately
499 * with an empty N_STR-shaped buffer. */
500 char* empty = NULL;
501 Malloc(empty, char, 1);
502 if (!empty) {
503 n_log(LOG_ERR, "n_reactor: alloc(empty payload) failed");
504 return 0;
505 }
506 reactor_recv_dispatch_frame(netw,
508 0, empty);
510 } else {
511 Malloc(netw->reactor_read_payload, char, word + 1);
513 n_log(LOG_ERR, "n_reactor: alloc(%u-byte payload) failed",
514 word);
515 return 0;
516 }
519 }
520 }
521 break;
522 }
523 case 2: /* PAYLOAD */
524 {
526 size_t take = (rem < need) ? rem : need;
528 p, take);
530 p += take;
531 rem -= take;
533 /* Partial payload, stay in payload-read state, wait
534 * for more bytes (next read or next epoll event). */
535 atomic_fetch_add(&reactor->reads_partial, 1);
536 break;
537 }
538 /* Complete payload, dispatch. ownership of the
539 * buffer transfers into the dispatcher. */
540 char* payload = netw->reactor_read_payload;
542 reactor_recv_dispatch_frame(netw,
545 payload);
548 break;
549 }
550 }
551 }
552 }
553 if (eof) return 0;
554 return 1;
555}
556
557/* public API */
558
559n_reactor* n_reactor_new(int max_fds_hint) {
560 (void)max_fds_hint; /* per-fd table grows on demand at register time */
561
562 n_reactor* r = NULL;
563 Malloc(r, n_reactor, 1); /* calloc-backed: r is zero-initialised */
564 __n_assert(r, return NULL);
565 r->epoll_fd = -1;
566 r->stop_efd = -1;
567 r->wake_efd = -1;
568
569 /* CLOEXEC because forking the asset auditor (server SIGHUP path
570 * does this) shouldn't leak the epoll fd to the child. */
571 r->epoll_fd = epoll_create1(EPOLL_CLOEXEC);
572 if (r->epoll_fd < 0) {
573 n_log(LOG_ERR, "n_reactor_new: epoll_create1 failed: %s",
574 strerror(errno));
575 goto fail;
576 }
577
578 /* Both eventfds: nonblocking (so drain doesn't block the run
579 * loop) and CLOEXEC (same fork hygiene). EFD_SEMAPHORE is NOT
580 * set, we want each `write` to add to the count, not sit
581 * one-event-at-a-time. */
582 r->stop_efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
583 if (r->stop_efd < 0) {
584 n_log(LOG_ERR, "n_reactor_new: eventfd(stop) failed: %s",
585 strerror(errno));
586 goto fail;
587 }
588 r->wake_efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
589 if (r->wake_efd < 0) {
590 n_log(LOG_ERR, "n_reactor_new: eventfd(wake) failed: %s",
591 strerror(errno));
592 goto fail;
593 }
594
595 if (!register_internal_efd(r->epoll_fd, r->stop_efd, N_REACTOR_TAG_STOP) ||
596 !register_internal_efd(r->epoll_fd, r->wake_efd, N_REACTOR_TAG_WAKE)) {
597 goto fail;
598 }
599
600 r->registered = new_generic_list(MAX_LIST_ITEMS);
601 if (!r->registered) {
602 n_log(LOG_ERR, "n_reactor_new: cannot allocate registered list");
603 goto fail;
604 }
605 pthread_mutex_init(&r->registered_lock, NULL);
606
607 atomic_store(&r->events_processed, 0);
608 atomic_store(&r->writes_partial, 0);
609 atomic_store(&r->reads_partial, 0);
610 atomic_store(&r->fds_registered, 0);
611 atomic_store(&r->fds_unregistered, 0);
612 atomic_store(&r->wake_signals, 0);
613 atomic_store(&r->wake_walks, 0);
614 atomic_store(&r->wake_walk_visits, 0);
615 atomic_store(&r->wake_walk_drains, 0);
616 r->stop_requested = 0;
617
618 /* Dirty-list: see n_reactor.h dirty_pending field comment for the
619 * design rationale. */
620 r->dirty_pending = new_generic_list(MAX_LIST_ITEMS);
621 if (!r->dirty_pending) {
622 n_log(LOG_ERR, "n_reactor_new: cannot allocate dirty_pending list");
623 goto fail;
624 }
625 pthread_mutex_init(&r->dirty_lock, NULL);
626
627 n_log(LOG_INFO, "n_reactor_new: created (epoll_fd=%d stop_efd=%d wake_efd=%d)",
628 r->epoll_fd, r->stop_efd, r->wake_efd);
629 return r;
630
631fail:
632 if (r) {
633 if (r->wake_efd >= 0) close(r->wake_efd);
634 if (r->stop_efd >= 0) close(r->stop_efd);
635 if (r->epoll_fd >= 0) close(r->epoll_fd);
636 Free(r);
637 }
638 return NULL;
639}
640
641void n_reactor_destroy(n_reactor** reactor) {
642 if (!reactor || !*reactor) return;
643 n_reactor* r = *reactor;
644
645 /* If the run loop is still on another thread, the caller
646 * should have called `n_reactor_stop` and joined that thread
647 * already. We don't enforce, best-effort cleanup either way. */
648 if (r->wake_efd >= 0) close(r->wake_efd);
649 if (r->stop_efd >= 0) close(r->stop_efd);
650 if (r->epoll_fd >= 0) close(r->epoll_fd);
651 if (r->registered) {
652 list_destroy(&r->registered); /* nodes hold raw NETWORK *, no destructor */
653 pthread_mutex_destroy(&r->registered_lock);
654 }
655 if (r->dirty_pending) {
656 list_destroy(&r->dirty_pending); /* same: nodes are NETWORK* aliases, owner == registered list */
657 pthread_mutex_destroy(&r->dirty_lock);
658 }
659
660 Free(r);
661 *reactor = NULL;
662}
663
664void n_reactor_run(n_reactor* reactor) {
665 if (!reactor) return;
666
667 struct epoll_event events[N_REACTOR_BATCH_SIZE];
668
669 while (!reactor->stop_requested) {
670 /* Heartbeat: bound the wait so the EXIT_ASKED sweep runs at
671 * least once per second even if nobody calls
672 * n_reactor_notify_send / n_reactor_close_netw_sync. The
673 * notify path remains the immediate one (sub-millisecond);
674 * the heartbeat is the safety net. */
675 int n = epoll_wait(reactor->epoll_fd, events,
676 N_REACTOR_BATCH_SIZE, 1000);
677 if (n < 0) {
678 if (errno == EINTR) continue;
679 n_log(LOG_ERR, "n_reactor_run: epoll_wait failed: %s",
680 strerror(errno));
681 break;
682 }
683 if (n == 0) {
684 /* Heartbeat tick: nothing on the wire, but a connection
685 * may have been flagged for teardown without an explicit
686 * notify. */
687 reactor_sweep_exit_asked(reactor);
688 continue;
689 }
690
691 for (int i = 0; i < n; i++) {
692 uint64_t tag = events[i].data.u64;
693 atomic_fetch_add(&reactor->events_processed, 1);
694
695 if (tag == N_REACTOR_TAG_STOP) {
696 /* Drain the eventfd so it doesn't refire, then
697 * mark the loop for exit. We don't break
698 * immediately, finish processing this batch so
699 * any concurrent NETWORK events still get drained. */
700 drain_eventfd(reactor->stop_efd);
701 reactor->stop_requested = 1;
702 continue;
703 }
704 if (tag == N_REACTOR_TAG_WAKE) {
705 /* Producer-side wakeup. Drain the eventfd, then scan
706 * registered NETWORKs for any with pending sends and
707 * try to drain them, for the common no-back-pressure
708 * case that finishes the work before the next
709 * epoll_wait, avoiding the EPOLLOUT round trip. */
710 long long drained = drain_eventfd(reactor->wake_efd);
711 atomic_fetch_add(&reactor->wake_signals, drained);
712
713 /* Wake-handler walks ONLY the dirty list. Splice
714 * dirty_pending into a local list under a single lock
715 * pair so producers can continue pushing onto the fresh
716 * list (picked up on the next wake) without contending
717 * with the drain loop. */
718 atomic_fetch_add(&reactor->wake_walks, 1);
719 long long visits_this_walk = 0;
720 long long drains_this_walk = 0;
721
722 LIST* walk_list = NULL;
723 int walk_owned = 0; /* 1 = we allocated walk_list, must list_destroy */
724 pthread_mutex_t* walk_lock = NULL;
725 pthread_mutex_lock(&reactor->dirty_lock);
726 {
727 LIST* old = reactor->dirty_pending;
729 if (fresh) {
730 reactor->dirty_pending = fresh;
731 walk_list = old;
732 walk_owned = 1;
733 } else {
734 /* Allocation failure: fall back to walking in-place
735 * under the lock. Suboptimal but correct. */
736 walk_list = old;
737 walk_lock = &reactor->dirty_lock;
738 }
739 }
740 if (walk_owned) {
741 pthread_mutex_unlock(&reactor->dirty_lock);
742 }
743
744 LIST_NODE* node = walk_list ? walk_list->start : NULL;
745 while (node) {
746 NETWORK* netw = (NETWORK*)node->ptr;
747 LIST_NODE* next = node->next;
748 /* Every entry is by construction a NETWORK with
749 * pending data when it was pushed; the reactor_mode
750 * gate stays for safety against concurrent unregister. */
752 visits_this_walk++;
753 /* Clear the in-list flag BEFORE draining.
754 * A producer that pushes during the drain succeeds
755 * the CAS and re-adds for the next wake; no
756 * message can be lost. */
757 __atomic_store_n(&netw->in_dirty_list, 0, __ATOMIC_RELEASE);
758 /* Quick check: nothing to do if both buffers
759 * (in-flight + queue) are empty. The recvbolt
760 * isn't taken, we read nb_items as a hint;
761 * the producer may be in the middle of a
762 * push, but the next wake will catch it. */
763 int has_inflight = (netw->reactor_send_buf != NULL);
764 int has_queued = (netw->send_buf->nb_items > 0);
765 if (has_inflight || has_queued) {
766 drains_this_walk++;
767 int rc = reactor_drain_writes(netw, reactor);
768 if (rc == 0) {
769 /* EAGAIN, arm EPOLLOUT for back-pressure relief. */
771 if (reactor_epoll_mod(reactor, netw,
772 EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET)) {
774 }
775 }
776 } else if (rc < 0) {
777 /* n_reactor_unregister takes its own
778 * locks (registered_lock + dirty_lock).
779 * Release whatever walk-side lock we
780 * currently hold first to preserve the
781 * documented ordering and avoid
782 * self-deadlock; reacquire after. Set the
783 * state flag BEFORE unregister: unregister
784 * publishes the close ack as its last act,
785 * after which the game thread may free the
786 * NETWORK, so netw must not be touched. */
787 if (walk_lock) pthread_mutex_unlock(walk_lock);
789 n_reactor_unregister(reactor, netw);
790 if (walk_lock) pthread_mutex_lock(walk_lock);
791 } else {
792 /* Fully drained, disarm EPOLLOUT if armed. */
794 if (reactor_epoll_mod(reactor, netw,
795 EPOLLIN | EPOLLRDHUP | EPOLLET)) {
797 }
798 }
799 }
800 }
801 }
802 node = next;
803 }
804 /* Release whatever walk-side lock we still hold
805 * (dirty_lock in the fallback path, none for the
806 * spliced-list happy path), then free the spliced list
807 * if we owned it. */
808 if (walk_lock) pthread_mutex_unlock(walk_lock);
809 if (walk_owned && walk_list) {
810 list_destroy(&walk_list); /* nodes are NETWORK* aliases, no destructor */
811 }
812 /* Flush per-walk visit/drain tally into the lifetime
813 * accumulators after dropping the lock. */
814 atomic_fetch_add(&reactor->wake_walk_visits, visits_this_walk);
815 atomic_fetch_add(&reactor->wake_walk_drains, drains_this_walk);
816 continue;
817 }
818
819 /* NETWORK event. data.ptr points at the NETWORK *.
820 * We rely on data.u64 >= N_REACTOR_TAG_INTERNAL_MAX
821 * because every legitimate pointer is far above 16 in
822 * any modern address space. */
823 NETWORK* netw = (NETWORK*)events[i].data.ptr;
824 if (!netw) continue;
825
826 uint32_t evmask = events[i].events;
827 if (evmask & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) {
828 /* Hard error or peer closed: flag the NETWORK so the
829 * game thread observes it on next netw_get_msg via the
830 * existing state-flag check, THEN unregister. Order
831 * matters: unregister publishes the close ack as its
832 * last act, releasing a game thread that may be spinning
833 * in n_reactor_close_netw_sync and free the NETWORK, so
834 * netw must not be touched after unregister returns. */
836 n_reactor_unregister(reactor, netw);
837 continue;
838 }
839 if (evmask & EPOLLIN) {
840 if (!reactor_handle_readable(netw, reactor)) {
841 /* EOF or unrecoverable read error. Flag before
842 * unregister (unregister publishes the close ack
843 * last; netw may be freed the moment it returns). */
845 n_reactor_unregister(reactor, netw);
846 continue;
847 }
848 /* TLS plumbing: a send that returned
849 * WANT_READ (renegotiation) can progress now that
850 * inbound bytes arrived. */
852 int rc = reactor_drain_writes(netw, reactor);
853 if (rc < 0) {
854 /* Flag before unregister: unregister publishes
855 * the close ack last and netw may be freed once
856 * it returns. */
858 n_reactor_unregister(reactor, netw);
859 continue;
860 }
861 if (rc > 0 && netw->reactor_write_armed &&
863 if (reactor_epoll_mod(reactor, netw,
864 EPOLLIN | EPOLLRDHUP | EPOLLET)) {
866 }
867 } else if (rc == 0 && !netw->reactor_write_armed) {
868 if (reactor_epoll_mod(reactor, netw,
869 EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET)) {
871 }
872 }
873 }
874 /* TLS plumbing: a recv that returned WANT_WRITE needs
875 * the socket writable: arm EPOLLOUT so the drain
876 * re-runs from that branch. */
878 if (reactor_epoll_mod(reactor, netw,
879 EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET)) {
881 }
882 }
883 }
884 if (evmask & EPOLLOUT) {
885 /* TLS plumbing: a recv blocked on WANT_WRITE
886 * retries first, the socket just turned writable. */
888 if (!reactor_handle_readable(netw, reactor)) {
889 /* Flag before unregister: unregister publishes
890 * the close ack last and netw may be freed once
891 * it returns. */
893 n_reactor_unregister(reactor, netw);
894 continue;
895 }
896 }
897 int rc = reactor_drain_writes(netw, reactor);
898 if (rc < 0) {
899 /* Flag before unregister: unregister publishes the
900 * close ack last and netw may be freed once it
901 * returns. */
903 n_reactor_unregister(reactor, netw);
904 continue;
905 }
906 if (rc > 0 && netw->reactor_write_armed &&
908 /* Drained; disarm EPOLLOUT to avoid spurious wakeups
909 * (kept armed while a renegotiating recv still
910 * needs the writable signal). */
911 if (reactor_epoll_mod(reactor, netw,
912 EPOLLIN | EPOLLRDHUP | EPOLLET)) {
914 }
915 }
916 /* rc == 0 (back-pressure / WANT) means EPOLLOUT remains
917 * armed, kernel will fire again when buffer has room. */
918 }
919 }
920 /* End-of-batch teardown sweep, pick up any NETWORK whose
921 * game-side has flagged EXIT_ASKED during this batch (or
922 * earlier batches that didn't reach the snapshot cap). */
923 reactor_sweep_exit_asked(reactor);
924 }
925
926 n_log(LOG_INFO, "n_reactor_run: exiting (events=%lld)",
927 (long long)atomic_load(&reactor->events_processed));
928}
929
930void n_reactor_stop(n_reactor* reactor) {
931 if (!reactor || reactor->stop_efd < 0) return;
932 /* eventfd write semantics: any 8-byte value > 0 increments the
933 * counter and triggers EPOLLIN on the read side. Use 1 for
934 * clarity. */
935 uint64_t one = 1;
936 ssize_t w = write(reactor->stop_efd, &one, sizeof(one));
937 (void)w; /* best-effort; if it fails the loop will exit on next
938 * idle epoll_wait via stop_requested check */
939}
940
941void* n_reactor_run_thread_entry(void* arg) {
943 return NULL;
944}
945
946void n_reactor_get_stats(const n_reactor* reactor, n_reactor_stats* out) {
947 if (!out) return;
948 if (!reactor) {
949 memset(out, 0, sizeof(*out));
950 return;
951 }
952 out->events_processed = atomic_load(&reactor->events_processed);
953 out->writes_partial = atomic_load(&reactor->writes_partial);
954 out->reads_partial = atomic_load(&reactor->reads_partial);
955 out->fds_registered = atomic_load(&reactor->fds_registered);
956 out->fds_unregistered = atomic_load(&reactor->fds_unregistered);
957 out->wake_signals = atomic_load(&reactor->wake_signals);
958 out->wake_walks = atomic_load(&reactor->wake_walks);
959 out->wake_walk_visits = atomic_load(&reactor->wake_walk_visits);
960 out->wake_walk_drains = atomic_load(&reactor->wake_walk_drains);
961}
962
964 if (!reactor || !netw) return 0;
966 n_log(LOG_WARNING, "n_reactor_register: socket %d already in reactor mode",
967 netw->link.sock);
968 return 0;
969 }
972 "n_reactor_register: socket %d has thread engine started; "
973 "reactor and thread mode are mutually exclusive",
974 netw->link.sock);
975 return 0;
976 }
977 if (!set_nonblocking(netw->link.sock)) {
978 n_log(LOG_ERR, "n_reactor_register: O_NONBLOCK on socket %d failed: %s",
979 netw->link.sock, strerror(errno));
980 return 0;
981 }
982 /* The reactor's I/O goes through the single-attempt
983 * pointers (TLS-aware). netw_new seeds them (and the crypto setup
984 * swaps in the SSL variants), but a hand-constructed NETWORK
985 * shell (tests, embedders) may leave them NULL, default to the
986 * raw cleartext variants rather than dereferencing NULL. */
989
990 /* Reset accumulator state from any prior use of the slot. */
991 reactor_recv_state_reset(netw);
992 reactor_send_state_reset(netw);
993 /* Clear stale ack from a prior cycle. This runs before the netw is
994 * published to the reactor thread (reactor_mode release, below), so
995 * it cannot race the sweep; use the same atomic accessor for
996 * consistency with the release/acquire pair on this flag. */
997 __atomic_store_n(&netw->reactor_close_acked, 0, __ATOMIC_RELEASE);
998
999 struct epoll_event ev;
1000 /* Edge-triggered: drain the socket fully on each event. EPOLLRDHUP
1001 * gives us peer-half-close as a separate signal. EPOLLOUT is added
1002 * lazily by the send path when there's pending data. */
1003 ev.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
1004 ev.data.ptr = netw;
1005 if (epoll_ctl(reactor->epoll_fd, EPOLL_CTL_ADD, netw->link.sock, &ev) != 0) {
1006 n_log(LOG_ERR, "n_reactor_register: epoll_ctl ADD socket %d failed: %s",
1007 netw->link.sock, strerror(errno));
1008 return 0;
1009 }
1010 netw_atomic_write_reactor_handle(netw, (void*)reactor);
1011 /* Latch that this NETWORK now owes a reactor close-handshake ack.
1012 * Set only here, on full registration success, and never cleared by
1013 * the reactor; netw_close keys its ack-wait on this rather than on
1014 * the live reactor_mode (see the field doc in n_network.h). The
1015 * register failure paths above all return 0 before this point, so a
1016 * caller that netw_close's a never-registered NETWORK leaves the
1017 * latch 0 and avoids an unanswerable wait. */
1018 __atomic_store_n(&netw->reactor_registered, 1, __ATOMIC_RELEASE);
1019 /* Publish reactor_mode last (release on the flag pairs with the
1020 * acquire reads in netw_close / netw_add_msg) so any thread that
1021 * sees reactor_mode == 1 also sees a non-NULL reactor_handle. */
1023
1024 /* Track in the reactor's registered list so the wake-event
1025 * handler can find this NETWORK without a tree lookup. The
1026 * list_push happens with NULL destructor, nodes hold raw
1027 * NETWORK *, ownership stays with the caller. */
1028 pthread_mutex_lock(&reactor->registered_lock);
1029 list_push(reactor->registered, netw, NULL);
1030 pthread_mutex_unlock(&reactor->registered_lock);
1031
1032 atomic_fetch_add(&reactor->fds_registered, 1);
1033 n_log(LOG_DEBUG, "n_reactor: registered socket %d", netw->link.sock);
1034 return 1;
1035}
1036
1037void n_reactor_unregister(n_reactor* reactor, NETWORK* netw) {
1038 if (!reactor || !netw) return;
1039 if (!netw_atomic_read_reactor_mode(netw)) return;
1040 /* EPOLL_CTL_DEL is best-effort, if the socket is already closed
1041 * the kernel returns EBADF, which we silently ignore. */
1042 if (epoll_ctl(reactor->epoll_fd, EPOLL_CTL_DEL, netw->link.sock, NULL) != 0) {
1043 if (errno != EBADF && errno != ENOENT) {
1045 "n_reactor_unregister: epoll_ctl DEL socket %d "
1046 "failed: %s",
1047 netw->link.sock, strerror(errno));
1048 }
1049 }
1050 reactor_recv_state_reset(netw);
1051 reactor_send_state_reset(netw);
1052
1053 /* Remove from registered list. Walk to find, small-N typical. */
1054 pthread_mutex_lock(&reactor->registered_lock);
1055 LIST_NODE* node = reactor->registered->start;
1056 while (node) {
1057 if (node->ptr == (void*)netw) {
1058 remove_list_node(reactor->registered, node, NETWORK);
1059 break;
1060 }
1061 node = node->next;
1062 }
1063 pthread_mutex_unlock(&reactor->registered_lock);
1064
1065 /* Defensive removal from dirty_pending. A racing producer
1066 * could have CAS'd in_dirty_list to 1 between the wake handler's
1067 * splice and our unregister. Walk + remove if present, then clear
1068 * in_dirty_list BEFORE clearing reactor_mode (below) so a producer
1069 * that observes the cleared mode never leaves a stale flag. */
1070 pthread_mutex_lock(&reactor->dirty_lock);
1071 {
1072 LIST_NODE* dn = reactor->dirty_pending->start;
1073 while (dn) {
1074 if (dn->ptr == (void*)netw) {
1075 remove_list_node(reactor->dirty_pending, dn, NETWORK);
1076 break;
1077 }
1078 dn = dn->next;
1079 }
1080 }
1081 pthread_mutex_unlock(&reactor->dirty_lock);
1082 __atomic_store_n(&netw->in_dirty_list, 0, __ATOMIC_RELEASE);
1083
1084 /* Clear reactor_mode first (release) so any thread that observes
1085 * the cleared flag will not subsequently dereference an invalid
1086 * reactor_handle pointer. The handle is then nulled, a brief
1087 * window exists where mode == 0 but handle != NULL, which is
1088 * harmless because every consumer guards on mode first. */
1091 atomic_fetch_add(&reactor->fds_unregistered, 1);
1092 n_log(LOG_DEBUG, "n_reactor: unregistered socket %d", netw->link.sock);
1093
1094 /* Publish the close ack as the very last action. Every unregister
1095 * path, the EXIT_ASKED sweep, a peer EOF / EPOLLRDHUP, or a hard
1096 * I/O error, must release a game thread that may already be
1097 * spinning in n_reactor_close_netw_sync; only the sweep used to do
1098 * this, so an event-driven unregister stranded the close (TSan
1099 * timeout). The release store orders every side effect above before
1100 * the game thread's acquire-load, after which it may close(fd) and
1101 * free the NETWORK, so nothing below this line may touch `netw`. */
1102 __atomic_store_n(&netw->reactor_close_acked, 1, __ATOMIC_RELEASE);
1103}
1104
1106 if (!netw) return;
1108 if (!r) return;
1109 if (r->wake_efd < 0) return;
1110
1111 /* Dirty-list: add this NETWORK to the reactor's pending list via
1112 * a CAS guard so only the first push between drains takes the
1113 * dirty_lock. Multiple producer threads pushing to the SAME
1114 * NETWORK between drains see CAS fail and skip the push (the first
1115 * one already enqueued it). The reactor clears in_dirty_list BEFORE
1116 * draining so any producer that pushes during the drain succeeds
1117 * the CAS and re-adds for the next pass. */
1118 {
1119 int expected = 0;
1120 if (__atomic_compare_exchange_n(&netw->in_dirty_list, &expected, 1,
1121 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
1122 pthread_mutex_lock(&r->dirty_lock);
1123 list_push(r->dirty_pending, netw, NULL); /* NULL dtor: alias only */
1124 pthread_mutex_unlock(&r->dirty_lock);
1125 }
1126 }
1127
1128 uint64_t one = 1;
1129 ssize_t w = write(r->wake_efd, &one, sizeof(one));
1130 (void)w; /* best-effort; eventfd write of 8 bytes either succeeds
1131 * fully or returns EAGAIN if the counter is at UINT64_MAX
1132 * minus 1, which is impossible in practice */
1133}
1134
1136 if (!netw) return;
1137
1138 /* Step 1: if the connection is still registered, prod the reactor to
1139 * tear it down. We must NOT gate the whole function on reactor_mode:
1140 * the reactor can clear reactor_mode mid-unregister (peer EOF / hard
1141 * error) and then keep touching the NETWORK for a few more lines
1142 * (the EPOLL_CTL_DEL, list removal, the unregistered-socket log read
1143 * of link.sock) before publishing the ack. Returning early on
1144 * reactor_mode == 0 would let the caller free the NETWORK under those
1145 * accesses (use-after-free). Instead we always wait for the ack
1146 * below; this prod is only needed when the reactor has not yet begun
1147 * the teardown. The reactor_mode read is racy but only steers the
1148 * (idempotent) prod, not the free, so the raciness is benign.
1149 *
1150 * netw_set serialises under eventbolt; n_reactor_notify_send is a
1151 * no-op once reactor_handle has been nulled, which is fine because in
1152 * that case the ack is already set or imminently will be. */
1156 }
1157
1158 /* Step 2: wait for the reactor to ack. Busy-poll with a 1 ms sleep,
1159 * same shape as the existing nb_running_threads spin in netw_close.
1160 * The ack is published by n_reactor_unregister as the very last thing
1161 * the reactor ever does with this NETWORK, regardless of which path
1162 * unregistered it (the EXIT_ASKED sweep in the common case, but also
1163 * a concurrent peer EOF / hard error). reactor_mode is always cleared
1164 * before the ack store, so once we observe reactor_mode == 0 the ack
1165 * is guaranteed to land. The acquire-load pairs with unregister's
1166 * release store so all of its side effects (epoll DEL, cleared
1167 * reactor_mode, freed accumulators, list removal, the final log) are
1168 * visible, and ordered before, the caller's subsequent close(fd) /
1169 * free. */
1170 while (!__atomic_load_n(&netw->reactor_close_acked, __ATOMIC_ACQUIRE)) {
1171 struct timespec ts = {0, 1000000L}; /* 1 ms */
1172 nanosleep(&ts, NULL);
1173 }
1174}
1175
1177 size_t send_list_limit,
1178 size_t recv_list_limit,
1179 int blocking,
1180 n_reactor* reactor,
1181 int* retval) {
1182 if (!reactor) {
1183 n_log(LOG_ERR, "netw_accept_into_reactor: NULL reactor");
1184 if (retval) *retval = EINVAL;
1185 return NULL;
1186 }
1187 NETWORK* netw = netw_accept_from_ex(listener, send_list_limit,
1188 recv_list_limit, blocking, retval);
1189 if (!netw) return NULL;
1190 /* Register before returning so the caller can't accidentally
1191 * call netw_start_thr_engine on a reactor-mode connection.
1192 * Failure path closes the freshly-accepted socket. */
1193 if (!n_reactor_register(reactor, netw)) {
1194 n_log(LOG_ERR, "netw_accept_into_reactor: register failed for socket %d",
1195 netw->link.sock);
1196 netw_close(&netw);
1197 if (retval) *retval = EIO;
1198 return NULL;
1199 }
1200 return netw;
1201}
1202
1203#else /* N_REACTOR_AVAILABLE */
1204
1205/* non-Linux stubs */
1206
1207n_reactor* n_reactor_new(int max_fds_hint) {
1208 (void)max_fds_hint;
1210 "n_reactor_new: epoll/eventfd not available on this "
1211 "platform; reactor mode unsupported (use thread mode)");
1212 return NULL;
1213}
1214
1216 if (reactor) *reactor = NULL;
1217}
1218
1219void n_reactor_run(n_reactor* reactor) {
1220 (void)reactor;
1221}
1222
1224 (void)reactor;
1225}
1226
1228 (void)arg;
1229 return NULL;
1230}
1231
1233 (void)reactor;
1234 if (out) memset(out, 0, sizeof(*out));
1235}
1236
1238 (void)reactor;
1239 (void)netw;
1240 return 0;
1241}
1242
1244 (void)reactor;
1245 (void)netw;
1246}
1247
1249 (void)netw;
1250}
1251
1253 (void)netw;
1254}
1255
1257 size_t send_list_limit,
1258 size_t recv_list_limit,
1259 int blocking,
1260 n_reactor* reactor,
1261 int* retval) {
1262 (void)listener;
1263 (void)send_list_limit;
1264 (void)recv_list_limit;
1265 (void)blocking;
1266 (void)reactor;
1267 if (retval) *retval = ENOSYS;
1268 return NULL;
1269}
1270
1271#endif /* N_REACTOR_AVAILABLE */
NETWORK * netw
Network for server mode, accepting incomming.
Definition ex_network.c:39
#define FreeNoLog(__ptr)
Free Handler without log.
Definition n_common.h:272
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:263
void * ptr
void pointer to store
Definition n_list.h:46
LIST_NODE * start
pointer to the start of the list
Definition n_list.h:66
size_t nb_items
number of item currently in the list
Definition n_list.h:61
struct LIST_NODE * next
pointer to the next node
Definition n_list.h:52
#define list_shift(__LIST_, __TYPE_)
Shift macro helper for void pointer casting.
Definition n_list.h:96
int list_push(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer to the end of the list.
Definition n_list.c:228
#define remove_list_node(__LIST_, __NODE_, __TYPE_)
Remove macro helper for void pointer casting.
Definition n_list.h:98
int list_destroy(LIST **list)
Empty and Free a list container.
Definition n_list.c:548
LIST * new_generic_list(size_t max_items)
Initialiaze a generic list container to max_items pointers.
Definition n_list.c:37
#define MAX_LIST_ITEMS
flag to pass to new_generic_list for the maximum possible number of item in a list
Definition n_list.h:75
Structure of a generic LIST container.
Definition n_list.h:59
Structure of a generic list node.
Definition n_list.h:44
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:89
#define LOG_DEBUG
debug-level messages
Definition n_log.h:84
#define LOG_ERR
error conditions
Definition n_log.h:76
#define LOG_WARNING
warning conditions
Definition n_log.h:78
#define LOG_INFO
informational
Definition n_log.h:82
N_STR * zip4_nstr(N_STR *src)
Compress src with LZ4 block format.
Definition n_lz4.c:55
N_STR * unzip4_nstr(N_STR *src)
Decompress an N_STR produced by zip4_nstr.
Definition n_lz4.c:107
size_t written
number of meaningful bytes in data, excluding the null terminator; the size including the null termin...
Definition n_str.h:68
char * data
the string
Definition n_str.h:63
size_t length
total allocation (in bytes) of the data buffer, padding included
Definition n_str.h:65
void free_nstr_ptr(void *ptr)
Free a N_STR pointer structure.
Definition n_str.c:70
#define free_nstr(__ptr)
free a N_STR structure and set the pointer to NULL
Definition n_str.h:203
A box including a string and his lenght.
Definition n_str.h:61
int reactor_recv_wants_write
TLS-over-reactor: recv returned NETW_IO_WANT_WRITE, the reactor re-runs the readable drain after the ...
Definition n_network.h:482
N_SOCKET link
networking socket
Definition n_network.h:388
int threaded_engine_status
Threaded network engine state for this network.
Definition n_network.h:315
char * reactor_read_payload
malloc'd accumulator for payload bytes
Definition n_network.h:459
int compress_mode
Per-packet compression mode, see NETW_COMPRESS_MODE.
Definition n_network.h:426
int reactor_write_armed
1 = EPOLLOUT currently registered
Definition n_network.h:473
size_t reactor_send_off
bytes already sent to socket
Definition n_network.h:472
int reactor_read_hdr_have
bytes accumulated in reactor_read_hdr_buf
Definition n_network.h:456
uint32_t reactor_read_pkt_state
state word for the in-flight frame
Definition n_network.h:457
int reactor_read_phase
0=STATE, 1=LENGTH, 2=PAYLOAD
Definition n_network.h:454
int in_dirty_list
Dirty-list membership flag.
Definition n_network.h:492
int reactor_close_acked
Close handshake.
Definition n_network.h:506
pthread_mutex_t recvbolt
mutex for threaded access of recv buf
Definition n_network.h:403
pthread_mutex_t sendbolt
mutex for threaded access of send_buf
Definition n_network.h:401
uint32_t reactor_read_pkt_length
payload length for the in-flight frame
Definition n_network.h:458
size_t reactor_read_payload_have
bytes accumulated into reactor_read_payload
Definition n_network.h:460
char * reactor_send_buf
malloc'd framed bytes, NULL when idle
Definition n_network.h:470
SOCKET sock
a normal socket
Definition n_network.h:293
netw_func recv_data_once
single-attempt recv, same contract as send_data_once.
Definition n_network.h:372
LIST * recv_buf
reveicing buffer (for incomming usage)
Definition n_network.h:393
size_t reactor_send_len
total bytes in reactor_send_buf
Definition n_network.h:471
int reactor_registered
Reactor close-handshake latch.
Definition n_network.h:520
netw_func send_data_once
single-attempt send (non-blocking / reactor use).
Definition n_network.h:369
unsigned char reactor_read_hdr_buf[4]
accumulator for header words
Definition n_network.h:455
int reactor_send_wants_read
TLS-over-reactor: the in-flight send returned NETW_IO_WANT_READ (renegotiation), the reactor retries ...
Definition n_network.h:478
LIST * send_buf
sending buffer (for outgoing queuing )
Definition n_network.h:391
#define NETW_IO_WANT_READ
single-attempt I/O (send_data_once / recv_data_once): the operation cannot progress until the socket ...
Definition n_network.h:77
#define NETW_COMPRESS_THRESHOLD
Opportunistic compression policy.
Definition n_network.h:279
#define NETW_COMPRESS_MIN_RATIO
Definition n_network.h:280
ssize_t recv_data_once(void *netw, char *buf, uint32_t n)
single-attempt recv for non-blocking sockets (reactor use).
Definition n_network.c:4354
ssize_t send_data_once(void *netw, char *buf, uint32_t n)
single-attempt send for non-blocking sockets (reactor use).
Definition n_network.c:4298
#define netw_atomic_write_reactor_handle(netw, val)
Definition n_network.h:556
#define netw_atomic_read_reactor_mode(netw)
Lock-free atomic read of the reactor_mode flag.
Definition n_network.h:550
NETWORK * netw_accept_from_ex(NETWORK *from, size_t send_list_limit, size_t recv_list_limit, int blocking, int *retval)
make a normal 'accept' .
Definition n_network.c:3358
#define netw_atomic_write_reactor_mode(netw, val)
Definition n_network.h:551
#define NETW_SOCKET_DISCONNECTED
Code for a disconnected recv.
Definition n_network.h:72
int netw_set(NETWORK *netw, int flag)
Restart or reset the specified network ability.
Definition n_network.c:2554
int netw_get_state(NETWORK *netw, uint32_t *state, int *thr_engine_status)
Get the state of a network.
Definition n_network.c:2532
#define netw_atomic_read_reactor_handle(netw)
Same contract for the back-pointer to the reactor.
Definition n_network.h:555
#define NETW_IO_WANT_WRITE
single-attempt I/O: the operation cannot progress until the socket is WRITABLE.
Definition n_network.h:81
int netw_close(NETWORK **netw)
Closing a specified Network, destroy queues, free the structure.
Definition n_network.c:2662
@ NETW_COMPRESS_NONE
no automatic compression on send, still decompresses inbound
Definition n_network.h:265
@ NETW_COMPRESS_LZ4
compress on send with LZ4, decompress either on recv
Definition n_network.h:269
@ NETW_COMPRESSED_LZ4
Definition n_network.h:283
@ NETW_THR_ENGINE_STARTED
Definition n_network.h:283
@ NETW_COMPRESSED_ZLIB
Definition n_network.h:283
@ NETW_RUN
Definition n_network.h:283
@ NETW_ERROR
Definition n_network.h:283
@ NETW_EXIT_ASKED
Definition n_network.h:283
Structure of a NETWORK.
Definition n_network.h:309
N_STR * unzip_nstr(N_STR *src)
return an uncompressed version of src
Definition n_zlib.c:217
N_STR * zip_nstr(N_STR *src)
return a compressed version of src
Definition n_zlib.c:166
Common headers and low-level functions & define.
List structures and definitions.
Generic log system.
LZ4 block-compression handler.
Network Engine.
void * n_reactor_run_thread_entry(void *arg)
pthread_create-compatible entry point that calls n_reactor_run on the reactor passed via arg.
Definition n_reactor.c:1227
void n_reactor_run(n_reactor *reactor)
Run the epoll loop on the calling thread.
Definition n_reactor.c:1219
void n_reactor_get_stats(const n_reactor *reactor, n_reactor_stats *out)
Read current stats counters into *out.
Definition n_reactor.c:1232
n_reactor * n_reactor_new(int max_fds_hint)
Create a new reactor.
Definition n_reactor.c:1207
NETWORK * netw_accept_into_reactor(NETWORK *listener, size_t send_list_limit, size_t recv_list_limit, int blocking, n_reactor *reactor, int *retval)
Accept a connection on listener and register it with reactor instead of starting per-connection threa...
Definition n_reactor.c:1256
void n_reactor_unregister(n_reactor *reactor, NETWORK *netw)
Unregister a NETWORK from the reactor.
Definition n_reactor.c:1243
int n_reactor_register(n_reactor *reactor, NETWORK *netw)
Register a NETWORK with the reactor.
Definition n_reactor.c:1237
void n_reactor_notify_send(NETWORK *netw)
Producer-side wake-up after a netw_add_msg.
Definition n_reactor.c:1248
void n_reactor_close_netw_sync(NETWORK *netw)
Synchronously close a reactor-registered NETWORK from the game thread.
Definition n_reactor.c:1252
void n_reactor_stop(n_reactor *reactor)
Signal the run loop to exit at the next iteration.
Definition n_reactor.c:1223
void n_reactor_destroy(n_reactor **reactor)
Tear down a reactor.
Definition n_reactor.c:1215
Single-threaded epoll reactor for n_network connections.
long long fds_registered
lifetime register call count
Definition n_reactor.h:82
long long writes_partial
EAGAIN on send -> re-armed EPOLLOUT.
Definition n_reactor.h:80
long long reads_partial
EAGAIN on recv -> kept accumulator.
Definition n_reactor.h:81
long long events_processed
total epoll events dispatched
Definition n_reactor.h:79
long long wake_signals
eventfd wake events processed
Definition n_reactor.h:84
long long wake_walk_visits
Definition n_reactor.h:91
long long wake_walk_drains
Definition n_reactor.h:92
struct n_reactor n_reactor
Opaque reactor handle.
Definition n_reactor.h:74
long long fds_unregistered
lifetime unregister call count
Definition n_reactor.h:83
long long wake_walks
Definition n_reactor.h:90
Counters for the dashboard / profile_server.sh.
Definition n_reactor.h:78
N_STR and string function declaration.
ZLIB compression handler.