#include "../config-host.h" /* SPDX-License-Identifier: MIT */ /* * Description: stress test multishot accept under rapid connection churn. * Submits a multishot accept and has client threads rapidly * connecting and disconnecting, verifying all connections are * properly accepted. */ #include #include #include #include #include #include #include #include #include #include #include #include "liburing.h" #include "helpers.h" #define NR_CONNS 200 #define NR_BURST 32 #define NR_BURST_ROUNDS 4 #define NR_RECONNECTS 1000 #define MSHOT_UDATA 1000 #define SEND_BYTE 0xa5 static int no_mshot_accept; struct stress_ctx { pthread_barrier_t barrier; struct sockaddr_in addr; int nr_conns; }; struct burst_ctx { pthread_barrier_t barrier; pthread_barrier_t round_barrier; struct sockaddr_in addr; int nr_burst; int nr_rounds; }; struct reconnect_ctx { pthread_barrier_t barrier; struct sockaddr_in addr; int nr_reconnects; }; static int create_listen_sock(struct sockaddr_in *addr) { int fd, ret, val = 1; fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP); if (fd < 0) { perror("socket"); return -1; } setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val)); setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); addr->sin_family = AF_INET; addr->sin_addr.s_addr = inet_addr("127.0.0.1"); ret = t_bind_ephemeral_port(fd, addr); if (ret) { close(fd); return -1; } ret = listen(fd, 256); if (ret < 0) { perror("listen"); close(fd); return -1; } return fd; } static int arm_mshot_accept(struct io_uring *ring, int listen_fd) { struct io_uring_sqe *sqe; sqe = io_uring_get_sqe(ring); if (!sqe) return -1; io_uring_prep_multishot_accept(sqe, listen_fd, NULL, NULL, 0); io_uring_sqe_set_data64(sqe, MSHOT_UDATA); return io_uring_submit(ring); } /* * Client thread for stress test: rapid connect + close */ static void *stress_client_fn(void *data) { struct stress_ctx *ctx = data; int i; pthread_barrier_wait(&ctx->barrier); for (i = 0; i < ctx->nr_conns; i++) { int fd; fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fd < 0) break; if (connect(fd, (struct sockaddr *)&ctx->addr, sizeof(ctx->addr)) < 0) { close(fd); break; } close(fd); } return NULL; } /* * Stress test: multishot accept with NR_CONNS rapid connections */ static int test_accept_mshot_stress(void) { struct io_uring ring; struct stress_ctx ctx; pthread_t thread; int listen_fd, ret, accepted = 0; struct __kernel_timespec ts = { .tv_sec = 5 }; ret = io_uring_queue_init(512, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); return T_EXIT_FAIL; } listen_fd = create_listen_sock(&ctx.addr); if (listen_fd < 0) { io_uring_queue_exit(&ring); return T_EXIT_FAIL; } ctx.nr_conns = NR_CONNS; pthread_barrier_init(&ctx.barrier, NULL, 2); ret = arm_mshot_accept(&ring, listen_fd); if (ret < 0) { fprintf(stderr, "arm accept: %d\n", ret); goto err; } pthread_create(&thread, NULL, stress_client_fn, &ctx); pthread_barrier_wait(&ctx.barrier); while (accepted < NR_CONNS) { struct io_uring_cqe *cqe; ret = io_uring_wait_cqe_timeout(&ring, &cqe, &ts); if (ret == -ETIME) break; if (ret) { fprintf(stderr, "wait: %d\n", ret); goto err_thread; } if (cqe->res == -EINVAL) { no_mshot_accept = 1; io_uring_cqe_seen(&ring, cqe); pthread_join(thread, NULL); close(listen_fd); io_uring_queue_exit(&ring); return T_EXIT_SKIP; } if (cqe->res < 0) { fprintf(stderr, "accept res: %d\n", cqe->res); io_uring_cqe_seen(&ring, cqe); goto err_thread; } close(cqe->res); accepted++; if (!(cqe->flags & IORING_CQE_F_MORE)) { io_uring_cqe_seen(&ring, cqe); arm_mshot_accept(&ring, listen_fd); continue; } io_uring_cqe_seen(&ring, cqe); } pthread_join(thread, NULL); if (accepted < NR_CONNS) { fprintf(stderr, "stress: accepted %d, expected %d\n", accepted, NR_CONNS); goto err; } close(listen_fd); io_uring_queue_exit(&ring); return T_EXIT_PASS; err_thread: pthread_join(thread, NULL); err: close(listen_fd); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } /* * Client thread for burst test: open NR_BURST connections, wait for * server to drain, close all, repeat. */ static void *burst_client_fn(void *data) { struct burst_ctx *ctx = data; int round; pthread_barrier_wait(&ctx->barrier); for (round = 0; round < ctx->nr_rounds; round++) { int fds[NR_BURST]; int i; for (i = 0; i < ctx->nr_burst; i++) { fds[i] = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fds[i] < 0) break; if (connect(fds[i], (struct sockaddr *)&ctx->addr, sizeof(ctx->addr)) < 0) { close(fds[i]); fds[i] = -1; break; } } /* Signal server: all connections are open */ pthread_barrier_wait(&ctx->round_barrier); /* Wait for server to drain */ pthread_barrier_wait(&ctx->round_barrier); for (i = 0; i < ctx->nr_burst; i++) { if (fds[i] >= 0) close(fds[i]); } } return NULL; } /* * Burst test: client opens NR_BURST connections at once, server drains, * client closes, repeat for NR_BURST_ROUNDS. */ static int test_accept_mshot_burst(void) { struct io_uring ring; struct burst_ctx ctx; pthread_t thread; int listen_fd, ret, total = 0; struct __kernel_timespec ts = { .tv_sec = 5 }; if (no_mshot_accept) return T_EXIT_SKIP; ret = io_uring_queue_init(256, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); return T_EXIT_FAIL; } listen_fd = create_listen_sock(&ctx.addr); if (listen_fd < 0) { io_uring_queue_exit(&ring); return T_EXIT_FAIL; } ctx.nr_burst = NR_BURST; ctx.nr_rounds = NR_BURST_ROUNDS; pthread_barrier_init(&ctx.barrier, NULL, 2); pthread_barrier_init(&ctx.round_barrier, NULL, 2); ret = arm_mshot_accept(&ring, listen_fd); if (ret < 0) goto err; pthread_create(&thread, NULL, burst_client_fn, &ctx); pthread_barrier_wait(&ctx.barrier); for (int round = 0; round < NR_BURST_ROUNDS; round++) { int round_accepted = 0; /* Wait for client to open all connections */ pthread_barrier_wait(&ctx.round_barrier); while (round_accepted < NR_BURST) { struct io_uring_cqe *cqe; ret = io_uring_wait_cqe_timeout(&ring, &cqe, &ts); if (ret == -ETIME) { fprintf(stderr, "burst round %d timeout at %d\n", round, round_accepted); goto err_thread; } if (ret) { fprintf(stderr, "wait: %d\n", ret); goto err_thread; } if (cqe->res < 0) { fprintf(stderr, "accept res: %d\n", cqe->res); io_uring_cqe_seen(&ring, cqe); goto err_thread; } close(cqe->res); round_accepted++; total++; if (!(cqe->flags & IORING_CQE_F_MORE)) { io_uring_cqe_seen(&ring, cqe); arm_mshot_accept(&ring, listen_fd); continue; } io_uring_cqe_seen(&ring, cqe); } /* Signal client: done draining */ pthread_barrier_wait(&ctx.round_barrier); } pthread_join(thread, NULL); if (total != NR_BURST * NR_BURST_ROUNDS) { fprintf(stderr, "burst: accepted %d, expected %d\n", total, NR_BURST * NR_BURST_ROUNDS); goto err; } close(listen_fd); io_uring_queue_exit(&ring); return T_EXIT_PASS; err_thread: pthread_join(thread, NULL); err: close(listen_fd); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } /* * Client thread for reconnect test: connect, send 1 byte, close, repeat. */ static void *reconnect_client_fn(void *data) { struct reconnect_ctx *ctx = data; unsigned char byte = SEND_BYTE; int i; pthread_barrier_wait(&ctx->barrier); for (i = 0; i < ctx->nr_reconnects; i++) { int fd; fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fd < 0) break; if (connect(fd, (struct sockaddr *)&ctx->addr, sizeof(ctx->addr)) < 0) { close(fd); break; } send(fd, &byte, 1, 0); close(fd); } return NULL; } /* * Reconnect test: client connects, sends 1 byte, closes, reconnects. * Server accepts each and reads the byte. */ static int test_accept_mshot_reconnect(void) { struct io_uring ring; struct reconnect_ctx ctx; pthread_t thread; int listen_fd, ret, accepted = 0; struct __kernel_timespec ts = { .tv_sec = 5 }; if (no_mshot_accept) return T_EXIT_SKIP; ret = io_uring_queue_init(512, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); return T_EXIT_FAIL; } listen_fd = create_listen_sock(&ctx.addr); if (listen_fd < 0) { io_uring_queue_exit(&ring); return T_EXIT_FAIL; } ctx.nr_reconnects = NR_RECONNECTS; pthread_barrier_init(&ctx.barrier, NULL, 2); ret = arm_mshot_accept(&ring, listen_fd); if (ret < 0) goto err; pthread_create(&thread, NULL, reconnect_client_fn, &ctx); pthread_barrier_wait(&ctx.barrier); while (accepted < NR_RECONNECTS) { struct io_uring_cqe *cqe; unsigned char buf; int conn_fd; ret = io_uring_wait_cqe_timeout(&ring, &cqe, &ts); if (ret == -ETIME) break; if (ret) { fprintf(stderr, "wait: %d\n", ret); goto err_thread; } if (cqe->res < 0) { fprintf(stderr, "accept res: %d\n", cqe->res); io_uring_cqe_seen(&ring, cqe); goto err_thread; } conn_fd = cqe->res; if (!(cqe->flags & IORING_CQE_F_MORE)) { io_uring_cqe_seen(&ring, cqe); arm_mshot_accept(&ring, listen_fd); } else { io_uring_cqe_seen(&ring, cqe); } /* Read the byte the client sent */ ret = read(conn_fd, &buf, 1); if (ret == 1 && buf != SEND_BYTE) { fprintf(stderr, "bad byte: 0x%x\n", buf); close(conn_fd); goto err_thread; } close(conn_fd); accepted++; } pthread_join(thread, NULL); if (accepted < NR_RECONNECTS) { fprintf(stderr, "reconnect: accepted %d, expected %d\n", accepted, NR_RECONNECTS); goto err; } close(listen_fd); io_uring_queue_exit(&ring); return T_EXIT_PASS; err_thread: pthread_join(thread, NULL); err: close(listen_fd); io_uring_queue_exit(&ring); return T_EXIT_FAIL; } int main(int argc, char *argv[]) { int ret; if (argc > 1) return T_EXIT_SKIP; ret = test_accept_mshot_stress(); if (ret == T_EXIT_SKIP) { printf("Multishot accept not supported, skipping\n"); return T_EXIT_SKIP; } if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_accept_mshot_stress failed\n"); return T_EXIT_FAIL; } ret = test_accept_mshot_burst(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_accept_mshot_burst failed\n"); return T_EXIT_FAIL; } ret = test_accept_mshot_reconnect(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_accept_mshot_reconnect failed\n"); return T_EXIT_FAIL; } return T_EXIT_PASS; }