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
27
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) {
161 FreeNoLog(netw->reactor_read_payload);
162 netw->reactor_read_phase = 0;
163 netw->reactor_read_hdr_have = 0;
164 netw->reactor_read_pkt_state = 0;
165 netw->reactor_read_pkt_length = 0;
166 netw->reactor_read_payload_have = 0;
167}
168
169/* Free any in-flight send buffer on the NETWORK. */
170static void reactor_send_state_reset(NETWORK* netw) {
171 FreeNoLog(netw->reactor_send_buf);
172 netw->reactor_send_len = 0;
173 netw->reactor_send_off = 0;
174 netw->reactor_write_armed = 0;
175 netw->reactor_send_wants_read = 0;
176 netw->reactor_recv_wants_write = 0;
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/* Does netw->send_buf hold anything? Read under sendbolt, the lock every
190 * producer (netw_add_msg -> list_push) and the consumer
191 * (reactor_send_state_load_next -> list_shift) already take.
192 *
193 * This used to be an unlocked `send_buf->nb_items > 0` "hint" read on the
194 * reactor thread. A stale value was indeed recovered by the next wake, but a
195 * plain read racing a locked write is still a C11 data race (undefined
196 * behaviour: the compiler may tear, cache or reorder it) and it was the one
197 * ThreadSanitizer report every reactor-mode consumer of this library had to
198 * carry as "known". One uncontended mutex per dirty connection per walk is
199 * noise next to the send() that follows. */
200static int reactor_send_queue_nonempty(NETWORK* netw) {
201 if (!netw->send_buf) return 0;
202 pthread_mutex_lock(&netw->sendbolt);
203 int nonempty = (netw->send_buf->nb_items > 0);
204 pthread_mutex_unlock(&netw->sendbolt);
205 return nonempty;
206}
207
208/* Pop the next N_STR from netw->send_buf (under sendbolt) and pre-frame
209 * it into a contiguous buffer ready to send: 4-byte state word + 4-byte
210 * payload length + payload bytes, with optional zlib/lz4 compression
211 * applied to the payload (same gating as netw_send_func: compress_mode
212 * != NONE && payload >= NETW_COMPRESS_THRESHOLD && ratio >= 10 %).
213 *
214 * On success populates netw->reactor_send_buf / _len / _off and returns
215 * 1. On nothing-to-send returns 0. On allocation failure returns -1
216 * (caller should treat as fatal).
217 */
218static int reactor_send_state_load_next(NETWORK* netw) {
219 if (netw->reactor_send_buf) return 1; /* already loaded */
220
221 pthread_mutex_lock(&netw->sendbolt);
222 N_STR* msg = list_shift(netw->send_buf, N_STR);
223 pthread_mutex_unlock(&netw->sendbolt);
224 if (!msg) return 0;
225
226 /* Optional compression, same gating as netw_send_func, including
227 * the per-NETWORK compress_mode choice (NONE / ZLIB / LZ4) and
228 * the threshold + ratio macros (NETW_COMPRESS_THRESHOLD,
229 * NETW_COMPRESS_MIN_RATIO) shared with the thread engine via the
230 * public n_network.h. */
231 uint32_t pkt_state = NETW_RUN;
232 if (netw->compress_mode != NETW_COMPRESS_NONE &&
233 msg->written >= (size_t)NETW_COMPRESS_THRESHOLD) {
234 N_STR* zipped = NULL;
235 uint32_t flag_bit = 0;
236 if (netw->compress_mode == NETW_COMPRESS_LZ4) {
237 zipped = zip4_nstr(msg);
238 flag_bit = NETW_COMPRESSED_LZ4;
239 } else {
240 zipped = zip_nstr(msg);
241 flag_bit = NETW_COMPRESSED_ZLIB;
242 }
243 if (zipped && zipped->written > 0 &&
244 zipped->written * 100u <=
245 msg->written * (100u - NETW_COMPRESS_MIN_RATIO)) {
246 /* Saved at least NETW_COMPRESS_MIN_RATIO %, swap. */
247 free_nstr(&msg);
248 msg = zipped;
249 pkt_state |= flag_bit;
250 } else if (zipped) {
251 free_nstr(&zipped);
252 }
253 }
254
255 if (msg->written > UINT32_MAX) {
256 n_log(LOG_ERR, "n_reactor: payload too large (%zu > UINT32_MAX); dropping",
257 msg->written);
258 free_nstr(&msg);
259 return 0;
260 }
261 size_t total = 8 + msg->written;
262 char* buf = NULL;
263 Malloc(buf, char, total);
264 if (!buf) {
265 free_nstr(&msg);
266 return -1;
267 }
268 uint32_t state_be = htonl(pkt_state);
269 uint32_t length_be = htonl((uint32_t)msg->written);
270 memcpy(buf, &state_be, 4);
271 memcpy(buf + 4, &length_be, 4);
272 memcpy(buf + 8, msg->data, msg->written);
273 free_nstr(&msg);
274
275 netw->reactor_send_buf = buf;
276 netw->reactor_send_len = total;
277 netw->reactor_send_off = 0;
278 return 1;
279}
280
281/* Drain as much of netw->reactor_send_buf as the kernel will accept,
282 * loop to load the next queued frame on completion. Returns:
283 * 1, drained fully (send_buf empty, no in-flight). Caller disarms
284 * EPOLLOUT.
285 * 0, back-pressure (EAGAIN). Caller arms EPOLLOUT.
286 * -1, hard error / connection closed by peer. Caller unregisters
287 * and flags NETW_ERROR.
288 */
289static int reactor_drain_writes(NETWORK* netw, n_reactor* reactor) {
290 for (;;) {
291 if (!netw->reactor_send_buf) {
292 int r = reactor_send_state_load_next(netw);
293 if (r < 0) return -1;
294 if (r == 0) return 1; /* nothing to send */
295 }
296 size_t remaining = netw->reactor_send_len - netw->reactor_send_off;
297 if (remaining == 0) {
298 /* Buffer fully drained, free and try the next queued msg. */
299 Free(netw->reactor_send_buf);
300 netw->reactor_send_buf = NULL;
301 netw->reactor_send_len = 0;
302 netw->reactor_send_off = 0;
303 continue;
304 }
305 /* TLS-over-reactor: route through the NETWORK's
306 * single-attempt send pointer (raw send() on cleartext,
307 * SSL_write on crypto sockets). EINTR is retried inside the
308 * call; partial-write resume rides the same buffer+offset
309 * thanks to SSL_MODE_ENABLE_PARTIAL_WRITE. */
310 netw->reactor_send_wants_read = 0;
311 uint32_t attempt = (remaining > UINT32_MAX) ? UINT32_MAX : (uint32_t)remaining;
312 ssize_t sent = netw->send_data_once((void*)netw,
313 netw->reactor_send_buf + netw->reactor_send_off,
314 attempt);
315 if (sent > 0) {
316 netw->reactor_send_off += (size_t)sent;
317 continue;
318 }
319 if (sent == NETW_IO_WANT_WRITE) {
320 atomic_fetch_add(&reactor->writes_partial, 1);
321 return 0;
322 }
323 if (sent == NETW_IO_WANT_READ) {
324 /* TLS renegotiation: the write needs inbound bytes first.
325 * Flag it so the readable path retries the drain; the
326 * caller's EPOLLOUT arming is harmless (edge-triggered,
327 * fires at most once while we wait). */
328 netw->reactor_send_wants_read = 1;
329 atomic_fetch_add(&reactor->writes_partial, 1);
330 return 0;
331 }
332 /* NETW_SOCKET_DISCONNECTED / NETW_SOCKET_ERROR (already logged
333 * inside the once-helper). */
334 return -1;
335 }
336}
337
338/* Push a fully-received frame onto the NETWORK's recv_buf. Mirrors
339 * the thread-mode netw_recv_func tail: optionally decompresses
340 * based on the frame's state-word flags, then list_push under the
341 * recvbolt mutex. Frees pkt_payload on failure. */
342static void reactor_recv_dispatch_frame(NETWORK* netw,
343 uint32_t pkt_state,
344 uint32_t pkt_length,
345 char* pkt_payload) {
346 /* Wrap the raw bytes in an N_STR, same shape the thread-mode
347 * recv path produces. */
348 N_STR* msg = NULL;
349 Malloc(msg, N_STR, 1);
350 if (!msg) {
351 Free(pkt_payload);
352 return;
353 }
354 msg->data = pkt_payload;
355 msg->length = (size_t)pkt_length + 1;
356 msg->written = (size_t)pkt_length;
357 /* Ensure NUL-terminator for downstream consumers that treat
358 * the buffer as a C string (decompression's unzip*_nstr
359 * inspects strptr->data[written] in some paths). The buffer
360 * was sized to pkt_length+1 in the caller. */
361 msg->data[pkt_length] = '\0';
362
363 /* Decompression: same logic as netw_recv_func. */
364 int want_zlib = (pkt_state & NETW_COMPRESSED_ZLIB) != 0;
365 int want_lz4 = (pkt_state & NETW_COMPRESSED_LZ4) != 0;
366 if (want_zlib || want_lz4) {
367 N_STR* plain = want_lz4 ? unzip4_nstr(msg) : unzip_nstr(msg);
368 if (plain) {
369 free_nstr(&msg);
370 msg = plain;
371 } else {
373 "n_reactor: failed to decompress payload "
374 "(%" PRIu32 " bytes, codec=%s); dropping",
375 pkt_length, want_lz4 ? "lz4" : "zlib");
376 free_nstr(&msg);
377 return;
378 }
379 }
380
381 pthread_mutex_lock(&netw->recvbolt);
382 if (list_push(netw->recv_buf, msg, free_nstr_ptr) == FALSE) {
383 pthread_mutex_unlock(&netw->recvbolt);
384 n_log(LOG_ERR, "n_reactor: recv_buf list_push failed; dropping frame");
385 free_nstr(&msg);
386 return;
387 }
388 pthread_mutex_unlock(&netw->recvbolt);
389}
390
391/* Sweep the registered list for NETWORKs whose game thread has set
392 * NETW_EXIT_ASKED, drain pending sends best-effort,
393 * shutdown(SHUT_WR) so the peer observes EOF, unregister from the
394 * epoll set, and signal the close ack so the game thread can close
395 * the fd.
396 *
397 * Snapshots pointers under the lock, then processes them with the
398 * lock released, `n_reactor_unregister` reacquires the same lock,
399 * so we mustn't hold it across the call. The snapshot batch caps at
400 * N_REACTOR_BATCH_SIZE; any additional EXIT_ASKED entries are caught
401 * on the next sweep (the sweep runs at the end of every wake batch
402 * + every heartbeat tick, so the lag is bounded). */
403static void reactor_sweep_exit_asked(n_reactor* reactor) {
404 NETWORK* batch[N_REACTOR_BATCH_SIZE];
405 int count = 0;
406
407 pthread_mutex_lock(&reactor->registered_lock);
408 LIST_NODE* node = reactor->registered->start;
409 while (node && count < N_REACTOR_BATCH_SIZE) {
410 NETWORK* n = (NETWORK*)node->ptr;
411 node = node->next;
412 if (!n || !netw_atomic_read_reactor_mode(n)) continue;
413 uint32_t st = 0;
414 int thr_st = 0;
415 netw_get_state(n, &st, &thr_st);
416 if (st & NETW_EXIT_ASKED) {
417 batch[count++] = n;
418 }
419 }
420 pthread_mutex_unlock(&reactor->registered_lock);
421
422 for (int i = 0; i < count; i++) {
423 NETWORK* n = batch[i];
424 /* Best-effort drain, peer is going away so partial is fine.
425 * Swallow the return value: error / EAGAIN both lead to the
426 * same teardown path next. */
427 if (n->reactor_send_buf || reactor_send_queue_nonempty(n)) {
428 (void)reactor_drain_writes(n, reactor);
429 }
430 /* Half-close the write side so the peer's read side observes
431 * EOF immediately. The fd close itself happens later in the
432 * caller's netw_close after `reactor_close_acked` flips.
433 * n_reactor_unregister publishes that ack with a release store
434 * as its final action, so nothing may touch `n` afterwards: the
435 * game thread's acquire-load can observe the ack and free the
436 * NETWORK the instant unregister returns. */
437 shutdown(n->link.sock, SHUT_WR);
438 n_reactor_unregister(reactor, n);
439 }
440}
441
442/* Drain the socket non-blockingly into the NETWORK's recv accumulator.
443 * Parses as many complete frames as the bytes allow; pushes each onto
444 * recv_buf. Edge-triggered: keeps reading until EAGAIN. Returns 1 on
445 * normal progress, 0 if the connection should be torn down (peer
446 * closed cleanly, or a hard error). */
447static int reactor_handle_readable(NETWORK* netw, n_reactor* reactor) {
448 char chunk[4096];
449 int eof = 0;
450
451 /* TLS-over-reactor: route through the NETWORK's
452 * single-attempt recv pointer (raw recv() on cleartext, SSL_read
453 * on crypto sockets). Looping until NETW_IO_WANT_READ both drains
454 * the edge-triggered readiness AND empties OpenSSL's internal
455 * plaintext buffer (SSL_read reports WANT_READ only once that
456 * buffer is dry), so no SSL_pending() poll is needed. */
457 netw->reactor_recv_wants_write = 0;
458
459 for (;;) {
460 ssize_t got = netw->recv_data_once((void*)netw, chunk, (uint32_t)sizeof(chunk));
461 if (got == NETW_SOCKET_DISCONNECTED) {
462 eof = 1;
463 break;
464 }
465 if (got == NETW_IO_WANT_READ) break; /* drained */
466 if (got == NETW_IO_WANT_WRITE) {
467 /* TLS renegotiation: the read needs outbound room first.
468 * Flag it so the run loop arms EPOLLOUT and re-runs this
469 * drain after the socket turns writable. */
470 netw->reactor_recv_wants_write = 1;
471 break;
472 }
473 if (got <= 0) {
474 /* NETW_SOCKET_ERROR: already logged inside the helper. */
475 return 0;
476 }
477
478 /* Feed `got` bytes through the state machine. */
479 const char* p = chunk;
480 size_t rem = (size_t)got;
481 while (rem > 0) {
482 switch (netw->reactor_read_phase) {
483 case 0: /* STATE word */
484 case 1: /* LENGTH word */
485 {
486 size_t need = 4 - (size_t)netw->reactor_read_hdr_have;
487 size_t take = (rem < need) ? rem : need;
488 memcpy(netw->reactor_read_hdr_buf + netw->reactor_read_hdr_have,
489 p, take);
490 netw->reactor_read_hdr_have += (int)take;
491 p += take;
492 rem -= take;
493 if (netw->reactor_read_hdr_have < 4) {
494 /* Need more bytes for this header word. */
495 atomic_fetch_add(&reactor->reads_partial, 1);
496 break;
497 }
498 /* Header word complete. */
499 uint32_t word;
500 memcpy(&word, netw->reactor_read_hdr_buf, sizeof(word));
501 word = ntohl(word);
502 netw->reactor_read_hdr_have = 0;
503 if (netw->reactor_read_phase == 0) {
504 netw->reactor_read_pkt_state = word;
505 if (word == NETW_EXIT_ASKED) {
506 /* Peer requested clean shutdown. Treat as EOF. */
507 eof = 1;
508 rem = 0;
509 break;
510 }
511 netw->reactor_read_phase = 1;
512 } else {
513 if (word > NETW_MSG_MAX_FRAME_SIZE) {
514 /* Hostile / corrupt length prefix. Reject before
515 * allocating: unbounded is a memory-DoS, and
516 * 0xFFFFFFFF would wrap `word + 1` to 0 below,
517 * yielding a zero-byte buffer the PAYLOAD phase
518 * then overflows. Drop the connection. */
520 "n_reactor: rejected oversized frame (%u bytes) exceeding NETW_MSG_MAX_FRAME_SIZE (%lu)",
521 word, (unsigned long)NETW_MSG_MAX_FRAME_SIZE);
522 return 0;
523 }
524 netw->reactor_read_pkt_length = word;
525 /* Allocate the payload buffer. +1 for the NUL
526 * the dispatch helper writes after fill. */
527 if (word == 0) {
528 /* Zero-byte payload: dispatch immediately
529 * with an empty N_STR-shaped buffer. */
530 char* empty = NULL;
531 Malloc(empty, char, 1);
532 if (!empty) {
533 n_log(LOG_ERR, "n_reactor: alloc(empty payload) failed");
534 return 0;
535 }
536 reactor_recv_dispatch_frame(netw,
537 netw->reactor_read_pkt_state,
538 0, empty);
539 netw->reactor_read_phase = 0;
540 } else {
541 Malloc(netw->reactor_read_payload, char, word + 1);
542 if (!netw->reactor_read_payload) {
543 n_log(LOG_ERR, "n_reactor: alloc(%u-byte payload) failed",
544 word);
545 return 0;
546 }
547 netw->reactor_read_payload_have = 0;
548 netw->reactor_read_phase = 2;
549 }
550 }
551 break;
552 }
553 case 2: /* PAYLOAD */
554 {
555 size_t need = (size_t)netw->reactor_read_pkt_length - netw->reactor_read_payload_have;
556 size_t take = (rem < need) ? rem : need;
557 memcpy(netw->reactor_read_payload + netw->reactor_read_payload_have,
558 p, take);
559 netw->reactor_read_payload_have += take;
560 p += take;
561 rem -= take;
562 if (netw->reactor_read_payload_have < netw->reactor_read_pkt_length) {
563 /* Partial payload, stay in payload-read state, wait
564 * for more bytes (next read or next epoll event). */
565 atomic_fetch_add(&reactor->reads_partial, 1);
566 break;
567 }
568 /* Complete payload, dispatch. ownership of the
569 * buffer transfers into the dispatcher. */
570 char* payload = netw->reactor_read_payload;
571 netw->reactor_read_payload = NULL;
572 reactor_recv_dispatch_frame(netw,
573 netw->reactor_read_pkt_state,
574 netw->reactor_read_pkt_length,
575 payload);
576 netw->reactor_read_phase = 0;
577 netw->reactor_read_payload_have = 0;
578 break;
579 }
580 }
581 }
582 }
583 if (eof) return 0;
584 return 1;
585}
586
587/* public API */
588
589n_reactor* n_reactor_new(int max_fds_hint) {
590 (void)max_fds_hint; /* per-fd table grows on demand at register time */
591
592 n_reactor* r = NULL;
593 Malloc(r, n_reactor, 1); /* calloc-backed: r is zero-initialised */
594 __n_assert(r, return NULL);
595 r->epoll_fd = -1;
596 r->stop_efd = -1;
597 r->wake_efd = -1;
598
599 /* CLOEXEC because forking the asset auditor (server SIGHUP path
600 * does this) shouldn't leak the epoll fd to the child. */
601 r->epoll_fd = epoll_create1(EPOLL_CLOEXEC);
602 if (r->epoll_fd < 0) {
603 n_log(LOG_ERR, "n_reactor_new: epoll_create1 failed: %s",
604 strerror(errno));
605 goto fail;
606 }
607
608 /* Both eventfds: nonblocking (so drain doesn't block the run
609 * loop) and CLOEXEC (same fork hygiene). EFD_SEMAPHORE is NOT
610 * set, we want each `write` to add to the count, not sit
611 * one-event-at-a-time. */
612 r->stop_efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
613 if (r->stop_efd < 0) {
614 n_log(LOG_ERR, "n_reactor_new: eventfd(stop) failed: %s",
615 strerror(errno));
616 goto fail;
617 }
618 r->wake_efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
619 if (r->wake_efd < 0) {
620 n_log(LOG_ERR, "n_reactor_new: eventfd(wake) failed: %s",
621 strerror(errno));
622 goto fail;
623 }
624
625 if (!register_internal_efd(r->epoll_fd, r->stop_efd, N_REACTOR_TAG_STOP) ||
626 !register_internal_efd(r->epoll_fd, r->wake_efd, N_REACTOR_TAG_WAKE)) {
627 goto fail;
628 }
629
630 r->registered = new_generic_list(MAX_LIST_ITEMS);
631 if (!r->registered) {
632 n_log(LOG_ERR, "n_reactor_new: cannot allocate registered list");
633 goto fail;
634 }
635 pthread_mutex_init(&r->registered_lock, NULL);
636
637 atomic_store(&r->events_processed, 0);
638 atomic_store(&r->writes_partial, 0);
639 atomic_store(&r->reads_partial, 0);
640 atomic_store(&r->fds_registered, 0);
641 atomic_store(&r->fds_unregistered, 0);
642 atomic_store(&r->wake_signals, 0);
643 atomic_store(&r->wake_walks, 0);
644 atomic_store(&r->wake_walk_visits, 0);
645 atomic_store(&r->wake_walk_drains, 0);
646 r->stop_requested = 0;
647
648 /* Dirty-list: see n_reactor.h dirty_pending field comment for the
649 * design rationale. */
650 r->dirty_pending = new_generic_list(MAX_LIST_ITEMS);
651 if (!r->dirty_pending) {
652 n_log(LOG_ERR, "n_reactor_new: cannot allocate dirty_pending list");
653 goto fail;
654 }
655 pthread_mutex_init(&r->dirty_lock, NULL);
656
657 n_log(LOG_INFO, "n_reactor_new: created (epoll_fd=%d stop_efd=%d wake_efd=%d)",
658 r->epoll_fd, r->stop_efd, r->wake_efd);
659 return r;
660
661fail:
662 if (r) {
663 if (r->wake_efd >= 0) close(r->wake_efd);
664 if (r->stop_efd >= 0) close(r->stop_efd);
665 if (r->epoll_fd >= 0) close(r->epoll_fd);
666 Free(r);
667 }
668 return NULL;
669}
670
671void n_reactor_destroy(n_reactor** reactor) {
672 if (!reactor || !*reactor) return;
673 n_reactor* r = *reactor;
674
675 /* If the run loop is still on another thread, the caller
676 * should have called `n_reactor_stop` and joined that thread
677 * already. We don't enforce, best-effort cleanup either way. */
678 if (r->wake_efd >= 0) close(r->wake_efd);
679 if (r->stop_efd >= 0) close(r->stop_efd);
680 if (r->epoll_fd >= 0) close(r->epoll_fd);
681 if (r->registered) {
682 list_destroy(&r->registered); /* nodes hold raw NETWORK *, no destructor */
683 pthread_mutex_destroy(&r->registered_lock);
684 }
685 if (r->dirty_pending) {
686 list_destroy(&r->dirty_pending); /* same: nodes are NETWORK* aliases, owner == registered list */
687 pthread_mutex_destroy(&r->dirty_lock);
688 }
689
690 Free(r);
691 *reactor = NULL;
692}
693
694void n_reactor_run(n_reactor* reactor) {
695 if (!reactor) return;
696
697 struct epoll_event events[N_REACTOR_BATCH_SIZE];
698
699 while (!reactor->stop_requested) {
700 /* Heartbeat: bound the wait so the EXIT_ASKED sweep runs at
701 * least once per second even if nobody calls
702 * n_reactor_notify_send / n_reactor_close_netw_sync. The
703 * notify path remains the immediate one (sub-millisecond);
704 * the heartbeat is the safety net. */
705 int n = epoll_wait(reactor->epoll_fd, events,
706 N_REACTOR_BATCH_SIZE, 1000);
707 if (n < 0) {
708 if (errno == EINTR) continue;
709 n_log(LOG_ERR, "n_reactor_run: epoll_wait failed: %s",
710 strerror(errno));
711 break;
712 }
713 if (n == 0) {
714 /* Heartbeat tick: nothing on the wire, but a connection
715 * may have been flagged for teardown without an explicit
716 * notify. */
717 reactor_sweep_exit_asked(reactor);
718 continue;
719 }
720
721 for (int i = 0; i < n; i++) {
722 uint64_t tag = events[i].data.u64;
723 atomic_fetch_add(&reactor->events_processed, 1);
724
725 if (tag == N_REACTOR_TAG_STOP) {
726 /* Drain the eventfd so it doesn't refire, then
727 * mark the loop for exit. We don't break
728 * immediately, finish processing this batch so
729 * any concurrent NETWORK events still get drained. */
730 drain_eventfd(reactor->stop_efd);
731 reactor->stop_requested = 1;
732 continue;
733 }
734 if (tag == N_REACTOR_TAG_WAKE) {
735 /* Producer-side wakeup. Drain the eventfd, then scan
736 * registered NETWORKs for any with pending sends and
737 * try to drain them, for the common no-back-pressure
738 * case that finishes the work before the next
739 * epoll_wait, avoiding the EPOLLOUT round trip. */
740 long long drained = drain_eventfd(reactor->wake_efd);
741 atomic_fetch_add(&reactor->wake_signals, drained);
742
743 /* Wake-handler walks ONLY the dirty list. Splice
744 * dirty_pending into a local list under a single lock
745 * pair so producers can continue pushing onto the fresh
746 * list (picked up on the next wake) without contending
747 * with the drain loop. */
748 atomic_fetch_add(&reactor->wake_walks, 1);
749 long long visits_this_walk = 0;
750 long long drains_this_walk = 0;
751
752 LIST* walk_list = NULL;
753 int walk_owned = 0; /* 1 = we allocated walk_list, must list_destroy */
754 pthread_mutex_t* walk_lock = NULL;
755 pthread_mutex_lock(&reactor->dirty_lock);
756 {
757 LIST* old = reactor->dirty_pending;
759 if (fresh) {
760 reactor->dirty_pending = fresh;
761 walk_list = old;
762 walk_owned = 1;
763 } else {
764 /* Allocation failure: fall back to walking in-place
765 * under the lock. Suboptimal but correct. */
766 walk_list = old;
767 walk_lock = &reactor->dirty_lock;
768 }
769 }
770 if (walk_owned) {
771 pthread_mutex_unlock(&reactor->dirty_lock);
772 }
773
774 LIST_NODE* node = walk_list ? walk_list->start : NULL;
775 while (node) {
776 NETWORK* netw = (NETWORK*)node->ptr;
777 LIST_NODE* next = node->next;
778 /* Every entry is by construction a NETWORK with
779 * pending data when it was pushed; the reactor_mode
780 * gate stays for safety against concurrent unregister. */
782 visits_this_walk++;
783 /* Clear the in-list flag BEFORE draining.
784 * A producer that pushes during the drain succeeds
785 * the CAS and re-adds for the next wake; no
786 * message can be lost. */
787 __atomic_store_n(&netw->in_dirty_list, 0, __ATOMIC_RELEASE);
788 /* Quick check: nothing to do if both buffers
789 * (in-flight + queue) are empty. reactor_send_buf
790 * is reactor-thread-only; the queue count is read
791 * under sendbolt (see reactor_send_queue_nonempty).
792 * A push landing right after the check re-marks
793 * the connection dirty and wakes the next walk. */
794 int has_inflight = (netw->reactor_send_buf != NULL);
795 int has_queued = reactor_send_queue_nonempty(netw);
796 if (has_inflight || has_queued) {
797 drains_this_walk++;
798 int rc = reactor_drain_writes(netw, reactor);
799 if (rc == 0) {
800 /* EAGAIN, arm EPOLLOUT for back-pressure relief. */
801 if (!netw->reactor_write_armed) {
802 if (reactor_epoll_mod(reactor, netw,
803 EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET)) {
804 netw->reactor_write_armed = 1;
805 }
806 }
807 } else if (rc < 0) {
808 /* n_reactor_unregister takes its own
809 * locks (registered_lock + dirty_lock).
810 * Release whatever walk-side lock we
811 * currently hold first to preserve the
812 * documented ordering and avoid
813 * self-deadlock; reacquire after. Set the
814 * state flag BEFORE unregister: unregister
815 * publishes the close ack as its last act,
816 * after which the game thread may free the
817 * NETWORK, so netw must not be touched. */
818 if (walk_lock) pthread_mutex_unlock(walk_lock);
820 n_reactor_unregister(reactor, netw);
821 if (walk_lock) pthread_mutex_lock(walk_lock);
822 } else {
823 /* Fully drained, disarm EPOLLOUT if armed. */
824 if (netw->reactor_write_armed) {
825 if (reactor_epoll_mod(reactor, netw,
826 EPOLLIN | EPOLLRDHUP | EPOLLET)) {
827 netw->reactor_write_armed = 0;
828 }
829 }
830 }
831 }
832 }
833 node = next;
834 }
835 /* Release whatever walk-side lock we still hold
836 * (dirty_lock in the fallback path, none for the
837 * spliced-list happy path), then free the spliced list
838 * if we owned it. */
839 if (walk_lock) pthread_mutex_unlock(walk_lock);
840 if (walk_owned && walk_list) {
841 list_destroy(&walk_list); /* nodes are NETWORK* aliases, no destructor */
842 }
843 /* Flush per-walk visit/drain tally into the lifetime
844 * accumulators after dropping the lock. */
845 atomic_fetch_add(&reactor->wake_walk_visits, visits_this_walk);
846 atomic_fetch_add(&reactor->wake_walk_drains, drains_this_walk);
847 continue;
848 }
849
850 /* NETWORK event. data.ptr points at the NETWORK *.
851 * We rely on data.u64 >= N_REACTOR_TAG_INTERNAL_MAX
852 * because every legitimate pointer is far above 16 in
853 * any modern address space. */
854 NETWORK* netw = (NETWORK*)events[i].data.ptr;
855 if (!netw) continue;
856
857 uint32_t evmask = events[i].events;
858 if (evmask & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) {
859 /* Hard error or peer closed: flag the NETWORK so the
860 * game thread observes it on next netw_get_msg via the
861 * existing state-flag check, THEN unregister. Order
862 * matters: unregister publishes the close ack as its
863 * last act, releasing a game thread that may be spinning
864 * in n_reactor_close_netw_sync and free the NETWORK, so
865 * netw must not be touched after unregister returns. */
867 n_reactor_unregister(reactor, netw);
868 continue;
869 }
870 if (evmask & EPOLLIN) {
871 if (!reactor_handle_readable(netw, reactor)) {
872 /* EOF or unrecoverable read error. Flag before
873 * unregister (unregister publishes the close ack
874 * last; netw may be freed the moment it returns). */
876 n_reactor_unregister(reactor, netw);
877 continue;
878 }
879 /* TLS plumbing: a send that returned
880 * WANT_READ (renegotiation) can progress now that
881 * inbound bytes arrived. */
882 if (netw->reactor_send_wants_read) {
883 int rc = reactor_drain_writes(netw, reactor);
884 if (rc < 0) {
885 /* Flag before unregister: unregister publishes
886 * the close ack last and netw may be freed once
887 * it returns. */
889 n_reactor_unregister(reactor, netw);
890 continue;
891 }
892 if (rc > 0 && netw->reactor_write_armed &&
893 !netw->reactor_recv_wants_write) {
894 if (reactor_epoll_mod(reactor, netw,
895 EPOLLIN | EPOLLRDHUP | EPOLLET)) {
896 netw->reactor_write_armed = 0;
897 }
898 } else if (rc == 0 && !netw->reactor_write_armed) {
899 if (reactor_epoll_mod(reactor, netw,
900 EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET)) {
901 netw->reactor_write_armed = 1;
902 }
903 }
904 }
905 /* TLS plumbing: a recv that returned WANT_WRITE needs
906 * the socket writable: arm EPOLLOUT so the drain
907 * re-runs from that branch. */
908 if (netw->reactor_recv_wants_write && !netw->reactor_write_armed) {
909 if (reactor_epoll_mod(reactor, netw,
910 EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET)) {
911 netw->reactor_write_armed = 1;
912 }
913 }
914 }
915 if (evmask & EPOLLOUT) {
916 /* TLS plumbing: a recv blocked on WANT_WRITE
917 * retries first, the socket just turned writable. */
918 if (netw->reactor_recv_wants_write) {
919 if (!reactor_handle_readable(netw, reactor)) {
920 /* Flag before unregister: unregister publishes
921 * the close ack last and netw may be freed once
922 * it returns. */
924 n_reactor_unregister(reactor, netw);
925 continue;
926 }
927 }
928 int rc = reactor_drain_writes(netw, reactor);
929 if (rc < 0) {
930 /* Flag before unregister: unregister publishes the
931 * close ack last and netw may be freed once it
932 * returns. */
934 n_reactor_unregister(reactor, netw);
935 continue;
936 }
937 if (rc > 0 && netw->reactor_write_armed &&
938 !netw->reactor_recv_wants_write) {
939 /* Drained; disarm EPOLLOUT to avoid spurious wakeups
940 * (kept armed while a renegotiating recv still
941 * needs the writable signal). */
942 if (reactor_epoll_mod(reactor, netw,
943 EPOLLIN | EPOLLRDHUP | EPOLLET)) {
944 netw->reactor_write_armed = 0;
945 }
946 }
947 /* rc == 0 (back-pressure / WANT) means EPOLLOUT remains
948 * armed, kernel will fire again when buffer has room. */
949 }
950 }
951 /* End-of-batch teardown sweep, pick up any NETWORK whose
952 * game-side has flagged EXIT_ASKED during this batch (or
953 * earlier batches that didn't reach the snapshot cap). */
954 reactor_sweep_exit_asked(reactor);
955 }
956
957 n_log(LOG_INFO, "n_reactor_run: exiting (events=%lld)",
958 (long long)atomic_load(&reactor->events_processed));
959}
960
961void n_reactor_stop(n_reactor* reactor) {
962 if (!reactor || reactor->stop_efd < 0) return;
963 /* eventfd write semantics: any 8-byte value > 0 increments the
964 * counter and triggers EPOLLIN on the read side. Use 1 for
965 * clarity. */
966 uint64_t one = 1;
967 ssize_t w = write(reactor->stop_efd, &one, sizeof(one));
968 (void)w; /* best-effort; if it fails the loop will exit on next
969 * idle epoll_wait via stop_requested check */
970}
971
972void* n_reactor_run_thread_entry(void* arg) {
974 return NULL;
975}
976
977void n_reactor_get_stats(const n_reactor* reactor, n_reactor_stats* out) {
978 if (!out) return;
979 if (!reactor) {
980 memset(out, 0, sizeof(*out));
981 return;
982 }
983 out->events_processed = atomic_load(&reactor->events_processed);
984 out->writes_partial = atomic_load(&reactor->writes_partial);
985 out->reads_partial = atomic_load(&reactor->reads_partial);
986 out->fds_registered = atomic_load(&reactor->fds_registered);
987 out->fds_unregistered = atomic_load(&reactor->fds_unregistered);
988 out->wake_signals = atomic_load(&reactor->wake_signals);
989 out->wake_walks = atomic_load(&reactor->wake_walks);
990 out->wake_walk_visits = atomic_load(&reactor->wake_walk_visits);
991 out->wake_walk_drains = atomic_load(&reactor->wake_walk_drains);
992}
993
995 if (!reactor || !netw) return 0;
997 n_log(LOG_WARNING, "n_reactor_register: socket %d already in reactor mode",
998 netw->link.sock);
999 return 0;
1000 }
1001 if (netw->threaded_engine_status == NETW_THR_ENGINE_STARTED) {
1002 n_log(LOG_ERR,
1003 "n_reactor_register: socket %d has thread engine started; "
1004 "reactor and thread mode are mutually exclusive",
1005 netw->link.sock);
1006 return 0;
1007 }
1008 if (!set_nonblocking(netw->link.sock)) {
1009 n_log(LOG_ERR, "n_reactor_register: O_NONBLOCK on socket %d failed: %s",
1010 netw->link.sock, strerror(errno));
1011 return 0;
1012 }
1013 /* The reactor's I/O goes through the single-attempt
1014 * pointers (TLS-aware). netw_new seeds them (and the crypto setup
1015 * swaps in the SSL variants), but a hand-constructed NETWORK
1016 * shell (tests, embedders) may leave them NULL, default to the
1017 * raw cleartext variants rather than dereferencing NULL. */
1018 if (!netw->send_data_once) netw->send_data_once = &send_data_once;
1019 if (!netw->recv_data_once) netw->recv_data_once = &recv_data_once;
1020
1021 /* Reset accumulator state from any prior use of the slot. */
1022 reactor_recv_state_reset(netw);
1023 reactor_send_state_reset(netw);
1024 /* Clear stale ack from a prior cycle. This runs before the netw is
1025 * published to the reactor thread (reactor_mode release, below), so
1026 * it cannot race the sweep; use the same atomic accessor for
1027 * consistency with the release/acquire pair on this flag. */
1028 __atomic_store_n(&netw->reactor_close_acked, 0, __ATOMIC_RELEASE);
1029
1030 struct epoll_event ev;
1031 /* Edge-triggered: drain the socket fully on each event. EPOLLRDHUP
1032 * gives us peer-half-close as a separate signal. EPOLLOUT is added
1033 * lazily by the send path when there's pending data. */
1034 ev.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
1035 ev.data.ptr = netw;
1036 if (epoll_ctl(reactor->epoll_fd, EPOLL_CTL_ADD, netw->link.sock, &ev) != 0) {
1037 n_log(LOG_ERR, "n_reactor_register: epoll_ctl ADD socket %d failed: %s",
1038 netw->link.sock, strerror(errno));
1039 return 0;
1040 }
1041 netw_atomic_write_reactor_handle(netw, (void*)reactor);
1042 /* Latch that this NETWORK now owes a reactor close-handshake ack.
1043 * Set only here, on full registration success, and never cleared by
1044 * the reactor; netw_close keys its ack-wait on this rather than on
1045 * the live reactor_mode (see the field doc in n_network.h). The
1046 * register failure paths above all return 0 before this point, so a
1047 * caller that netw_close's a never-registered NETWORK leaves the
1048 * latch 0 and avoids an unanswerable wait. */
1049 __atomic_store_n(&netw->reactor_registered, 1, __ATOMIC_RELEASE);
1050 /* Publish reactor_mode last (release on the flag pairs with the
1051 * acquire reads in netw_close / netw_add_msg) so any thread that
1052 * sees reactor_mode == 1 also sees a non-NULL reactor_handle. */
1054
1055 /* Track in the reactor's registered list so the wake-event
1056 * handler can find this NETWORK without a tree lookup. The
1057 * list_push happens with NULL destructor, nodes hold raw
1058 * NETWORK *, ownership stays with the caller. */
1059 pthread_mutex_lock(&reactor->registered_lock);
1060 list_push(reactor->registered, netw, NULL);
1061 pthread_mutex_unlock(&reactor->registered_lock);
1062
1063 atomic_fetch_add(&reactor->fds_registered, 1);
1064 n_log(LOG_DEBUG, "n_reactor: registered socket %d", netw->link.sock);
1065 return 1;
1066}
1067
1068void n_reactor_unregister(n_reactor* reactor, NETWORK* netw) {
1069 if (!reactor || !netw) return;
1070 if (!netw_atomic_read_reactor_mode(netw)) return;
1071 /* EPOLL_CTL_DEL is best-effort, if the socket is already closed
1072 * the kernel returns EBADF, which we silently ignore. */
1073 if (epoll_ctl(reactor->epoll_fd, EPOLL_CTL_DEL, netw->link.sock, NULL) != 0) {
1074 if (errno != EBADF && errno != ENOENT) {
1076 "n_reactor_unregister: epoll_ctl DEL socket %d "
1077 "failed: %s",
1078 netw->link.sock, strerror(errno));
1079 }
1080 }
1081 reactor_recv_state_reset(netw);
1082 reactor_send_state_reset(netw);
1083
1084 /* Remove from registered list. Walk to find, small-N typical. */
1085 pthread_mutex_lock(&reactor->registered_lock);
1086 LIST_NODE* node = reactor->registered->start;
1087 while (node) {
1088 if (node->ptr == (void*)netw) {
1089 remove_list_node(reactor->registered, node, NETWORK);
1090 break;
1091 }
1092 node = node->next;
1093 }
1094 pthread_mutex_unlock(&reactor->registered_lock);
1095
1096 /* Defensive removal from dirty_pending. A racing producer
1097 * could have CAS'd in_dirty_list to 1 between the wake handler's
1098 * splice and our unregister. Walk + remove if present, then clear
1099 * in_dirty_list BEFORE clearing reactor_mode (below) so a producer
1100 * that observes the cleared mode never leaves a stale flag. */
1101 pthread_mutex_lock(&reactor->dirty_lock);
1102 {
1103 LIST_NODE* dn = reactor->dirty_pending->start;
1104 while (dn) {
1105 if (dn->ptr == (void*)netw) {
1106 remove_list_node(reactor->dirty_pending, dn, NETWORK);
1107 break;
1108 }
1109 dn = dn->next;
1110 }
1111 }
1112 pthread_mutex_unlock(&reactor->dirty_lock);
1113 __atomic_store_n(&netw->in_dirty_list, 0, __ATOMIC_RELEASE);
1114
1115 /* Clear reactor_mode first (release) so any thread that observes
1116 * the cleared flag will not subsequently dereference an invalid
1117 * reactor_handle pointer. The handle is then nulled, a brief
1118 * window exists where mode == 0 but handle != NULL, which is
1119 * harmless because every consumer guards on mode first. */
1122 atomic_fetch_add(&reactor->fds_unregistered, 1);
1123 n_log(LOG_DEBUG, "n_reactor: unregistered socket %d", netw->link.sock);
1124
1125 /* Publish the close ack as the very last action. Every unregister
1126 * path, the EXIT_ASKED sweep, a peer EOF / EPOLLRDHUP, or a hard
1127 * I/O error, must release a game thread that may already be
1128 * spinning in n_reactor_close_netw_sync; only the sweep used to do
1129 * this, so an event-driven unregister stranded the close (TSan
1130 * timeout). The release store orders every side effect above before
1131 * the game thread's acquire-load, after which it may close(fd) and
1132 * free the NETWORK, so nothing below this line may touch `netw`. */
1133 __atomic_store_n(&netw->reactor_close_acked, 1, __ATOMIC_RELEASE);
1134}
1135
1137 if (!netw) return;
1139 if (!r) return;
1140 if (r->wake_efd < 0) return;
1141
1142 /* Dirty-list: add this NETWORK to the reactor's pending list via
1143 * a CAS guard so only the first push between drains takes the
1144 * dirty_lock. Multiple producer threads pushing to the SAME
1145 * NETWORK between drains see CAS fail and skip the push (the first
1146 * one already enqueued it). The reactor clears in_dirty_list BEFORE
1147 * draining so any producer that pushes during the drain succeeds
1148 * the CAS and re-adds for the next pass. */
1149 {
1150 int expected = 0;
1151 if (__atomic_compare_exchange_n(&netw->in_dirty_list, &expected, 1,
1152 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
1153 pthread_mutex_lock(&r->dirty_lock);
1154 list_push(r->dirty_pending, netw, NULL); /* NULL dtor: alias only */
1155 pthread_mutex_unlock(&r->dirty_lock);
1156 }
1157 }
1158
1159 uint64_t one = 1;
1160 ssize_t w = write(r->wake_efd, &one, sizeof(one));
1161 (void)w; /* best-effort; eventfd write of 8 bytes either succeeds
1162 * fully or returns EAGAIN if the counter is at UINT64_MAX
1163 * minus 1, which is impossible in practice */
1164}
1165
1167 if (!netw) return;
1168
1169 /* Step 1: if the connection is still registered, prod the reactor to
1170 * tear it down. We must NOT gate the whole function on reactor_mode:
1171 * the reactor can clear reactor_mode mid-unregister (peer EOF / hard
1172 * error) and then keep touching the NETWORK for a few more lines
1173 * (the EPOLL_CTL_DEL, list removal, the unregistered-socket log read
1174 * of link.sock) before publishing the ack. Returning early on
1175 * reactor_mode == 0 would let the caller free the NETWORK under those
1176 * accesses (use-after-free). Instead we always wait for the ack
1177 * below; this prod is only needed when the reactor has not yet begun
1178 * the teardown. The reactor_mode read is racy but only steers the
1179 * (idempotent) prod, not the free, so the raciness is benign.
1180 *
1181 * netw_set serialises under eventbolt; n_reactor_notify_send is a
1182 * no-op once reactor_handle has been nulled, which is fine because in
1183 * that case the ack is already set or imminently will be. */
1187 }
1188
1189 /* Step 2: wait for the reactor to ack. Busy-poll with a 1 ms sleep,
1190 * same shape as the existing nb_running_threads spin in netw_close.
1191 * The ack is published by n_reactor_unregister as the very last thing
1192 * the reactor ever does with this NETWORK, regardless of which path
1193 * unregistered it (the EXIT_ASKED sweep in the common case, but also
1194 * a concurrent peer EOF / hard error). reactor_mode is always cleared
1195 * before the ack store, so once we observe reactor_mode == 0 the ack
1196 * is guaranteed to land. The acquire-load pairs with unregister's
1197 * release store so all of its side effects (epoll DEL, cleared
1198 * reactor_mode, freed accumulators, list removal, the final log) are
1199 * visible, and ordered before, the caller's subsequent close(fd) /
1200 * free. */
1201 while (!__atomic_load_n(&netw->reactor_close_acked, __ATOMIC_ACQUIRE)) {
1202 struct timespec ts = {0, 1000000L}; /* 1 ms */
1203 nanosleep(&ts, NULL);
1204 }
1205}
1206
1208 size_t send_list_limit,
1209 size_t recv_list_limit,
1210 int blocking,
1211 n_reactor* reactor,
1212 int* retval) {
1213 if (!reactor) {
1214 n_log(LOG_ERR, "netw_accept_into_reactor: NULL reactor");
1215 if (retval) *retval = EINVAL;
1216 return NULL;
1217 }
1218 NETWORK* netw = netw_accept_from_ex(listener, send_list_limit,
1219 recv_list_limit, blocking, retval);
1220 if (!netw) return NULL;
1221 /* Register before returning so the caller can't accidentally
1222 * call netw_start_thr_engine on a reactor-mode connection.
1223 * Failure path closes the freshly-accepted socket. */
1224 if (!n_reactor_register(reactor, netw)) {
1225 n_log(LOG_ERR, "netw_accept_into_reactor: register failed for socket %d",
1226 netw->link.sock);
1227 netw_close(&netw);
1228 if (retval) *retval = EIO;
1229 return NULL;
1230 }
1231 return netw;
1232}
1233
1234#else /* N_REACTOR_AVAILABLE */
1235
1236/* non-Linux stubs */
1237
1238n_reactor* n_reactor_new(int max_fds_hint) {
1239 (void)max_fds_hint;
1241 "n_reactor_new: epoll/eventfd not available on this "
1242 "platform; reactor mode unsupported (use thread mode)");
1243 return NULL;
1244}
1245
1247 if (reactor) *reactor = NULL;
1248}
1249
1250void n_reactor_run(n_reactor* reactor) {
1251 (void)reactor;
1252}
1253
1255 (void)reactor;
1256}
1257
1259 (void)arg;
1260 return NULL;
1261}
1262
1264 (void)reactor;
1265 if (out) memset(out, 0, sizeof(*out));
1266}
1267
1269 (void)reactor;
1270 (void)netw;
1271 return 0;
1272}
1273
1275 (void)reactor;
1276 (void)netw;
1277}
1278
1280 (void)netw;
1281}
1282
1284 (void)netw;
1285}
1286
1288 size_t send_list_limit,
1289 size_t recv_list_limit,
1290 int blocking,
1291 n_reactor* reactor,
1292 int* retval) {
1293 (void)listener;
1294 (void)send_list_limit;
1295 (void)recv_list_limit;
1296 (void)blocking;
1297 (void)reactor;
1298 if (retval) *retval = ENOSYS;
1299 return NULL;
1300}
1301
1302#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
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:113
#define LOG_DEBUG
debug-level messages
Definition n_log.h:108
#define LOG_ERR
error conditions
Definition n_log.h:100
#define LOG_WARNING
warning conditions
Definition n_log.h:102
#define LOG_INFO
informational
Definition n_log.h:106
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
N_SOCKET link
networking socket
Definition n_network.h:454
char * reactor_send_buf
malloc'd framed bytes, NULL when idle
Definition n_network.h:536
SOCKET sock
a normal socket
Definition n_network.h:356
#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:342
#define NETW_COMPRESS_MIN_RATIO
Definition n_network.h:343
#define NETW_MSG_MAX_FRAME_SIZE
Maximum accepted size, in bytes, of a single received message frame payload.
Definition n_network.h:93
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:5008
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:4952
#define netw_atomic_write_reactor_handle(netw, val)
Definition n_network.h:622
#define netw_atomic_read_reactor_mode(netw)
Lock-free atomic read of the reactor_mode flag.
Definition n_network.h:616
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:3992
#define netw_atomic_write_reactor_mode(netw, val)
Definition n_network.h:617
#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:3150
int netw_get_state(NETWORK *netw, uint32_t *state, int *thr_engine_status)
Get the state of a network.
Definition n_network.c:3128
#define netw_atomic_read_reactor_handle(netw)
Same contract for the back-pointer to the reactor.
Definition n_network.h:621
#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:3258
@ NETW_COMPRESS_NONE
no automatic compression on send, still decompresses inbound
Definition n_network.h:328
@ NETW_COMPRESS_LZ4
compress on send with LZ4, decompress either on recv
Definition n_network.h:332
@ NETW_COMPRESSED_LZ4
Definition n_network.h:346
@ NETW_THR_ENGINE_STARTED
Definition n_network.h:346
@ NETW_COMPRESSED_ZLIB
Definition n_network.h:346
@ NETW_RUN
Definition n_network.h:346
@ NETW_ERROR
Definition n_network.h:346
@ NETW_EXIT_ASKED
Definition n_network.h:346
Structure of a NETWORK.
Definition n_network.h:375
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:1258
void n_reactor_run(n_reactor *reactor)
Run the epoll loop on the calling thread.
Definition n_reactor.c:1250
void n_reactor_get_stats(const n_reactor *reactor, n_reactor_stats *out)
Read current stats counters into *out.
Definition n_reactor.c:1263
n_reactor * n_reactor_new(int max_fds_hint)
Create a new reactor.
Definition n_reactor.c:1238
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:1287
void n_reactor_unregister(n_reactor *reactor, NETWORK *netw)
Unregister a NETWORK from the reactor.
Definition n_reactor.c:1274
int n_reactor_register(n_reactor *reactor, NETWORK *netw)
Register a NETWORK with the reactor.
Definition n_reactor.c:1268
void n_reactor_notify_send(NETWORK *netw)
Producer-side wake-up after a netw_add_msg.
Definition n_reactor.c:1279
void n_reactor_close_netw_sync(NETWORK *netw)
Synchronously close a reactor-registered NETWORK from the game thread.
Definition n_reactor.c:1283
void n_reactor_stop(n_reactor *reactor)
Signal the run loop to exit at the next iteration.
Definition n_reactor.c:1254
void n_reactor_destroy(n_reactor **reactor)
Tear down a reactor.
Definition n_reactor.c:1246
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.