Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
reactor_io_tests.h
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <[email protected]>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#pragma once
20
21// The epoll backend is Linux-only, so are these tests. They drive real pollable
22// descriptors (socketpairs, loopback TCP/UDP, eventfd) end to end, submit an
23// op, let the reactor thread arm it and perform the transfer on readiness, then
24// reap the completion. Regular files are deliberately absent -- epoll cannot
25// poll them.
26#ifdef __linux__
27
28# include <arpa/inet.h>
29# include <errno.h>
30# include <fcntl.h>
31# include <netinet/in.h>
32# include <stdint.h>
33# include <stdlib.h>
34# include <string.h>
35# include <sys/epoll.h>
36# include <sys/eventfd.h>
37# include <sys/socket.h>
38# include <sys/stat.h>
39# include <unistd.h>
40
41# include "acutest.h"
42# include "base.h"
43// reactor_tests.h brings in rt/reactor/internal.h and the white-box
44// reactor_test_submit / reactor_test_await helpers; backend.h completes the
45// vtable type so the nudge (`backend->wake`) can be called.
46# include "reactor_tests.h"
47# include "rt/reactor/backend.h"
48# include "serene/rt/fiber/io.h"
49
50# define REACTOR_IO_TESTS(X) \
51 X("io::read", test_io_read), X("io::write", test_io_write), X("io::poll", test_io_poll), \
52 X("io::accept", test_io_accept), X("io::connect_success", test_io_connect), \
53 X("io::connect_refused", test_io_refused), X("io::recvfrom", test_io_recvfrom), \
54 X("io::sendto", test_io_sendto), X("io::recvmsg", test_io_recvmsg), \
55 X("io::sendmsg", test_io_sendmsg), X("io::shared_fd", test_io_shared_fd), \
56 X("io::accept_fanout", test_io_fanout), X("io::tombstone_reuse", test_io_reuse), \
57 X("io::bad_fd", test_io_bad_fd), X("io::peer_close_eof", test_io_eof), \
58 X("io::write_epipe", test_io_epipe), X("io::fiber_read", test_io_fiber_read), \
59 X("io::fiber_write", test_io_fiber_write), X("io::fiber_sleep", test_io_fiber_sleep), \
60 X("io::fiber_pipe", test_io_fiber_pipe), X("io::fiber_drain", test_io_fiber_drain), \
61 X("io::fiber_file", test_io_fiber_file), \
62 X("io::fiber_accept_connect", test_io_fiber_accept_connect), \
63 X("io::fiber_dgram", test_io_fiber_dgram), X("io::inline_helpers", test_io_inline_helpers), \
64 X("io::channel_admission", test_io_channel_admission), \
65 X("io::busy_retry", test_io_busy_retry), X("io::open_nonblock", test_io_open_nonblock), \
66 X("io::fiber_fifo", test_io_fiber_fifo)
67
68// ---------------------------------------------------------------------------
69// helpers
70// ---------------------------------------------------------------------------
71
72static void io_nonblock(int fd) {
73 int fl = fcntl(fd, F_GETFL, 0);
74 TEST_ASSERT(fl >= 0);
75 TEST_ASSERT(fcntl(fd, F_SETFL, fl | O_NONBLOCK) == 0);
76}
77
78// Push a request and nudge the reactor, so it drains the SQ at once instead of
79// waiting out the reactor loop's idle timeout.
80static void io_submit(srn_reactor_t *r, size_t channel, const srn_reactor_io_request_t *req) {
81 reactor_test_submit(r, channel, req);
82 r->backend->wake(r);
83}
84
85static srn_reactor_io_completion_t io_await(srn_reactor_t *r, size_t channel) {
86 return reactor_test_await(r, channel);
87}
88
89// A connected stream pair, both ends non-blocking.
90static void io_socketpair(int sv[2]) {
91 TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0);
92 io_nonblock(sv[0]);
93 io_nonblock(sv[1]);
94}
95
96// A non-blocking loopback TCP listener; its bound port (network order) is
97// returned through `port`.
98static int io_tcp_listener(uint16_t *port) {
99 int s = socket(AF_INET, SOCK_STREAM, 0);
100 TEST_ASSERT(s >= 0);
101 io_nonblock(s);
102 struct sockaddr_in a = {.sin_family = AF_INET};
103 a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
104 TEST_ASSERT(bind(s, (struct sockaddr *)&a, sizeof(a)) == 0);
105 TEST_ASSERT(listen(s, 16) == 0);
106 socklen_t al = sizeof(a);
107 TEST_ASSERT(getsockname(s, (struct sockaddr *)&a, &al) == 0);
108 *port = a.sin_port;
109 return s;
110}
111
112// A blocking connect to a loopback port; returns the client fd. Loopback
113// completes the handshake at once, so this does not stall.
114static int io_tcp_connect(uint16_t port) {
115 int c = socket(AF_INET, SOCK_STREAM, 0);
116 TEST_ASSERT(c >= 0);
117 struct sockaddr_in a = {.sin_family = AF_INET, .sin_port = port};
118 a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
119 TEST_ASSERT(connect(c, (struct sockaddr *)&a, sizeof(a)) == 0);
120 return c;
121}
122
123// A non-blocking UDP socket bound to a loopback ephemeral port (returned via
124// `port`).
125static int io_udp_bound(uint16_t *port) {
126 int s = socket(AF_INET, SOCK_DGRAM, 0);
127 TEST_ASSERT(s >= 0);
128 io_nonblock(s);
129 struct sockaddr_in a = {.sin_family = AF_INET};
130 a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
131 TEST_ASSERT(bind(s, (struct sockaddr *)&a, sizeof(a)) == 0);
132 socklen_t al = sizeof(a);
133 TEST_ASSERT(getsockname(s, (struct sockaddr *)&a, &al) == 0);
134 *port = a.sin_port;
135 return s;
136}
137
138// ---------------------------------------------------------------------------
139// per-op happy paths
140// ---------------------------------------------------------------------------
141
142// READ pulls bytes already waiting on the descriptor.
143static void test_io_read() {
144 MAKE_ENGINE(mm, engine);
145 srn_reactor_t *r = engine->reactor;
146 reactor_test_start(r, 1);
147
148 int sv[2];
149 io_socketpair(sv);
150 TEST_ASSERT(write(sv[1], "hello", 5) == 5);
151
152 char buf[16] = {0};
153 int marker = 0;
156 .fd = sv[0],
157 .buf = buf,
158 .len = sizeof(buf),
159 .offset = -1,
160 .fiber_data = &marker
161 };
162 io_submit(r, 0, &req);
163
164 srn_reactor_io_completion_t c = io_await(r, 0);
165 TEST_CHECK(c.fiber_data == &marker);
166 TEST_CHECK(c.result == 5);
167 TEST_CHECK(memcmp(buf, "hello", 5) == 0);
168
169 close(sv[0]);
170 close(sv[1]);
171 SHUTDOWN_ENGINE(mm, engine);
172}
173
174// WRITE pushes bytes the peer can then read.
175static void test_io_write() {
176 MAKE_ENGINE(mm, engine);
177 srn_reactor_t *r = engine->reactor;
178 reactor_test_start(r, 1);
179
180 int sv[2];
181 io_socketpair(sv);
182
183 char out[] = "world";
184 int marker = 0;
187 .fd = sv[0],
188 .buf = out,
189 .len = 5,
190 .offset = -1,
191 .fiber_data = &marker
192 };
193 io_submit(r, 0, &req);
194
195 srn_reactor_io_completion_t c = io_await(r, 0);
196 TEST_CHECK(c.result == 5);
197
198 char buf[16] = {0};
199 TEST_CHECK(read(sv[1], buf, sizeof(buf)) == 5);
200 TEST_CHECK(memcmp(buf, "world", 5) == 0);
201
202 close(sv[0]);
203 close(sv[1]);
204 SHUTDOWN_ENGINE(mm, engine);
205}
206
207// POLL reports readiness without transferring anything; `result` is the mask.
208static void test_io_poll() {
209 MAKE_ENGINE(mm, engine);
210 srn_reactor_t *r = engine->reactor;
211 reactor_test_start(r, 1);
212
213 int efd = eventfd(0, EFD_NONBLOCK);
214 TEST_ASSERT(efd >= 0);
215 uint64_t one = 1;
216 TEST_ASSERT(write(efd, &one, sizeof(one)) == (ssize_t)sizeof(one));
217
218 int marker = 0;
220 .op = SRN_REACTOR_IO_POLL, .fd = efd, .flags = EPOLLIN, .fiber_data = &marker
221 };
222 io_submit(r, 0, &req);
223
224 srn_reactor_io_completion_t c = io_await(r, 0);
225 TEST_CHECK((c.result & EPOLLIN) != 0);
226
227 close(efd);
228 SHUTDOWN_ENGINE(mm, engine);
229}
230
231// ACCEPT yields a new descriptor and fills the peer address (family-agnostic;
232// here IPv4), reporting its length on the completion.
233static void test_io_accept() {
234 MAKE_ENGINE(mm, engine);
235 srn_reactor_t *r = engine->reactor;
236 reactor_test_start(r, 1);
237
238 uint16_t port;
239 int lst = io_tcp_listener(&port);
240
241 struct sockaddr_storage ss;
242 memset(&ss, 0, sizeof(ss));
243 int marker = 0;
246 .fd = lst,
247 .addr = &ss,
248 .addrlen = sizeof(ss),
249 .fiber_data = &marker
250 };
251 io_submit(r, 0, &req);
252
253 int cli = io_tcp_connect(port);
254
255 srn_reactor_io_completion_t c = io_await(r, 0);
256 TEST_CHECK(!HAS_ERROR(c)); // the accepted fd is in c.result
257 TEST_CHECK(c.addrlen >= sizeof(struct sockaddr_in));
258 TEST_CHECK(((struct sockaddr_in *)&ss)->sin_family == AF_INET);
259
260 if (!HAS_ERROR(c)) {
261 close((int)c.result);
262 }
263 close(cli);
264 close(lst);
265 SHUTDOWN_ENGINE(mm, engine);
266}
267
268// CONNECT to a live listener succeeds with a zero result.
269static void test_io_connect() {
270 MAKE_ENGINE(mm, engine);
271 srn_reactor_t *r = engine->reactor;
272 reactor_test_start(r, 1);
273
274 uint16_t port;
275 int lst = io_tcp_listener(&port);
276
277 int s = socket(AF_INET, SOCK_STREAM, 0);
278 TEST_ASSERT(s >= 0);
279 io_nonblock(s); // non-blocking so connect returns EINPROGRESS, never blocks
280 // the reactor thread
281 struct sockaddr_in a = {.sin_family = AF_INET, .sin_port = port};
282 a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
283
284 int marker = 0;
286 .op = SRN_REACTOR_IO_CONNECT, .fd = s, .addr = &a, .addrlen = sizeof(a), .fiber_data = &marker
287 };
288 io_submit(r, 0, &req);
289
290 srn_reactor_io_completion_t c = io_await(r, 0);
291 TEST_CHECK(c.result == 0);
292
293 close(s);
294 close(lst);
295 SHUTDOWN_ENGINE(mm, engine);
296}
297
298// CONNECT to a dead port fails with -ECONNREFUSED (whether the kernel refuses
299// at submit time or via SO_ERROR on the EPOLLOUT path).
300static void test_io_refused() {
301 MAKE_ENGINE(mm, engine);
302 srn_reactor_t *r = engine->reactor;
303 reactor_test_start(r, 1);
304
305 uint16_t port;
306 int lst = io_tcp_listener(&port);
307 close(lst); // free the port so nothing is listening
308
309 int s = socket(AF_INET, SOCK_STREAM, 0);
310 TEST_ASSERT(s >= 0);
311 io_nonblock(s);
312 struct sockaddr_in a = {.sin_family = AF_INET, .sin_port = port};
313 a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
314
315 int marker = 0;
317 .op = SRN_REACTOR_IO_CONNECT, .fd = s, .addr = &a, .addrlen = sizeof(a), .fiber_data = &marker
318 };
319 io_submit(r, 0, &req);
320
321 srn_reactor_io_completion_t c = io_await(r, 0);
322 TEST_CHECK(HAS_ERROR(c) && c.maybe_error->tag == CONNECTION_REFUSED);
323
324 close(s);
325 SHUTDOWN_ENGINE(mm, engine);
326}
327
328// RECVFROM receives a datagram and records the sender's address and its length.
329static void test_io_recvfrom() {
330 MAKE_ENGINE(mm, engine);
331 srn_reactor_t *r = engine->reactor;
332 reactor_test_start(r, 1);
333
334 uint16_t port;
335 int rcv = io_udp_bound(&port);
336 int snd = socket(AF_INET, SOCK_DGRAM, 0);
337 TEST_ASSERT(snd >= 0);
338
339 struct sockaddr_storage ss;
340 memset(&ss, 0, sizeof(ss));
341 char buf[32] = {0};
342 int marker = 0;
345 .fd = rcv,
346 .buf = buf,
347 .len = sizeof(buf),
348 .addr = &ss,
349 .addrlen = sizeof(ss),
350 .fiber_data = &marker
351 };
352 io_submit(r, 0, &req);
353
354 struct sockaddr_in to = {.sin_family = AF_INET, .sin_port = port};
355 to.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
356 char msg[] = "ping";
357 TEST_ASSERT(sendto(snd, msg, 4, 0, (struct sockaddr *)&to, sizeof(to)) == 4);
358
359 srn_reactor_io_completion_t c = io_await(r, 0);
360 TEST_CHECK(c.result == 4);
361 TEST_CHECK(memcmp(buf, "ping", 4) == 0);
362 TEST_CHECK(c.addrlen >= sizeof(struct sockaddr_in));
363 TEST_CHECK(((struct sockaddr_in *)&ss)->sin_family == AF_INET);
364
365 close(snd);
366 close(rcv);
367 SHUTDOWN_ENGINE(mm, engine);
368}
369
370// SENDTO delivers a datagram to a named peer.
371static void test_io_sendto() {
372 MAKE_ENGINE(mm, engine);
373 srn_reactor_t *r = engine->reactor;
374 reactor_test_start(r, 1);
375
376 uint16_t port;
377 int rcv = io_udp_bound(&port);
378 int snd = socket(AF_INET, SOCK_DGRAM, 0);
379 TEST_ASSERT(snd >= 0);
380 io_nonblock(snd);
381
382 struct sockaddr_in to = {.sin_family = AF_INET, .sin_port = port};
383 to.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
384 char msg[] = "pong";
385 int marker = 0;
386
389 .fd = snd,
390 .buf = msg,
391 .len = 4,
392 .addr = &to,
393 .addrlen = sizeof(to),
394 .fiber_data = &marker
395 };
396 io_submit(r, 0, &req);
397
398 srn_reactor_io_completion_t c = io_await(r, 0);
399 TEST_CHECK(c.result == 4);
400
401 char buf[32] = {0};
402 TEST_CHECK(recvfrom(rcv, buf, sizeof(buf), 0, nullptr, nullptr) == 4);
403 TEST_CHECK(memcmp(buf, "pong", 4) == 0);
404
405 close(snd);
406 close(rcv);
407 SHUTDOWN_ENGINE(mm, engine);
408}
409
410// RECVMSG scatters a single read across multiple iovec segments.
411static void test_io_recvmsg() {
412 MAKE_ENGINE(mm, engine);
413 srn_reactor_t *r = engine->reactor;
414 reactor_test_start(r, 1);
415
416 int sv[2];
417 io_socketpair(sv);
418 TEST_ASSERT(write(sv[1], "foobar", 6) == 6);
419
420 char p1[3] = {0};
421 char p2[3] = {0};
422 struct iovec iov[2] = {{.iov_base = p1, .iov_len = 3}, {.iov_base = p2, .iov_len = 3}};
423 struct msghdr mh;
424 memset(&mh, 0, sizeof(mh));
425 mh.msg_iov = iov;
426 mh.msg_iovlen = 2;
427
428 int marker = 0;
430 .op = SRN_REACTOR_IO_RECVMSG, .fd = sv[0], .buf = &mh, .fiber_data = &marker
431 };
432 io_submit(r, 0, &req);
433
434 srn_reactor_io_completion_t c = io_await(r, 0);
435 TEST_CHECK(c.result == 6);
436 TEST_CHECK(memcmp(p1, "foo", 3) == 0);
437 TEST_CHECK(memcmp(p2, "bar", 3) == 0);
438
439 close(sv[0]);
440 close(sv[1]);
441 SHUTDOWN_ENGINE(mm, engine);
442}
443
444// SENDMSG gathers multiple iovec segments into a single write.
445static void test_io_sendmsg() {
446 MAKE_ENGINE(mm, engine);
447 srn_reactor_t *r = engine->reactor;
448 reactor_test_start(r, 1);
449
450 int sv[2];
451 io_socketpair(sv);
452
453 char a[] = "foo";
454 char b[] = "bar";
455 struct iovec iov[2] = {{.iov_base = a, .iov_len = 3}, {.iov_base = b, .iov_len = 3}};
456 struct msghdr mh;
457 memset(&mh, 0, sizeof(mh));
458 mh.msg_iov = iov;
459 mh.msg_iovlen = 2;
460
461 int marker = 0;
463 .op = SRN_REACTOR_IO_SENDMSG, .fd = sv[0], .buf = &mh, .fiber_data = &marker
464 };
465 io_submit(r, 0, &req);
466
467 srn_reactor_io_completion_t c = io_await(r, 0);
468 TEST_CHECK(c.result == 6);
469
470 char buf[16] = {0};
471 TEST_CHECK(read(sv[1], buf, sizeof(buf)) == 6);
472 TEST_CHECK(memcmp(buf, "foobar", 6) == 0);
473
474 close(sv[0]);
475 close(sv[1]);
476 SHUTDOWN_ENGINE(mm, engine);
477}
478
479// ---------------------------------------------------------------------------
480// shared descriptors -- the reason the backend groups ops per fd
481// ---------------------------------------------------------------------------
482
483// A READ and a WRITE on one descriptor at once, the fd is armed for the union
484// EPOLLIN|EPOLLOUT and both complete. Both requests are queued before the nudge
485// so the reactor arms them together.
486static void test_io_shared_fd() {
487 MAKE_ENGINE(mm, engine);
488 srn_reactor_t *r = engine->reactor;
489 reactor_test_start(r, 1);
490
491 int sv[2];
492 io_socketpair(sv);
493 TEST_ASSERT(write(sv[1], "in", 2) == 2); // make the read side ready
494
495 char rbuf[8] = {0};
496 char wbuf[] = "out";
497 int m_read = 0;
498 int m_write = 0;
501 .fd = sv[0],
502 .buf = rbuf,
503 .len = sizeof(rbuf),
504 .offset = -1,
505 .fiber_data = &m_read
506 };
509 .fd = sv[0],
510 .buf = wbuf,
511 .len = 3,
512 .offset = -1,
513 .fiber_data = &m_write
514 };
515 reactor_test_submit(r, 0, &rd);
516 reactor_test_submit(r, 0, &wr);
517 r->backend->wake(r);
518
519 bool got_read = false;
520 bool got_write = false;
521 for (int i = 0; i < 2; i++) {
522 srn_reactor_io_completion_t c = io_await(r, 0);
523 if (c.fiber_data == &m_read) {
524 got_read = true;
525 TEST_CHECK(c.result == 2);
526 TEST_CHECK(memcmp(rbuf, "in", 2) == 0);
527 } else if (c.fiber_data == &m_write) {
528 got_write = true;
529 TEST_CHECK(c.result == 3);
530 } else {
531 TEST_CHECK(false); // unexpected completion
532 }
533 }
534 TEST_CHECK(got_read && got_write);
535
536 char buf[8] = {0};
537 TEST_CHECK(read(sv[1], buf, sizeof(buf)) == 3);
538 TEST_CHECK(memcmp(buf, "out", 3) == 0);
539
540 close(sv[0]);
541 close(sv[1]);
542 SHUTDOWN_ENGINE(mm, engine);
543}
544
545// Many ACCEPTs queued on one listener all complete -- fan-out on a single fd.
546static void test_io_fanout() {
547 MAKE_ENGINE(mm, engine);
548 srn_reactor_t *r = engine->reactor;
549 reactor_test_start(r, 1);
550
551 uint16_t port;
552 int lst = io_tcp_listener(&port);
553
554 enum { K = 3 };
555 int markers[K];
556 struct sockaddr_storage ss[K];
557 for (int i = 0; i < K; i++) {
558 memset(&ss[i], 0, sizeof(ss[i]));
561 .fd = lst,
562 .addr = &ss[i],
563 .addrlen = sizeof(ss[i]),
564 .fiber_data = &markers[i]
565 };
566 reactor_test_submit(r, 0, &req);
567 }
568 r->backend->wake(r);
569
570 int clients[K];
571 for (int i = 0; i < K; i++) {
572 clients[i] = io_tcp_connect(port);
573 }
574
575 int accepted = 0;
576 for (int i = 0; i < K; i++) {
577 srn_reactor_io_completion_t c = io_await(r, 0);
578 if (!HAS_ERROR(c)) {
579 accepted++;
580 close((int)c.result);
581 }
582 }
583 TEST_CHECK(accepted == K);
584
585 for (int i = 0; i < K; i++) {
586 close(clients[i]);
587 }
588 close(lst);
589 SHUTDOWN_ENGINE(mm, engine);
590}
591
592// ---------------------------------------------------------------------------
593// lifecycle and errors
594// ---------------------------------------------------------------------------
595
596// After an fd's op completes its list goes dormant; a later op on the same fd
597// integer revives it (the tombstone reuse path).
598static void test_io_reuse() {
599 MAKE_ENGINE(mm, engine);
600 srn_reactor_t *r = engine->reactor;
601 reactor_test_start(r, 1);
602
603 int sv[2];
604 io_socketpair(sv);
605
606 TEST_ASSERT(write(sv[1], "a", 1) == 1);
607 char b1[4] = {0};
608 int m1 = 0;
611 .fd = sv[0],
612 .buf = b1,
613 .len = sizeof(b1),
614 .offset = -1,
615 .fiber_data = &m1
616 };
617 io_submit(r, 0, &rq1);
618 srn_reactor_io_completion_t c1 = io_await(r, 0);
619 TEST_CHECK(c1.result == 1 && b1[0] == 'a');
620
621 // Same fd again, its list was tombstoned, so this must re-arm it.
622 TEST_ASSERT(write(sv[1], "b", 1) == 1);
623 char b2[4] = {0};
624 int m2 = 0;
627 .fd = sv[0],
628 .buf = b2,
629 .len = sizeof(b2),
630 .offset = -1,
631 .fiber_data = &m2
632 };
633 io_submit(r, 0, &rq2);
634 srn_reactor_io_completion_t c2 = io_await(r, 0);
635 TEST_CHECK(c2.result == 1 && b2[0] == 'b');
636
637 close(sv[0]);
638 close(sv[1]);
639 SHUTDOWN_ENGINE(mm, engine);
640}
641
642// Arming a closed descriptor fails the op back with -EBADF rather than hanging.
643static void test_io_bad_fd() {
644 MAKE_ENGINE(mm, engine);
645 srn_reactor_t *r = engine->reactor;
646 reactor_test_start(r, 1);
647
648 int fd = dup(0);
649 TEST_ASSERT(fd >= 0);
650 close(fd); // fd is now an invalid descriptor number
651
652 char buf[4] = {0};
653 int marker = 0;
656 .fd = fd,
657 .buf = buf,
658 .len = sizeof(buf),
659 .offset = -1,
660 .fiber_data = &marker
661 };
662 io_submit(r, 0, &req);
663
664 srn_reactor_io_completion_t c = io_await(r, 0);
665 TEST_CHECK(HAS_ERROR(c) && c.maybe_error->tag == BAD_DESCRIPTOR);
666
667 SHUTDOWN_ENGINE(mm, engine);
668}
669
670// A closed peer makes the descriptor readable at EOF: READ completes with 0.
671static void test_io_eof() {
672 MAKE_ENGINE(mm, engine);
673 srn_reactor_t *r = engine->reactor;
674 reactor_test_start(r, 1);
675
676 int sv[2];
677 io_socketpair(sv);
678 close(sv[1]); // peer gone
679
680 char buf[8] = {0};
681 int marker = 0;
684 .fd = sv[0],
685 .buf = buf,
686 .len = sizeof(buf),
687 .offset = -1,
688 .fiber_data = &marker
689 };
690 io_submit(r, 0, &req);
691
692 srn_reactor_io_completion_t c = io_await(r, 0);
693 TEST_CHECK(c.result == 0);
694
695 close(sv[0]);
696 SHUTDOWN_ENGINE(mm, engine);
697}
698
699// Writing to a closed peer completes with -EPIPE. SIGPIPE is suppressed by the
700// runtime (srn_engine_make), so the write fails rather than killing the
701// process.
702static void test_io_epipe() {
703 MAKE_ENGINE(mm, engine);
704 srn_reactor_t *r = engine->reactor;
705 reactor_test_start(r, 1);
706
707 int sv[2];
708 io_socketpair(sv);
709 close(sv[1]);
710
711 char out[] = "x";
712 int marker = 0;
715 .fd = sv[0],
716 .buf = out,
717 .len = 1,
718 .offset = -1,
719 .fiber_data = &marker
720 };
721 io_submit(r, 0, &req);
722
723 srn_reactor_io_completion_t c = io_await(r, 0);
724 TEST_CHECK(HAS_ERROR(c) && c.maybe_error->tag == BROKEN_PIPE);
725
726 close(sv[0]);
727 SHUTDOWN_ENGINE(mm, engine);
728}
729
730// ---------------------------------------------------------------------------
731// through a real fiber, the full path, srn_fiber_read/write suspends the
732// fiber, the reactor performs the transfer, the worker resumes the fiber with
733// the result. Driven by srn_sched_run (which activates the reactor itself), not
734// the white-box helpers above.
735// ---------------------------------------------------------------------------
736
737typedef struct fiber_io_args_t {
738 int fd;
739 void *buf;
740 size_t len;
742} fiber_io_args_t;
743
744// A fiber entry must return a non-null result (the launcher panics on null), so
745// the IO fibers return this once they have stored their result in the args.
746static int fiber_io_done;
747
748static srn_fiber_result_t fiber_read_entry(srn_context_t *ctx, void *arg) {
749 UNUSED(ctx);
750 fiber_io_args_t *a = (fiber_io_args_t *)arg;
751 SRN_WITH_RETRY(a->res.maybe_error, { a->res = srn_fiber_read(a->fd, a->buf, a->len, -1); });
752 return &fiber_io_done;
753}
754
755static srn_fiber_result_t fiber_write_entry(srn_context_t *ctx, void *arg) {
756 UNUSED(ctx);
757 fiber_io_args_t *a = (fiber_io_args_t *)arg;
758 a->res = srn_fiber_write(a->fd, a->buf, a->len, -1);
759 return &fiber_io_done;
760}
761
762// A fiber reads a socketpair via srn_fiber_read, it suspends, the reactor
763// reads, the worker resumes it with the byte count. The run only returns once
764// the reactor is idle too, so reaching here means the whole loop closed.
765static void test_io_fiber_read() {
766 MAKE_ENGINE(mm, engine);
767 MAKE_CONTEXT(engine, ctx);
768 srn_scheduler_t *sched = engine->scheduler;
769 ASSERT_NOT_NULL(sched);
770
771 int sv[2];
772 io_socketpair(sv);
773 TEST_ASSERT(write(sv[1], "hello", 5) == 5);
774
775 char buf[16] = {0};
776 fiber_io_args_t a = {.fd = sv[0], .buf = buf, .len = sizeof(buf)};
777 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_read_entry, &a));
778
779 srn_sched_run(sched, 1);
780
781 TEST_CHECK(!HAS_ERROR(a.res) && a.res.count == 5);
782 TEST_CHECK(memcmp(buf, "hello", 5) == 0);
783 TEST_CHECK(srn_reactor_idle(engine->reactor));
784
785 close(sv[0]);
786 close(sv[1]);
787 RELEASE_CONTEXT(ctx);
788 SHUTDOWN_ENGINE(mm, engine);
789}
790
791// A fiber writes a socketpair via srn_fiber_write; the peer end then has the
792// bytes.
793static void test_io_fiber_write() {
794 MAKE_ENGINE(mm, engine);
795 MAKE_CONTEXT(engine, ctx);
796 srn_scheduler_t *sched = engine->scheduler;
797 ASSERT_NOT_NULL(sched);
798
799 int sv[2];
800 io_socketpair(sv);
801
802 char out[] = "world";
803 fiber_io_args_t a = {.fd = sv[0], .buf = out, .len = 5};
804 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_write_entry, &a));
805
806 srn_sched_run(sched, 1);
807
808 TEST_CHECK(!HAS_ERROR(a.res) && a.res.count == 5);
809 char buf[16] = {0};
810 TEST_CHECK(read(sv[1], buf, sizeof(buf)) == 5);
811 TEST_CHECK(memcmp(buf, "world", 5) == 0);
812
813 close(sv[0]);
814 close(sv[1]);
815 RELEASE_CONTEXT(ctx);
816 SHUTDOWN_ENGINE(mm, engine);
817}
818
819typedef struct fiber_sleep_args_t {
820 uint64_t duration_ns;
821 srn_error_t *err;
822} fiber_sleep_args_t;
823
824static srn_fiber_result_t fiber_sleep_entry(srn_context_t *ctx, void *arg) {
825 UNUSED(ctx);
826 fiber_sleep_args_t *a = (fiber_sleep_args_t *)arg;
827 a->err = srn_fiber_sleep_ns(a->duration_ns);
828 return &fiber_io_done;
829}
830
831// A fiber sleeps via srn_fiber_sleep_ns, it suspends, the reactor's timer keeps
832// the worker alive past the point where no fibers are runnable, and the worker
833// resumes the fiber once the deadline passes. The run only returns after the
834// reactor goes idle, so the wall clock has advanced at least the requested
835// span.
836static void test_io_fiber_sleep() {
837 MAKE_ENGINE(mm, engine);
838 MAKE_CONTEXT(engine, ctx);
839 srn_scheduler_t *sched = engine->scheduler;
840 ASSERT_NOT_NULL(sched);
841
842 uint64_t delay = 50ULL * SRN_NS_PER_MS; // 50ms
843 fiber_sleep_args_t a = {.duration_ns = delay, .err = nullptr};
844 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_sleep_entry, &a));
845
846 uint64_t before = srn_now_ns();
847 srn_sched_run(sched, 1);
848 uint64_t elapsed = srn_now_ns() - before;
849
850 TEST_CHECK(a.err == nullptr);
851 TEST_CHECK(elapsed >= delay); // resumed no earlier than the deadline
852 TEST_CHECK(srn_reactor_idle(engine->reactor));
853
854 RELEASE_CONTEXT(ctx);
855 SHUTDOWN_ENGINE(mm, engine);
856}
857
858// A channel admits at most one ring's worth of in-flight operations. With
859// more sleeping fibers than the ring capacity on a single worker, the first
860// capacity submissions are admitted and every one beyond that fails with
861// CHANNEL_BUSY, instead of overflowing the rings and stalling the reactor.
862static atomic_int io_adm_ok;
863static atomic_int io_adm_busy;
864static atomic_int io_adm_other;
865
866static srn_fiber_result_t io_adm_entry(srn_context_t *ctx, void *arg) {
867 UNUSED(ctx);
868 UNUSED(arg);
869 srn_error_t *e = srn_fiber_sleep_ms(200ULL);
870 if (e == nullptr) {
871 atomic_fetch_add(&io_adm_ok, 1);
872 } else if (e->tag == CHANNEL_BUSY) {
873 atomic_fetch_add(&io_adm_busy, 1);
874 } else {
875 atomic_fetch_add(&io_adm_other, 1);
876 }
877 return &fiber_io_done;
878}
879
880static void test_io_channel_admission() {
881 MAKE_ENGINE(mm, engine);
882 MAKE_CONTEXT(engine, ctx);
883 srn_scheduler_t *sched = engine->scheduler;
884 ASSERT_NOT_NULL(sched);
885
886 size_t cap = (size_t)1 << engine->config.reactor.ring_magnitude;
887 size_t extra = 32;
888
889 atomic_store(&io_adm_ok, 0);
890 atomic_store(&io_adm_busy, 0);
891 atomic_store(&io_adm_other, 0);
892
893 // All fibers submit their sleeps well within the 200ms window, so the
894 // channel really holds cap in-flight timers when the extra ones arrive.
895 for (size_t i = 0; i < cap + extra; i++) {
896 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, io_adm_entry, nullptr));
897 }
898
899 srn_sched_run(sched, 1);
900
901 TEST_MSG(
902 "ok=%d busy=%d other=%d cap=%zu", atomic_load(&io_adm_ok), atomic_load(&io_adm_busy),
903 atomic_load(&io_adm_other), cap
904 );
905 TEST_CHECK(atomic_load(&io_adm_ok) == (int)cap);
906 TEST_CHECK(atomic_load(&io_adm_busy) == (int)extra);
907 TEST_CHECK(atomic_load(&io_adm_other) == 0);
908 TEST_CHECK(srn_reactor_idle(engine->reactor));
909
910 RELEASE_CONTEXT(ctx);
911 SHUTDOWN_ENGINE(mm, engine);
912}
913
914// The retry idiom over srn_fiber_io_retry, with more sleeping fibers than the
915// channel admits, the overflow fibers retry (yielding between attempts) until
916// completions reopen the admission window, and every fiber eventually
917// succeeds. No CHANNEL_BUSY leaks through the loop.
918static atomic_int io_retry_ok;
919static atomic_int io_retry_failed;
920
921static srn_fiber_result_t io_retry_entry(srn_context_t *ctx, void *arg) {
922 UNUSED(ctx);
923 UNUSED(arg);
924
925 srn_error_t *e;
926 SRN_WITH_RETRY(e, { e = srn_fiber_sleep_ms(20ULL); });
927
928 if (e == nullptr) {
929 atomic_fetch_add(&io_retry_ok, 1);
930 } else {
931 atomic_fetch_add(&io_retry_failed, 1);
932 }
933 return &fiber_io_done;
934}
935
936static void test_io_busy_retry() {
937 MAKE_ENGINE(mm, engine);
938 MAKE_CONTEXT(engine, ctx);
939 srn_scheduler_t *sched = engine->scheduler;
940 ASSERT_NOT_NULL(sched);
941
942 size_t cap = (size_t)1 << engine->config.reactor.ring_magnitude;
943 size_t n = cap + 64;
944
945 atomic_store(&io_retry_ok, 0);
946 atomic_store(&io_retry_failed, 0);
947
948 for (size_t i = 0; i < n; i++) {
949 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, io_retry_entry, nullptr));
950 }
951
952 srn_sched_run(sched, 1);
953
954 TEST_MSG("ok=%d failed=%d n=%zu", atomic_load(&io_retry_ok), atomic_load(&io_retry_failed), n);
955 TEST_CHECK(atomic_load(&io_retry_ok) == (int)n);
956 TEST_CHECK(atomic_load(&io_retry_failed) == 0);
957 TEST_CHECK(srn_reactor_idle(engine->reactor));
958
959 RELEASE_CONTEXT(ctx);
960 SHUTDOWN_ENGINE(mm, engine);
961}
962
963// Two fibers over one pipe, a writer pushes bytes with srn_fiber_write and a
964// reader pulls them with srn_fiber_read, both suspending through the reactor on
965// a single worker. Whichever runs first suspends and yields the worker to the
966// other, so the transfer closes the loop -- the reader sees exactly what the
967// writer sent.
968static void test_io_fiber_pipe() {
969 MAKE_ENGINE(mm, engine);
970 MAKE_CONTEXT(engine, ctx);
971 srn_scheduler_t *sched = engine->scheduler;
972 ASSERT_NOT_NULL(sched);
973
974 int fds[2];
975 TEST_ASSERT(pipe(fds) == 0);
976 io_nonblock(fds[0]);
977 io_nonblock(fds[1]);
978
979 char out[] = "ping-pong";
980 size_t n = sizeof(out) - 1;
981 char in[16] = {0};
982 fiber_io_args_t w = {.fd = fds[1], .buf = out, .len = n};
983 fiber_io_args_t rd = {.fd = fds[0], .buf = in, .len = sizeof(in)};
984
985 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_write_entry, &w));
986 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_read_entry, &rd));
987
988 srn_sched_run(sched, 1);
989
990 TEST_CHECK(!HAS_ERROR(w.res) && w.res.count == n);
991 TEST_CHECK(!HAS_ERROR(rd.res) && rd.res.count == n);
992 TEST_CHECK(memcmp(in, out, n) == 0);
993 TEST_CHECK(srn_reactor_idle(engine->reactor));
994
995 close(fds[0]);
996 close(fds[1]);
997 RELEASE_CONTEXT(ctx);
998 SHUTDOWN_ENGINE(mm, engine);
999}
1000
1001typedef struct fiber_drain_args_t {
1002 srn_scheduler_t *sched;
1003 uint64_t duration_ns;
1004 srn_error_t *err;
1005} fiber_drain_args_t;
1006
1007// Submits a real sleep and parks on it. The op is in flight when the drain
1008// begins, so it must finish normally (no error), not be abandoned.
1009static srn_fiber_result_t fiber_drain_sleeper(srn_context_t *ctx, void *arg) {
1010 UNUSED(ctx);
1011 fiber_drain_args_t *a = (fiber_drain_args_t *)arg;
1012 a->err = srn_fiber_sleep_ns(a->duration_ns);
1013 return &fiber_io_done;
1014}
1015
1016// Begins the drain, then attempts another sleep. That submission is fenced, so
1017// it comes back as the CANCELED error without ever parking and the fiber
1018// unwinds.
1019static srn_fiber_result_t fiber_drain_starter(srn_context_t *ctx, void *arg) {
1020 UNUSED(ctx);
1021 fiber_drain_args_t *a = (fiber_drain_args_t *)arg;
1022 srn_sched_drain(a->sched);
1023 a->err = srn_fiber_sleep_ns(a->duration_ns);
1024 return &fiber_io_done;
1025}
1026
1027// srn_sched_drain winds the pool down gracefully, an op already in flight when
1028// the drain starts still completes, while a submission attempted after the
1029// drain is fenced into -ECANCELED. On one worker the sleeper runs first and
1030// parks, then the starter drains while that op is in flight, so the order is
1031// deterministic. The run returns once both fibers have unwound and the reactor
1032// is idle -- the drain converges to the same quiescence as a natural finish.
1033static void test_io_fiber_drain() {
1034 MAKE_ENGINE(mm, engine);
1035 MAKE_CONTEXT(engine, ctx);
1036 srn_scheduler_t *sched = engine->scheduler;
1037 ASSERT_NOT_NULL(sched);
1038
1039 fiber_drain_args_t sleeper = {
1040 .sched = sched, .duration_ns = 30ULL * SRN_NS_PER_MS, .err = nullptr
1041 };
1042 fiber_drain_args_t starter = {
1043 .sched = sched, .duration_ns = 30ULL * SRN_NS_PER_MS, .err = nullptr
1044 };
1045 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_drain_sleeper, &sleeper));
1046 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_drain_starter, &starter));
1047
1048 srn_sched_run(sched, 1);
1049
1050 TEST_CHECK(sleeper.err == nullptr); // in-flight op finished
1051 TEST_CHECK(starter.err != nullptr && starter.err->tag == CANCELED); // post-drain submit fenced
1052 TEST_CHECK(srn_reactor_idle(engine->reactor)); // converged to quiescence
1053
1054 RELEASE_CONTEXT(ctx);
1055 SHUTDOWN_ENGINE(mm, engine);
1056}
1057
1058// A regular file is not pollable, so srn_fiber_read on it cannot go through the
1059// epoll reactor; it is served by a synchronous read on the worker instead. The
1060// fiber still gets its bytes, and the reactor is never touched (stays idle).
1061static void test_io_fiber_file() {
1062 MAKE_ENGINE(mm, engine);
1063 MAKE_CONTEXT(engine, ctx);
1064 srn_scheduler_t *sched = engine->scheduler;
1065 ASSERT_NOT_NULL(sched);
1066
1067 char path[] = "/tmp/serene_fiber_file_XXXXXX";
1068 int fd = mkstemp(path);
1069 TEST_ASSERT(fd >= 0);
1070 unlink(path); // anonymous; the open fd keeps it alive
1071 TEST_ASSERT(write(fd, "diskdata", 8) == 8);
1072 TEST_ASSERT(lseek(fd, 0, SEEK_SET) == 0);
1073
1074 char buf[16] = {0};
1075 fiber_io_args_t a = {.fd = fd, .buf = buf, .len = sizeof(buf)};
1076 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_read_entry, &a));
1077
1078 srn_sched_run(sched, 1);
1079
1080 TEST_CHECK(!HAS_ERROR(a.res) && a.res.count == 8);
1081 TEST_CHECK(memcmp(buf, "diskdata", 8) == 0);
1082 TEST_CHECK(srn_reactor_idle(engine->reactor)); // never reached the reactor
1083
1084 close(fd);
1085 RELEASE_CONTEXT(ctx);
1086 SHUTDOWN_ENGINE(mm, engine);
1087}
1088
1089// ---------------------------------------------------------------------------
1090// through a real fiber -- the C10 socket surface (srn_fiber_accept / connect /
1091// recvfrom / sendto / poll). Each builds a request and parks like read/write;
1092// accept and recvfrom additionally carry the peer address length back.
1093// ---------------------------------------------------------------------------
1094
1095typedef struct fiber_accept_args_t {
1096 int listener;
1097 struct sockaddr_storage peer;
1099} fiber_accept_args_t;
1100
1101static srn_fiber_result_t fiber_accept_entry(srn_context_t *ctx, void *arg) {
1102 UNUSED(ctx);
1103 fiber_accept_args_t *a = (fiber_accept_args_t *)arg;
1104 a->res = srn_fiber_accept(a->listener, (struct sockaddr *)&a->peer, sizeof(a->peer));
1105 return &fiber_io_done;
1106}
1107
1108typedef struct fiber_connect_args_t {
1109 int fd;
1110 struct sockaddr_in to;
1111 srn_error_t *err;
1112} fiber_connect_args_t;
1113
1114static srn_fiber_result_t fiber_connect_entry(srn_context_t *ctx, void *arg) {
1115 UNUSED(ctx);
1116 fiber_connect_args_t *a = (fiber_connect_args_t *)arg;
1117 a->err = srn_fiber_connect(a->fd, (struct sockaddr *)&a->to, sizeof(a->to));
1118 return &fiber_io_done;
1119}
1120
1121// One fiber accepts on a loopback listener while another connects to it, both
1122// through the suspending wrappers on a single worker. The acceptor gets the
1123// connected fd with the peer address (and its length) filled in; the connector
1124// gets a zero result.
1125static void test_io_fiber_accept_connect() {
1126 MAKE_ENGINE(mm, engine);
1127 MAKE_CONTEXT(engine, ctx);
1128 srn_scheduler_t *sched = engine->scheduler;
1129 ASSERT_NOT_NULL(sched);
1130
1131 uint16_t port;
1132 int lst = io_tcp_listener(&port);
1133
1134 int cli = socket(AF_INET, SOCK_STREAM, 0);
1135 TEST_ASSERT(cli >= 0);
1136 io_nonblock(cli); // non-blocking so connect parks instead of blocking
1137
1138 fiber_accept_args_t acc = {.listener = lst};
1139 fiber_connect_args_t con = {.fd = cli, .to = {.sin_family = AF_INET, .sin_port = port}};
1140 con.to.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1141
1142 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_accept_entry, &acc));
1143 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_connect_entry, &con));
1144
1145 srn_sched_run(sched, 1);
1146
1147 TEST_CHECK(!HAS_ERROR(acc.res)); // accepted fd is in acc.res.fd
1148 TEST_CHECK(con.err == nullptr); // connected
1149 TEST_CHECK(acc.res.addrlen >= sizeof(struct sockaddr_in));
1150 TEST_CHECK(((struct sockaddr_in *)&acc.peer)->sin_family == AF_INET);
1151
1152 if (!HAS_ERROR(acc.res)) {
1153 close(acc.res.fd);
1154 }
1155 close(cli);
1156 close(lst);
1157 RELEASE_CONTEXT(ctx);
1158 SHUTDOWN_ENGINE(mm, engine);
1159}
1160
1161typedef struct fiber_recvfrom_args_t {
1162 int fd;
1163 char buf[32];
1164 struct sockaddr_storage from;
1166} fiber_recvfrom_args_t;
1167
1168static srn_fiber_result_t fiber_recvfrom_entry(srn_context_t *ctx, void *arg) {
1169 UNUSED(ctx);
1170 fiber_recvfrom_args_t *a = (fiber_recvfrom_args_t *)arg;
1171 a->res = srn_fiber_recvfrom(
1172 a->fd, a->buf, sizeof(a->buf), 0, (struct sockaddr *)&a->from, sizeof(a->from)
1173 );
1174 return &fiber_io_done;
1175}
1176
1177typedef struct fiber_sendto_args_t {
1178 int fd;
1179 struct sockaddr_in to;
1181} fiber_sendto_args_t;
1182
1183static srn_fiber_result_t fiber_sendto_entry(srn_context_t *ctx, void *arg) {
1184 UNUSED(ctx);
1185 fiber_sendto_args_t *a = (fiber_sendto_args_t *)arg;
1186 a->res = srn_fiber_sendto(a->fd, "ping", 4, 0, (struct sockaddr *)&a->to, sizeof(a->to));
1187 return &fiber_io_done;
1188}
1189
1190// One fiber receives a datagram while another sends it, over loopback UDP. The
1191// receiver gets the bytes and the sender's address length; the sender gets the
1192// byte count.
1193static void test_io_fiber_dgram() {
1194 MAKE_ENGINE(mm, engine);
1195 MAKE_CONTEXT(engine, ctx);
1196 srn_scheduler_t *sched = engine->scheduler;
1197 ASSERT_NOT_NULL(sched);
1198
1199 uint16_t port;
1200 int rcv = io_udp_bound(&port);
1201 int snd = socket(AF_INET, SOCK_DGRAM, 0);
1202 TEST_ASSERT(snd >= 0);
1203 io_nonblock(snd);
1204
1205 fiber_recvfrom_args_t r = {.fd = rcv};
1206 fiber_sendto_args_t s = {.fd = snd, .to = {.sin_family = AF_INET, .sin_port = port}};
1207 s.to.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1208
1209 // Receiver first so it is armed before the datagram is sent.
1210 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_recvfrom_entry, &r));
1211 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_sendto_entry, &s));
1212
1213 srn_sched_run(sched, 1);
1214
1215 TEST_CHECK(!HAS_ERROR(r.res) && r.res.count == 4);
1216 TEST_CHECK(memcmp(r.buf, "ping", 4) == 0);
1217 TEST_CHECK(!HAS_ERROR(s.res) && s.res.count == 4);
1218 TEST_CHECK(r.res.addrlen >= sizeof(struct sockaddr_in));
1219
1220 close(rcv);
1221 close(snd);
1222 RELEASE_CONTEXT(ctx);
1223 SHUTDOWN_ENGINE(mm, engine);
1224}
1225
1226// The non-suspending helpers are plain syscall wrappers -- no reactor, no
1227// scheduler. Build a loopback listener with them, exercise an error path, and
1228// open a temp file, checking the null-or-error convention and the tag mapping.
1229static void test_io_inline_helpers() {
1230 srn_io_fd_result_t s = srn_fiber_socket(AF_INET, SOCK_STREAM, 0);
1231 TEST_CHECK(!HAS_ERROR(s));
1232 int lst = s.fd;
1233
1234 struct sockaddr_in a = {.sin_family = AF_INET};
1235 a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1236 TEST_CHECK(srn_fiber_bind(lst, (struct sockaddr *)&a, sizeof(a)) == nullptr);
1237 TEST_CHECK(srn_fiber_listen(lst, 4) == nullptr);
1238
1239 int one = 1;
1240 TEST_CHECK(srn_fiber_setsockopt(lst, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) == nullptr);
1241 int got = 0;
1242 socklen_t gl = sizeof(got);
1243 TEST_CHECK(srn_fiber_getsockopt(lst, SOL_SOCKET, SO_REUSEADDR, &got, &gl) == nullptr);
1244
1245 TEST_CHECK(srn_fiber_close(lst) == nullptr);
1246 // Closing the now-stale descriptor fails with a bad-descriptor error.
1247 srn_error_t *e = srn_fiber_close(lst);
1248 TEST_CHECK(e != nullptr && e->tag == BAD_DESCRIPTOR);
1249
1250 // open() a real temp file through the helper, then a missing path.
1251 char path[] = "/tmp/serene_io_helper_XXXXXX";
1252 int tmp = mkstemp(path);
1253 TEST_ASSERT(tmp >= 0);
1254 close(tmp);
1255
1256 srn_io_fd_result_t o = srn_fiber_open(path, O_RDONLY, 0);
1257 TEST_CHECK(!HAS_ERROR(o));
1258 if (!HAS_ERROR(o)) {
1259 TEST_CHECK(srn_fiber_close(o.fd) == nullptr);
1260 }
1261 unlink(path);
1262
1263 srn_io_fd_result_t miss = srn_fiber_open("/tmp/serene_no_such_file_zzq", O_RDONLY, 0);
1264 TEST_CHECK(HAS_ERROR(miss) && miss.maybe_error->tag == NOT_FOUND);
1265}
1266
1267// srn_fiber_open forces O_NONBLOCK and O_CLOEXEC, so a descriptor it returns
1268// is always safe to hand to the suspending calls, the reactor thread must
1269// never end up waiting on a blocking descriptor.
1270static void test_io_open_nonblock() {
1271 srn_io_fd_result_t r = srn_fiber_open("/dev/null", O_RDONLY, 0);
1273
1274 int fl = fcntl(r.fd, F_GETFL, 0);
1275 TEST_CHECK(fl >= 0);
1276 TEST_CHECK((fl & O_NONBLOCK) != 0);
1277
1278 int fdfl = fcntl(r.fd, F_GETFD, 0);
1279 TEST_CHECK(fdfl >= 0);
1280 TEST_CHECK((fdfl & FD_CLOEXEC) != 0);
1281
1282 TEST_CHECK(srn_fiber_close(r.fd) == nullptr);
1283}
1284
1285// A FIFO opened through srn_fiber_open is non-blocking, so both ends are
1286// pollable and a write/read pair crosses it through the reactor like any
1287// socket or pipe. The read end opens first, with O_NONBLOCK an O_RDONLY open
1288// returns at once instead of blocking the opener until a writer appears.
1289static void test_io_fiber_fifo() {
1290 MAKE_ENGINE(mm, engine);
1291 MAKE_CONTEXT(engine, ctx);
1292 srn_scheduler_t *sched = engine->scheduler;
1293 ASSERT_NOT_NULL(sched);
1294
1295 char dir[] = "/tmp/serene_fifo_XXXXXX";
1296 TEST_ASSERT(mkdtemp(dir) != nullptr);
1297 char path[128];
1298 TEST_ASSERT(snprintf(path, sizeof(path), "%s/fifo", dir) > 0);
1299 TEST_ASSERT(mkfifo(path, 0600) == 0);
1300
1301 srn_io_fd_result_t rend = srn_fiber_open(path, O_RDONLY, 0);
1302 TEST_ASSERT(!HAS_ERROR(rend));
1303 srn_io_fd_result_t wend = srn_fiber_open(path, O_WRONLY, 0);
1304 TEST_ASSERT(!HAS_ERROR(wend));
1305
1306 char out[] = "fifo-ping";
1307 size_t n = sizeof(out) - 1;
1308 char in[16] = {0};
1309 fiber_io_args_t w = {.fd = wend.fd, .buf = out, .len = n};
1310 fiber_io_args_t rd = {.fd = rend.fd, .buf = in, .len = sizeof(in)};
1311
1312 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_write_entry, &w));
1313 ASSERT_NOT_NULL(srn_fiber_spawn(ctx, fiber_read_entry, &rd));
1314
1315 srn_sched_run(sched, 1);
1316
1317 TEST_CHECK(!HAS_ERROR(w.res) && w.res.count == n);
1318 TEST_CHECK(!HAS_ERROR(rd.res) && rd.res.count == n);
1319 TEST_CHECK(memcmp(in, out, n) == 0);
1320 TEST_CHECK(srn_reactor_idle(engine->reactor));
1321
1322 TEST_CHECK(srn_fiber_close(rend.fd) == nullptr);
1323 TEST_CHECK(srn_fiber_close(wend.fd) == nullptr);
1324 TEST_ASSERT(unlink(path) == 0);
1325 TEST_ASSERT(rmdir(dir) == 0);
1326 RELEASE_CONTEXT(ctx);
1327 SHUTDOWN_ENGINE(mm, engine);
1328}
1329
1330#endif // __linux__
static srn_fiber_result_t sleeper(srn_context_t *ctx, void *arg)
Definition 07_sleep.c:43
#define TEST_CHECK(cond)
Definition acutest.h:95
int n
Definition acutest.h:525
#define TEST_ASSERT(cond)
Definition acutest.h:117
#define TEST_MSG(...)
Definition acutest.h:223
Internal reactor backend interface.
#define RELEASE_CONTEXT(x)
Definition base.h:46
#define ASSERT_NOT_NULL(x)
Definition base.h:30
#define SHUTDOWN_ENGINE(mm, engine)
Definition base.h:38
#define MAKE_ENGINE(mm, engine)
Definition base.h:32
#define MAKE_CONTEXT(engine, x)
Definition base.h:42
struct srn_error_t srn_error_t
Definition core.h:32
struct srn_scheduler_t srn_scheduler_t
Definition engine.h:39
#define HAS_ERROR(x)
True when x – a value carrying an ERROR_HEADER – has an error attached.
Definition errors.h:63
@ BROKEN_PIPE
Definition errors.h:94
@ CHANNEL_BUSY
The channel already has a full ring's worth of operations in flight.
Definition errors.h:117
@ CONNECTION_REFUSED
Definition errors.h:91
@ BAD_DESCRIPTOR
Definition errors.h:95
@ NOT_FOUND
Definition errors.h:106
@ CANCELED
Definition errors.h:97
srn_fiber_t * srn_fiber_spawn(srn_context_t *ctx, srn_fiber_entry_t entry, void *arg)
Make and schedule a fiber with every default, the engine's scheduler, the configured stack size,...
Definition fiber.c:247
void * srn_fiber_result_t
What a fiber's entry produces, type-erased.
Definition fiber.h:157
srn_error_t * srn_fiber_listen(int fd, int backlog)
Mark fd as a listening socket with the given backlog, like listen(2).
Definition io.c:443
srn_error_t * srn_fiber_getsockopt(int fd, int level, int optname, void *optval, socklen_t *optlen)
Read a socket option into optval/optlen, like getsockopt(2).
Definition io.c:467
srn_error_t * srn_fiber_close(int fd)
Close fd, like close(2).
Definition io.c:451
srn_error_t * srn_fiber_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen)
Set a socket option from optval/optlen, like setsockopt(2).
Definition io.c:476
srn_io_size_result_t srn_fiber_write(int fd, const void *buf, size_t len, int64_t offset)
Write len bytes from buf to fd, suspending the calling fiber until the operation completes rather tha...
Definition io.c:296
srn_error_t * srn_fiber_sleep_ms(uint64_t duration_ms)
Suspend the calling fiber for at least duration_ms miliseconds, letting its worker run other fibers m...
Definition io.c:278
srn_io_recvfrom_result_t srn_fiber_recvfrom(int fd, void *buf, size_t len, int flags, struct sockaddr *addr, socklen_t addr_cap)
Receive a datagram from fd into buf/len with flags (the MSG_* flags), suspending the calling fiber un...
Definition io.c:340
srn_error_t * srn_fiber_connect(int fd, const struct sockaddr *addr, socklen_t addrlen)
Connect the socket fd to addr/addrlen, suspending the calling fiber until the connection completes ra...
Definition io.c:330
srn_io_fd_result_t srn_fiber_socket(int domain, int type, int protocol)
Create a socket, like socket(2), additionally forcing SOCK_NONBLOCK and SOCK_CLOEXEC so the descripto...
Definition io.c:427
srn_io_size_result_t srn_fiber_sendto(int fd, const void *buf, size_t len, int flags, const struct sockaddr *addr, socklen_t addrlen)
Send buf/len on fd to addr/addrlen with flags (the MSG_* flags), suspending the calling fiber until t...
Definition io.c:360
srn_error_t * srn_fiber_bind(int fd, const struct sockaddr *addr, socklen_t addrlen)
Bind fd to addr/addrlen, like bind(2).
Definition io.c:435
srn_io_accept_result_t srn_fiber_accept(int fd, struct sockaddr *addr, socklen_t addr_cap)
Accept a pending connection on the listening socket fd, suspending the calling fiber until one arrive...
Definition io.c:308
srn_error_t * srn_fiber_sleep_ns(uint64_t duration_ns)
Suspend the calling fiber for at least duration_ns nanoseconds, letting its worker run other fibers m...
Definition io.c:251
srn_io_fd_result_t srn_fiber_open(const char *path, int flags, mode_t mode)
Open path, like open(2), additionally forcing O_NONBLOCK and O_CLOEXEC so the descriptor is always sa...
Definition io.c:484
The fiber facing IO API: blocking looking calls a fiber uses to do IO.
struct srn_io_recvfrom_result_t srn_io_recvfrom_result_t
The result of srn_fiber_recvfrom.
#define SRN_WITH_RETRY(err,...)
Run block and retry it (yielding between attempts through srn_fiber_io_retry) while err holds the tra...
Definition io.h:132
struct srn_io_accept_result_t srn_io_accept_result_t
The result of srn_fiber_accept.
struct srn_io_size_result_t srn_io_size_result_t
The result of a byte transfer op (read/write/sendto/recvmsg/sendmsg).
bool srn_reactor_idle(srn_reactor_t *reactor)
Whether the reactor has no operations in flight.
Definition reactor.c:169
@ SRN_REACTOR_IO_ACCEPT
Accept a pending connection on the listening socket fd, with flags (the accept4 flags).
Definition reactor.h:79
@ SRN_REACTOR_IO_READ
Read from a descriptor into buf. Uses fd, buf, len, offset.
Definition reactor.h:68
@ SRN_REACTOR_IO_SENDTO
Send buf/len on fd to the address in addr/addrlen with flags.
Definition reactor.h:92
@ SRN_REACTOR_IO_CONNECT
Connect fd to the address in addr/addrlen.
Definition reactor.h:83
@ SRN_REACTOR_IO_RECVFROM
Receive a datagram from fd into buf/len with flags.
Definition reactor.h:88
@ SRN_REACTOR_IO_RECVMSG
Receive into the struct msghdr that buf points at, on fd, with flags (scatter/gather and ancillary da...
Definition reactor.h:96
@ SRN_REACTOR_IO_WRITE
Write buf to a descriptor. Uses fd, buf, len, offset.
Definition reactor.h:70
@ SRN_REACTOR_IO_POLL
Wait until fd is ready for the events requested in flags (EPOLLIN/EPOLLOUT) without performing any tr...
Definition reactor.h:105
@ SRN_REACTOR_IO_SENDMSG
Send the struct msghdr that buf points at, on fd, with flags.
Definition reactor.h:100
static srn_reactor_io_completion_t reactor_test_await(srn_reactor_t *r, size_t channel)
static void reactor_test_start(srn_reactor_t *r, size_t nworkers)
static void reactor_test_submit(srn_reactor_t *r, size_t channel, const srn_reactor_io_request_t *req)
void srn_sched_drain(srn_scheduler_t *sched)
Ask a running scheduler to wind down gracefully.
Definition scheduler.c:963
void srn_sched_run(srn_scheduler_t *sched, size_t nworkers)
Run the scheduler with nworkers os threads draining it, returning once the pool goes quiescent (every...
Definition scheduler.c:843
A runtime error, a tag classifying the failure and a human-readable message.
Definition errors.h:125
srn_error_tag_t tag
Definition errors.h:126
The result of an op that yields a descriptor (srn_fiber_socket, srn_fiber_open).
Definition io.h:66
void(* wake)(srn_reactor_t *reactor)
Definition backend.h:77
A completion, the result of one submission, echoed back on the channel.
Definition reactor.h:156
void * fiber_data
The fiber_data of the submission this completes.
Definition reactor.h:159
uint32_t addrlen
For SRN_REACTOR_IO_ACCEPT and SRN_REACTOR_IO_RECVFROM, the actual length of the address written into ...
Definition reactor.h:169
size_t result
The result of the operation on success, bytes transferred for the transfers, the accepted descriptor ...
Definition reactor.h:163
A submission, one request a fiber places on its channel.
Definition reactor.h:113
const srn_reactor_backend_t * backend
The chosen kernel mechanism and its private state, set at activation.
Definition internal.h:47
#define SRN_NS_PER_MS
nanoseconds in one millisecond
Definition utils.h:308
uint64_t srn_now_ns(void)
The current time on the monotonic clock, in nanoseconds.
Definition utils.c:46
#define UNUSED(x)
Definition utils.h:45