From ae85e4e80c21eface17fc32964386f1c2660683a Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Wed, 4 Feb 2026 17:12:45 +0100 Subject: [PATCH 01/11] Annotate slow paths --- .../scala-native/snunit/nxt_auto_config.h | 11 ++ .../scala-native/snunit/nxt_unit_embed.c | 170 +++++++++--------- 2 files changed, 96 insertions(+), 85 deletions(-) diff --git a/snunit/resources/scala-native/snunit/nxt_auto_config.h b/snunit/resources/scala-native/snunit/nxt_auto_config.h index 0639ef5..82b99a8 100644 --- a/snunit/resources/scala-native/snunit/nxt_auto_config.h +++ b/snunit/resources/scala-native/snunit/nxt_auto_config.h @@ -21,4 +21,15 @@ #define NXT_DEBUG 0 +/* Branch prediction hints (from unit src/nxt_clang.h). */ +#if defined(__GNUC__) || defined(__clang__) +#define nxt_expect(c, x) __builtin_expect((long) (x), (c)) +#define nxt_fast_path(x) nxt_expect(1, x) +#define nxt_slow_path(x) nxt_expect(0, x) +#else +#define nxt_expect(c, x) (x) +#define nxt_fast_path(x) (x) +#define nxt_slow_path(x) (x) +#endif + #endif diff --git a/snunit/resources/scala-native/snunit/nxt_unit_embed.c b/snunit/resources/scala-native/snunit/nxt_unit_embed.c index 86e1f29..ed940d5 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_embed.c +++ b/snunit/resources/scala-native/snunit/nxt_unit_embed.c @@ -308,19 +308,19 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) int fd, opt = 1; struct sockaddr_in sa; - if (init == NULL) { + if (nxt_slow_path(init == NULL)) { fprintf(stderr, "nxt_unit_embed: init is NULL\n"); fflush(stderr); return NULL; } - if (host == NULL || host[0] == '\0') { + if (nxt_slow_path(host == NULL || host[0] == '\0')) { fprintf(stderr, "nxt_unit_embed: host is NULL or empty\n"); fflush(stderr); return NULL; } emb = (embed_ctx_t *) calloc(1, sizeof(embed_ctx_t)); - if (emb == NULL) { + if (nxt_slow_path(emb == NULL)) { fprintf(stderr, "nxt_unit_embed: calloc failed\n"); fflush(stderr); return NULL; @@ -338,7 +338,7 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) emb->port = (port > 0) ? port : 8080; fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) { + if (nxt_slow_path(fd < 0)) { fprintf(stderr, "nxt_unit_embed: socket() failed: %s\n", strerror(errno)); fflush(stderr); free(emb); @@ -352,7 +352,7 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) sa.sin_addr.s_addr = INADDR_ANY; else inet_pton(AF_INET, emb->listen_addr, &sa.sin_addr); - if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) { + if (nxt_slow_path(bind(fd, (struct sockaddr *) &sa, sizeof(sa)) != 0)) { fprintf(stderr, "nxt_unit_embed: bind(%s:%d) failed: %s\n", emb->listen_addr, emb->port, strerror(errno)); fflush(stderr); @@ -360,14 +360,14 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) free(emb); return NULL; } - if (listen(fd, 128) != 0) { + if (nxt_slow_path(listen(fd, 128) != 0)) { fprintf(stderr, "nxt_unit_embed: listen() failed: %s\n", strerror(errno)); fflush(stderr); close(fd); free(emb); return NULL; } - if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) { + if (nxt_slow_path(fcntl(fd, F_SETFL, O_NONBLOCK) < 0)) { fprintf(stderr, "nxt_unit_embed: fcntl(O_NONBLOCK) failed: %s\n", strerror(errno)); fflush(stderr); close(fd); @@ -384,7 +384,7 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) static int run_loop_process_conn(embed_ctx_t *emb, conn_t **p, conn_t *c, int can_read, int can_write, int err_or_hup) { int n; (void) emb; - if (err_or_hup) { + if (nxt_slow_path(err_or_hup)) { *p = c->next; #if EMB_USE_EPOLL || EMB_USE_KQUEUE embed_ev_remove_conn(emb, c); @@ -561,7 +561,7 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { struct sockaddr_in peer; socklen_t peer_len; - if (ctx == NULL) return NXT_UNIT_ERROR; + if (nxt_slow_path(ctx == NULL)) return NXT_UNIT_ERROR; emb = (embed_ctx_t *) ((char *) ctx - offsetof(embed_ctx_t, ctx)); emb->quit = 0; @@ -572,8 +572,8 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { int nevents, i; emb->ev_fd = epoll_create(1); - if (emb->ev_fd < 0) return NXT_UNIT_ERROR; - if (embed_ev_add_listen(emb) != 0) { + if (nxt_slow_path(emb->ev_fd < 0)) return NXT_UNIT_ERROR; + if (nxt_slow_path(embed_ev_add_listen(emb) != 0)) { close(emb->ev_fd); emb->ev_fd = -1; return NXT_UNIT_ERROR; @@ -581,23 +581,23 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { while (!emb->quit) { /* -1: block until an event. No spinning (0) or long wait (1000ms). */ nevents = epoll_wait(emb->ev_fd, events, EMB_EPOLL_MAXEV, -1); - if (nevents < 0) { if (errno == EINTR) continue; break; } - if (nevents == 0) continue; + if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } + if (nxt_slow_path(nevents == 0)) continue; for (i = 0; i < nevents; i++) { int rev = events[i].events; - if (events[i].data.ptr == NULL) { + if (nxt_slow_path(events[i].data.ptr == NULL)) { /* listen fd */ peer_len = sizeof(peer); new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); - if (new_fd >= 0) { + if (nxt_fast_path(new_fd >= 0)) { fcntl(new_fd, F_SETFL, O_NONBLOCK); setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); c = conn_new(new_fd, emb); - if (c != NULL) { + if (nxt_fast_path(c != NULL)) { c->next = conns; conns = c; - if (embed_ev_add_conn(emb, c) != 0) { + if (nxt_slow_path(embed_ev_add_conn(emb, c) != 0)) { conns = c->next; conn_free(c); } @@ -609,11 +609,11 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { c = (conn_t *) events[i].data.ptr; p = &conns; while (*p != NULL && *p != c) p = &(*p)->next; - if (*p == NULL) continue; - if (!run_loop_process_conn(emb, p, c, + if (nxt_slow_path(*p == NULL)) continue; + if (nxt_slow_path(!run_loop_process_conn(emb, p, c, (rev & EPOLLIN) != 0, (rev & EPOLLOUT) != 0, - (rev & (EPOLLERR | EPOLLHUP)) != 0)) + (rev & (EPOLLERR | EPOLLHUP)) != 0)))) embed_ev_rearm_conn(emb, c); } } @@ -628,16 +628,16 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { int nevents, i; emb->ev_fd = kqueue(); - if (emb->ev_fd < 0) return NXT_UNIT_ERROR; + if (nxt_slow_path(emb->ev_fd < 0)) return NXT_UNIT_ERROR; emb->kq_mchanges = EMB_KQUEUE_MAXCHANGES; emb->kq_changes = (struct kevent *) malloc((size_t) emb->kq_mchanges * sizeof(struct kevent)); - if (emb->kq_changes == NULL) { + if (nxt_slow_path(emb->kq_changes == NULL)) { close(emb->ev_fd); emb->ev_fd = -1; return NXT_UNIT_ERROR; } emb->kq_nchanges = 0; - if (embed_ev_add_listen(emb) != 0) { + if (nxt_slow_path(embed_ev_add_listen(emb) != 0)) { free(emb->kq_changes); close(emb->ev_fd); emb->ev_fd = -1; @@ -647,26 +647,26 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { while (!emb->quit) { nevents = kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, events, EMB_KQUEUE_MAXEV, NULL); emb->kq_nchanges = 0; - if (nevents < 0) { if (errno == EINTR) continue; break; } - if (nevents == 0) continue; + if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } + if (nxt_slow_path(nevents == 0)) continue; for (i = 0; i < nevents; i++) { int filter = events[i].filter; int flags = events[i].flags; int err_or_hup = (flags & EV_ERROR) != 0 || (flags & EV_EOF) != 0; - if (events[i].udata == NULL) { + if (nxt_slow_path(events[i].udata == NULL)) { /* listen fd */ - if (filter == EVFILT_READ && !err_or_hup) { + if (nxt_fast_path(filter == EVFILT_READ && !err_or_hup)) { peer_len = sizeof(peer); new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); - if (new_fd >= 0) { + if (nxt_fast_path(new_fd >= 0)) { fcntl(new_fd, F_SETFL, O_NONBLOCK); setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); c = conn_new(new_fd, emb); - if (c != NULL) { + if (nxt_fast_path(c != NULL)) { c->next = conns; conns = c; - if (embed_ev_add_conn(emb, c) != 0) { + if (nxt_slow_path(embed_ev_add_conn(emb, c) != 0)) { conns = c->next; conn_free(c); } @@ -679,8 +679,8 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { c = (conn_t *) events[i].udata; p = &conns; while (*p != NULL && *p != c) p = &(*p)->next; - if (*p == NULL) continue; - if (filter == EVFILT_READ) + if (nxt_slow_path(*p == NULL)) continue; + if (nxt_fast_path(filter == EVFILT_READ)) run_loop_process_conn(emb, p, c, 1, 0, err_or_hup); else if (filter == EVFILT_WRITE) run_loop_process_conn(emb, p, c, 0, 1, err_or_hup); @@ -698,7 +698,7 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { cap = 64; pfds = (struct pollfd *) malloc((size_t) cap * sizeof(struct pollfd)); - if (pfds == NULL) return NXT_UNIT_ERROR; + if (nxt_slow_path(pfds == NULL)) return NXT_UNIT_ERROR; while (!emb->quit) { nfds = 0; @@ -706,10 +706,10 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { pfds[nfds].events = POLLIN; nfds++; for (c = conns; c != NULL; c = c->next) { - if (nfds >= cap) { + if (nxt_slow_path(nfds >= cap)) { cap *= 2; struct pollfd *np = (struct pollfd *) realloc(pfds, (size_t) cap * sizeof(struct pollfd)); - if (np == NULL) { free(pfds); return NXT_UNIT_ERROR; } + if (nxt_slow_path(np == NULL)) { free(pfds); return NXT_UNIT_ERROR; } pfds = np; } pfds[nfds].fd = c->fd; @@ -720,17 +720,17 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { /* -1: block until an event. No spinning (0) or long wait (1000ms). */ n = poll(pfds, (nfds_t) nfds, -1); - if (n < 0) { if (errno == EINTR) continue; break; } - if (n == 0) continue; + if (nxt_slow_path(n < 0)) { if (errno == EINTR) continue; break; } + if (nxt_slow_path(n == 0)) continue; - if (pfds[0].revents & POLLIN) { + if (nxt_fast_path(pfds[0].revents & POLLIN)) { peer_len = sizeof(peer); new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); - if (new_fd >= 0) { + if (nxt_fast_path(new_fd >= 0)) { fcntl(new_fd, F_SETFL, O_NONBLOCK); setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); c = conn_new(new_fd, emb); - if (c != NULL) { + if (nxt_fast_path(c != NULL)) { c->next = conns; conns = c; } else @@ -742,11 +742,11 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { while (*p != NULL) { c = *p; for (i = 1; i < nfds && pfds[i].fd != c->fd; i++) ; - if (i >= nfds) { p = &c->next; continue; } - if (!run_loop_process_conn(emb, p, c, + if (nxt_slow_path(i >= nfds)) { p = &c->next; continue; } + if (nxt_slow_path(!run_loop_process_conn(emb, p, c, (pfds[i].revents & POLLIN) != 0, (pfds[i].revents & POLLOUT) != 0, - (pfds[i].revents & (POLLERR | POLLHUP)) != 0)) + (pfds[i].revents & (POLLERR | POLLHUP)) != 0)))) p = &(*p)->next; } } @@ -787,9 +787,9 @@ int nxt_unit_process_port_msg(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port) { void nxt_unit_done(nxt_unit_ctx_t *ctx) { embed_ctx_t *emb; - if (ctx == NULL) return; + if (nxt_slow_path(ctx == NULL)) return; emb = (embed_ctx_t *) ((char *) ctx - offsetof(embed_ctx_t, ctx)); - if (emb->listen_fd >= 0) { + if (nxt_fast_path(emb->listen_fd >= 0)) { close(emb->listen_fd); emb->listen_fd = -1; } @@ -831,9 +831,9 @@ static conn_t *current_dispatch_conn; static conn_t *req_to_conn(nxt_unit_request_info_t *req) { conn_t *c; - if (req == NULL) return NULL; + if (nxt_slow_path(req == NULL)) return NULL; c = *(conn_t **)((char *) req - sizeof(conn_t *)); - if (c != NULL && c->req_info == req) return c; + if (nxt_fast_path(c != NULL && c->req_info == req)) return c; return current_dispatch_conn; } @@ -849,12 +849,12 @@ int nxt_unit_response_init(nxt_unit_request_info_t *req, uint16_t status, uint32_t max_fields_count, uint32_t max_fields_size) { conn_t *c = req_to_conn(req); uint32_t alloc_count; - if (c == NULL || c->response != NULL) return NXT_UNIT_ERROR; + if (nxt_slow_path(c == NULL || c->response != NULL)) return NXT_UNIT_ERROR; alloc_count = max_fields_count < RESPONSE_MIN_FIELDS ? RESPONSE_MIN_FIELDS : max_fields_count; pool_align_8(c); c->response = (nxt_unit_response_t *) (c->req_pool + c->req_pool_used); c->req_pool_used += sizeof(nxt_unit_response_t) + (size_t) alloc_count * sizeof(nxt_unit_field_t); - if (c->req_pool_used > REQ_POOL_SIZE) return NXT_UNIT_ERROR; + if (nxt_slow_path(c->req_pool_used > REQ_POOL_SIZE)) return NXT_UNIT_ERROR; c->response->content_length = 0; c->response->fields_count = 0; c->response->piggyback_content_length = 0; @@ -883,9 +883,9 @@ int nxt_unit_response_add_field(nxt_unit_request_info_t *req, conn_t *c = req_to_conn(req); nxt_unit_field_t *f; char *dst; - if (c == NULL || c->response == NULL) return NXT_UNIT_ERROR; - if (c->response->fields_count >= req->response_max_fields) return NXT_UNIT_ERROR; - if (c->req_pool_used + (size_t) name_length + (size_t) value_length + 32 > REQ_POOL_SIZE) return NXT_UNIT_ERROR; + if (nxt_slow_path(c == NULL || c->response == NULL)) return NXT_UNIT_ERROR; + if (nxt_slow_path(c->response->fields_count >= req->response_max_fields)) return NXT_UNIT_ERROR; + if (nxt_slow_path(c->req_pool_used + (size_t) name_length + (size_t) value_length + 32 > REQ_POOL_SIZE)) return NXT_UNIT_ERROR; f = &c->response->fields[c->response->fields_count]; dst = c->req_pool + c->req_pool_used; f->hash = (uint16_t) field_hash(name, (size_t) name_length); @@ -906,7 +906,7 @@ int nxt_unit_response_add_field(nxt_unit_request_info_t *req, } int nxt_unit_response_add_content(nxt_unit_request_info_t *req, const void *src, uint32_t size) { conn_t *c = req_to_conn(req); - if (c == NULL || c->response_buf.free + size > c->response_buf.end) return NXT_UNIT_ERROR; + if (nxt_slow_path(c == NULL || c->response_buf.free + size > c->response_buf.end)) return NXT_UNIT_ERROR; memcpy(c->response_buf.free, src, (size_t) size); c->response_buf.free += size; c->response->content_length += size; @@ -915,7 +915,7 @@ int nxt_unit_response_add_content(nxt_unit_request_info_t *req, const void *src, int nxt_unit_response_send(nxt_unit_request_info_t *req) { conn_t *c = req_to_conn(req); int r; - if (c == NULL) return NXT_UNIT_ERROR; + if (nxt_slow_path(c == NULL)) return NXT_UNIT_ERROR; r = conn_send_response(c); #ifdef SCALANATIVE_MULTITHREADING_ENABLED pthread_mutex_lock(&c->dispatch_mutex); @@ -931,8 +931,8 @@ int nxt_unit_response_is_sent(nxt_unit_request_info_t *req) { } nxt_unit_buf_t *nxt_unit_response_buf_alloc(nxt_unit_request_info_t *req, uint32_t size) { conn_t *c = req_to_conn(req); - if (c == NULL) return NULL; - if (c->req_pool_used + size > REQ_POOL_SIZE) return NULL; + if (nxt_slow_path(c == NULL)) return NULL; + if (nxt_slow_path(c->req_pool_used + size > REQ_POOL_SIZE)) return NULL; c->response_buf.start = c->req_pool + c->req_pool_used; c->response_buf.free = c->response_buf.start; c->response_buf.end = c->response_buf.start + size; @@ -954,10 +954,10 @@ int nxt_unit_response_upgrade(nxt_unit_request_info_t *req) { const char *magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; size_t magic_len = 36; int i, n; - if (req == NULL || req->request == NULL || !req->request->websocket_handshake) + if (nxt_slow_path(req == NULL || req->request == NULL || !req->request->websocket_handshake)) return NXT_UNIT_ERROR; c = req_to_conn(req); - if (c == NULL) return NXT_UNIT_ERROR; + if (nxt_slow_path(c == NULL)) return NXT_UNIT_ERROR; r = req->request; for (i = 0; i < (int) r->fields_count; i++) { f = &r->fields[i]; @@ -967,7 +967,7 @@ int nxt_unit_response_upgrade(nxt_unit_request_info_t *req) { break; } } - if (key_ptr == NULL || key_len == 0 || key_len > 64) return NXT_UNIT_ERROR; + if (nxt_slow_path(key_ptr == NULL || key_len == 0 || key_len > 64)) return NXT_UNIT_ERROR; memcpy(key_buf, key_ptr, key_len); memcpy(key_buf + key_len, magic, magic_len); sha1_hash(key_buf, key_len + magic_len, accept_bin); @@ -975,7 +975,7 @@ int nxt_unit_response_upgrade(nxt_unit_request_info_t *req) { n = snprintf(c->send_buf, SEND_BUF_SIZE, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\nSec-WebSocket-Version: 13\r\n\r\n", accept_b64); - if (n <= 0 || (size_t) n >= SEND_BUF_SIZE) return NXT_UNIT_ERROR; + if (nxt_slow_path(n <= 0 || (size_t) n >= SEND_BUF_SIZE)) return NXT_UNIT_ERROR; c->send_len = (size_t) n; c->response_sent = 1; c->is_websocket = 1; @@ -1068,7 +1068,7 @@ uint32_t nxt_unit_buf_max(void) { return 65536; } uint32_t nxt_unit_buf_min(void) { return 4096; } int nxt_unit_response_write(nxt_unit_request_info_t *req, const void *start, size_t size) { conn_t *c = req_to_conn(req); - if (c == NULL || c->response_buf.free + size > c->response_buf.end) return NXT_UNIT_ERROR; + if (nxt_slow_path(c == NULL || c->response_buf.free + size > c->response_buf.end)) return NXT_UNIT_ERROR; memcpy(c->response_buf.free, start, size); c->response_buf.free += size; c->response->content_length += (uint64_t) size; @@ -1077,7 +1077,7 @@ int nxt_unit_response_write(nxt_unit_request_info_t *req, const void *start, siz ssize_t nxt_unit_response_write_nb(nxt_unit_request_info_t *req, const void *start, size_t size, size_t min_size) { (void) min_size; - if (nxt_unit_response_write(req, start, size) != NXT_UNIT_OK) return -1; + if (nxt_slow_path(nxt_unit_response_write(req, start, size) != NXT_UNIT_OK)) return -1; return (ssize_t) size; } int nxt_unit_response_write_cb(nxt_unit_request_info_t *req, nxt_unit_read_info_t *read_info) { @@ -1088,7 +1088,7 @@ int nxt_unit_response_write_cb(nxt_unit_request_info_t *req, nxt_unit_read_info_ ssize_t nxt_unit_request_read(nxt_unit_request_info_t *req, void *dst, size_t size) { conn_t *c = req_to_conn(req); size_t avail; - if (c == NULL) return -1; + if (nxt_slow_path(c == NULL)) return -1; avail = c->recv_len - c->recv_parsed; if (size > avail) size = avail; memcpy(dst, c->recv_buf + c->recv_parsed, size); @@ -1103,7 +1103,7 @@ ssize_t nxt_unit_request_readline_size(nxt_unit_request_info_t *req, size_t max_ void nxt_unit_request_done(nxt_unit_request_info_t *req, int rc) { conn_t *c = req_to_conn(req); (void) rc; - if (c != NULL) { + if (nxt_fast_path(c != NULL)) { #ifdef SCALANATIVE_MULTITHREADING_ENABLED pthread_mutex_lock(&c->dispatch_mutex); c->handler_done = 1; @@ -1118,12 +1118,12 @@ int nxt_unit_websocket_send(nxt_unit_request_info_t *req, uint8_t opcode, conn_t *c; size_t len, header_len, total; uint8_t *p; - if (req == NULL) return NXT_UNIT_ERROR; + if (nxt_slow_path(req == NULL)) return NXT_UNIT_ERROR; c = req_to_conn(req); - if (c == NULL || !c->is_websocket) return NXT_UNIT_ERROR; + if (nxt_slow_path(c == NULL || !c->is_websocket)) return NXT_UNIT_ERROR; header_len = 2 + (size > 125 ? (size > 65535 ? 8 : 2) : 0); total = header_len + size; - if (c->send_len + total > SEND_BUF_SIZE) return NXT_UNIT_ERROR; + if (nxt_slow_path(c->send_len + total > SEND_BUF_SIZE)) return NXT_UNIT_ERROR; p = (uint8_t *) (c->send_buf + c->send_len); p[0] = (uint8_t) ((last ? 0x80 : 0) | (opcode & 0x0F)); if (size <= 125) { @@ -1145,19 +1145,19 @@ int nxt_unit_websocket_sendv(nxt_unit_request_info_t *req, uint8_t opcode, uint8_t last, const struct iovec *iov, int iovcnt) { size_t total = 0; int i; - if (req == NULL || iov == NULL) return NXT_UNIT_ERROR; + if (nxt_slow_path(req == NULL || iov == NULL)) return NXT_UNIT_ERROR; for (i = 0; i < iovcnt; i++) total += iov[i].iov_len; - if (total == 0) + if (nxt_slow_path(total == 0)) return nxt_unit_websocket_send(req, opcode, last, NULL, 0); - if (total > SEND_BUF_SIZE) return NXT_UNIT_ERROR; + if (nxt_slow_path(total > SEND_BUF_SIZE)) return NXT_UNIT_ERROR; { conn_t *c = req_to_conn(req); size_t len, header_len, off = 0; uint8_t *p; - if (c == NULL || !c->is_websocket) return NXT_UNIT_ERROR; + if (nxt_slow_path(c == NULL || !c->is_websocket)) return NXT_UNIT_ERROR; header_len = 2 + (total > 125 ? (total > 65535 ? 8 : 2) : 0); - if (c->send_len + header_len + total > SEND_BUF_SIZE) return NXT_UNIT_ERROR; + if (nxt_slow_path(c->send_len + header_len + total > SEND_BUF_SIZE)) return NXT_UNIT_ERROR; p = (uint8_t *) (c->send_buf + c->send_len); p[0] = (uint8_t) ((last ? 0x80 : 0) | (opcode & 0x0F)); if (total <= 125) { @@ -1184,9 +1184,9 @@ ssize_t nxt_unit_websocket_read(nxt_unit_websocket_frame_t *ws, void *dst, size_ conn_t *c; size_t avail; ssize_t res; - if (ws == NULL || dst == NULL) return -1; + if (nxt_slow_path(ws == NULL || dst == NULL)) return -1; c = req_to_conn(ws->req); - if (c == NULL) return -1; + if (nxt_slow_path(c == NULL)) return -1; avail = (size_t) (ws->content_buf->end - ws->content_buf->free); if (size > avail) size = avail; memcpy(dst, ws->content_buf->free, size); @@ -1201,9 +1201,9 @@ int nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws) { } void nxt_unit_websocket_done(nxt_unit_websocket_frame_t *ws) { conn_t *c; - if (ws == NULL) return; + if (nxt_slow_path(ws == NULL)) return; c = req_to_conn(ws->req); - if (c != NULL) + if (nxt_fast_path(c != NULL)) c->recv_parsed += c->ws_frame_size; } @@ -1244,7 +1244,7 @@ static uint16_t field_hash(const char *name, size_t len) { static conn_t *conn_new(int fd, embed_ctx_t *emb) { conn_t *c = (conn_t *) calloc(1, sizeof(conn_t)); - if (c == NULL) return NULL; + if (nxt_slow_path(c == NULL)) return NULL; c->fd = fd; c->emb = emb; #ifdef SCALANATIVE_MULTITHREADING_ENABLED @@ -1255,8 +1255,8 @@ static conn_t *conn_new(int fd, embed_ctx_t *emb) { } static void conn_free(conn_t *c) { - if (c == NULL) return; - if (c->fd >= 0) close(c->fd); + if (nxt_slow_path(c == NULL)) return; + if (nxt_fast_path(c->fd >= 0)) close(c->fd); #ifdef SCALANATIVE_MULTITHREADING_ENABLED pthread_mutex_destroy(&c->dispatch_mutex); pthread_cond_destroy(&c->dispatch_cond); @@ -1297,7 +1297,7 @@ static int conn_parse_request(conn_t *c) { char *host_value = NULL; size_t host_value_len = 0; - if (c->req_info != NULL) return 0; + if (nxt_fast_path(c->req_info != NULL)) return 0; end = c->recv_buf + c->recv_len; headers_end = 0; for (p = c->recv_buf + c->recv_parsed; p + 2 <= end; p++) { @@ -1310,13 +1310,13 @@ static int conn_parse_request(conn_t *c) { break; } } - if (headers_end == 0) return 0; + if (nxt_slow_path(headers_end == 0)) return 0; c->recv_parsed = headers_end; c->headers_end = headers_end; /* Store for calculating consumed later */ end = c->recv_buf + headers_end; /* parse only up to end of headers */ /* Allocate conn* then request_info so req_to_conn can get conn from req (works from any thread) */ - if (c->req_pool_used + 8 + sizeof(conn_t *) + sizeof(nxt_unit_request_info_t) + sizeof(nxt_unit_request_t) + 64 * sizeof(nxt_unit_field_t) + 4096 > REQ_POOL_MAX) + if (nxt_slow_path(c->req_pool_used + 8 + sizeof(conn_t *) + sizeof(nxt_unit_request_info_t) + sizeof(nxt_unit_request_t) + 64 * sizeof(nxt_unit_field_t) + 4096 > REQ_POOL_MAX)) return -1; pool_align_8(c); *(conn_t **)(c->req_pool + c->req_pool_used) = c; @@ -1547,12 +1547,12 @@ static int conn_send_response(conn_t *c) { int n, i; size_t len, body_len; - if (c->response_sent) + if (nxt_fast_path(c->response_sent)) return NXT_UNIT_OK; /* Note: We always build headers here because response_sent == 0 means we're starting a new response. * Reset send_len to 0 to clear any leftover data from a previous response. */ c->send_len = 0; - if (c->response == NULL) { + if (nxt_slow_path(c->response == NULL)) { snprintf(c->send_buf, sizeof(c->send_buf), "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); c->send_len = strlen(c->send_buf); @@ -1561,7 +1561,7 @@ static int conn_send_response(conn_t *c) { r = c->response; body_len = (size_t)(c->response_buf.free - c->response_buf.start); n = snprintf(status_line, sizeof(status_line), "HTTP/1.1 %u \r\n", (unsigned) r->status); - if (n <= 0 || (size_t) n >= sizeof(status_line)) return NXT_UNIT_ERROR; + if (nxt_slow_path(n <= 0 || (size_t) n >= sizeof(status_line))) return NXT_UNIT_ERROR; len = 0; if (len + (size_t) n <= SEND_BUF_SIZE) memcpy(c->send_buf + len, status_line, (size_t) n); From 165bff1434c865a55d912d4ac730bd1fc025299b Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Thu, 5 Feb 2026 10:24:42 +0100 Subject: [PATCH 02/11] Improve handling of many connections --- .../scala-native/snunit/nxt_unit_embed.c | 125 ++++++++++++------ 1 file changed, 83 insertions(+), 42 deletions(-) diff --git a/snunit/resources/scala-native/snunit/nxt_unit_embed.c b/snunit/resources/scala-native/snunit/nxt_unit_embed.c index ed940d5..f13b65b 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_embed.c +++ b/snunit/resources/scala-native/snunit/nxt_unit_embed.c @@ -155,6 +155,7 @@ typedef struct { typedef struct conn conn_t; struct conn { conn_t *next; + conn_t **prev; /* & of the pointer that points to this node; O(1) unlink (from nxt_unit_mmap_buf); NULL when removed */ int fd; char recv_buf[RECV_BUF_SIZE]; size_t recv_len; @@ -194,10 +195,22 @@ static embed_ctx_t *global_emb; static conn_t *conn_new(int fd, embed_ctx_t *emb); static void conn_free(conn_t *c); +/* O(1) insert/unlink adapted from nxt_unit_mmap_buf_insert/nxt_unit_mmap_buf_unlink (nxt_unit.c) */ +static inline void conn_insert(conn_t **head, conn_t *c) { + c->next = *head; + if (c->next != NULL) c->next->prev = &c->next; + *head = c; + c->prev = head; +} +static inline void conn_unlink(conn_t *c) { + conn_t **p = c->prev; + if (c->next != NULL) c->next->prev = p; + if (p != NULL) *p = c->next; +} static int conn_parse_request(conn_t *c); static int conn_dispatch_request(conn_t *c); static int conn_send_response(conn_t *c); -static int conn_parse_websocket_frames(embed_ctx_t *emb, conn_t **p, conn_t *c); +static int conn_parse_websocket_frames(embed_ctx_t *emb, conn_t *c); static uint16_t field_hash(const char *name, size_t len); #if EMB_USE_EPOLL @@ -380,19 +393,23 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) } /* --- nxt_unit_run --- */ -/* Returns 1 if conn was removed (caller must not advance p), 0 otherwise. */ -static int run_loop_process_conn(embed_ctx_t *emb, conn_t **p, conn_t *c, int can_read, int can_write, int err_or_hup) { +/* Returns 1 if conn was removed (caller must not advance p), 0 otherwise. + * Removed conns are appended to *pending_free for deferred free (avoids use-after-free when + * epoll/kqueue delivers multiple events for the same fd in one batch). */ +static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int can_write, int err_or_hup, conn_t **pending_free) { int n; (void) emb; if (nxt_slow_path(err_or_hup)) { - *p = c->next; + conn_unlink(c); + c->prev = NULL; + c->next = *pending_free; + *pending_free = c; #if EMB_USE_EPOLL || EMB_USE_KQUEUE embed_ev_remove_conn(emb, c); #if EMB_USE_KQUEUE embed_kq_flush(emb); #endif #endif - conn_free(c); return 1; } /* Drain send buffer: up to EMB_DRAIN_MAX_WRITE write() per wakeup. */ @@ -416,14 +433,16 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t **p, conn_t *c, int ca } if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) break; - *p = c->next; + conn_unlink(c); + c->prev = NULL; + c->next = *pending_free; + *pending_free = c; #if EMB_USE_EPOLL || EMB_USE_KQUEUE embed_ev_remove_conn(emb, c); #if EMB_USE_KQUEUE embed_kq_flush(emb); #endif #endif - conn_free(c); return 1; } } @@ -444,50 +463,58 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t **p, conn_t *c, int ca continue; } if (n == 0) { - *p = c->next; + conn_unlink(c); + c->prev = NULL; + c->next = *pending_free; + *pending_free = c; #if EMB_USE_EPOLL || EMB_USE_KQUEUE embed_ev_remove_conn(emb, c); #if EMB_USE_KQUEUE embed_kq_flush(emb); #endif #endif - conn_free(c); return 1; } if (errno == EAGAIN || errno == EWOULDBLOCK) break; - *p = c->next; + conn_unlink(c); + c->prev = NULL; + c->next = *pending_free; + *pending_free = c; #if EMB_USE_EPOLL || EMB_USE_KQUEUE embed_ev_remove_conn(emb, c); #if EMB_USE_KQUEUE embed_kq_flush(emb); #endif #endif - conn_free(c); return 1; } } if (c->recv_len == 0) { - *p = c->next; + conn_unlink(c); + c->prev = NULL; + c->next = *pending_free; + *pending_free = c; #if EMB_USE_EPOLL || EMB_USE_KQUEUE embed_ev_remove_conn(emb, c); #if EMB_USE_KQUEUE embed_kq_flush(emb); #endif #endif - conn_free(c); return 1; } if (c->is_websocket) { - if (conn_parse_websocket_frames(emb, p, c) != 0) { - *p = c->next; + if (conn_parse_websocket_frames(emb, c) != 0) { + conn_unlink(c); + c->prev = NULL; + c->next = *pending_free; + *pending_free = c; #if EMB_USE_EPOLL || EMB_USE_KQUEUE embed_ev_remove_conn(emb, c); #if EMB_USE_KQUEUE embed_kq_flush(emb); #endif #endif - conn_free(c); return 1; } #if EMB_USE_EPOLL || EMB_USE_KQUEUE @@ -495,14 +522,16 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t **p, conn_t *c, int ca #endif } else { if (conn_parse_request(c) != 0) { - *p = c->next; + conn_unlink(c); + c->prev = NULL; + c->next = *pending_free; + *pending_free = c; #if EMB_USE_EPOLL || EMB_USE_KQUEUE embed_ev_remove_conn(emb, c); #if EMB_USE_KQUEUE embed_kq_flush(emb); #endif #endif - conn_free(c); return 1; } /* If we have req_info but not yet request_ready, we may be waiting for the full body (Content-Length) */ @@ -570,6 +599,7 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { #define EMB_EPOLL_MAXEV 256 struct epoll_event events[EMB_EPOLL_MAXEV]; int nevents, i; + conn_t *pending_free; emb->ev_fd = epoll_create(1); if (nxt_slow_path(emb->ev_fd < 0)) return NXT_UNIT_ERROR; @@ -584,6 +614,7 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } if (nxt_slow_path(nevents == 0)) continue; + pending_free = NULL; for (i = 0; i < nevents; i++) { int rev = events[i].events; if (nxt_slow_path(events[i].data.ptr == NULL)) { @@ -595,10 +626,9 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); c = conn_new(new_fd, emb); if (nxt_fast_path(c != NULL)) { - c->next = conns; - conns = c; + conn_insert(&conns, c); if (nxt_slow_path(embed_ev_add_conn(emb, c) != 0)) { - conns = c->next; + conn_unlink(c); conn_free(c); } } else @@ -607,15 +637,18 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { continue; } c = (conn_t *) events[i].data.ptr; - p = &conns; - while (*p != NULL && *p != c) p = &(*p)->next; - if (nxt_slow_path(*p == NULL)) continue; - if (nxt_slow_path(!run_loop_process_conn(emb, p, c, + if (nxt_slow_path(c->prev == NULL)) continue; /* already removed this batch */ + if (nxt_slow_path(!run_loop_process_conn(emb, c, (rev & EPOLLIN) != 0, (rev & EPOLLOUT) != 0, - (rev & (EPOLLERR | EPOLLHUP)) != 0)))) + (rev & (EPOLLERR | EPOLLHUP)) != 0, &pending_free))) embed_ev_rearm_conn(emb, c); } + while (pending_free != NULL) { + c = pending_free; + pending_free = c->next; + conn_free(c); + } } close(emb->ev_fd); emb->ev_fd = -1; @@ -626,6 +659,7 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { #define EMB_KQUEUE_MAXCHANGES 512 struct kevent events[EMB_KQUEUE_MAXEV]; int nevents, i; + conn_t *pending_free; emb->ev_fd = kqueue(); if (nxt_slow_path(emb->ev_fd < 0)) return NXT_UNIT_ERROR; @@ -650,6 +684,7 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } if (nxt_slow_path(nevents == 0)) continue; + pending_free = NULL; for (i = 0; i < nevents; i++) { int filter = events[i].filter; int flags = events[i].flags; @@ -664,10 +699,9 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); c = conn_new(new_fd, emb); if (nxt_fast_path(c != NULL)) { - c->next = conns; - conns = c; + conn_insert(&conns, c); if (nxt_slow_path(embed_ev_add_conn(emb, c) != 0)) { - conns = c->next; + conn_unlink(c); conn_free(c); } } else @@ -677,13 +711,16 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { continue; } c = (conn_t *) events[i].udata; - p = &conns; - while (*p != NULL && *p != c) p = &(*p)->next; - if (nxt_slow_path(*p == NULL)) continue; + if (nxt_slow_path(c->prev == NULL)) continue; /* already removed this batch */ if (nxt_fast_path(filter == EVFILT_READ)) - run_loop_process_conn(emb, p, c, 1, 0, err_or_hup); + run_loop_process_conn(emb, c, 1, 0, err_or_hup, &pending_free); else if (filter == EVFILT_WRITE) - run_loop_process_conn(emb, p, c, 0, 1, err_or_hup); + run_loop_process_conn(emb, c, 0, 1, err_or_hup, &pending_free); + } + while (pending_free != NULL) { + c = pending_free; + pending_free = c->next; + conn_free(c); } } free(emb->kq_changes); @@ -695,6 +732,7 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { { struct pollfd *pfds; int nfds, cap, i; + conn_t *pending_free; cap = 64; pfds = (struct pollfd *) malloc((size_t) cap * sizeof(struct pollfd)); @@ -731,23 +769,28 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); c = conn_new(new_fd, emb); if (nxt_fast_path(c != NULL)) { - c->next = conns; - conns = c; + conn_insert(&conns, c); } else close(new_fd); } } + pending_free = NULL; p = &conns; while (*p != NULL) { c = *p; for (i = 1; i < nfds && pfds[i].fd != c->fd; i++) ; if (nxt_slow_path(i >= nfds)) { p = &c->next; continue; } - if (nxt_slow_path(!run_loop_process_conn(emb, p, c, + if (nxt_slow_path(!run_loop_process_conn(emb, c, (pfds[i].revents & POLLIN) != 0, (pfds[i].revents & POLLOUT) != 0, - (pfds[i].revents & (POLLERR | POLLHUP)) != 0)))) - p = &(*p)->next; + (pfds[i].revents & (POLLERR | POLLHUP)) != 0, &pending_free))) + p = &c->next; + } + while (pending_free != NULL) { + c = pending_free; + pending_free = c->next; + conn_free(c); } } free(pfds); @@ -987,13 +1030,11 @@ int nxt_unit_response_is_websocket(nxt_unit_request_info_t *req) { } /* Parse WebSocket frames from recv_buf; dispatch each to websocket_handler. Returns 0 on success, -1 on error/close. */ -static int conn_parse_websocket_frames(embed_ctx_t *emb, conn_t **p, conn_t *c) { +static int conn_parse_websocket_frames(embed_ctx_t *emb, conn_t *c) { size_t payload_len, frame_len, ext_len, i; uint8_t *buf; uint64_t payload_len64; conn_t *prev_conn; - - (void) p; while (c->recv_parsed + 2 <= c->recv_len) { buf = (uint8_t *) (c->recv_buf + c->recv_parsed); memcpy(&c->ws_header, buf, 2); From 19224f4aa5f0ab3884aa34675bd4bb76e62ee160 Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Wed, 11 Feb 2026 09:58:15 +0100 Subject: [PATCH 03/11] Update mill, improve tests --- .github/workflows/build.yml | 2 ++ build.mill | 2 +- integration/test/src/utils.scala | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 318391c..f363919 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,8 @@ jobs: run: ./mill __.compile - name: Publish libraries locally run: ./mill __.publishLocal + - name: Link all test binaries + run: ./mill __.nativeLink - name: Run Unit Tests run: ./mill snunit.test - name: Run Integration Tests diff --git a/build.mill b/build.mill index 004f6c8..09be438 100644 --- a/build.mill +++ b/build.mill @@ -1,4 +1,4 @@ -//| mill-version: 1.1.1 +//| mill-version: 1.1.2 //| mvnDeps: //| - com.goyeau::mill-scalafix::0.6.0 //| - com.lihaoyi::mill-contrib-buildinfo:$MILL_VERSION diff --git a/integration/test/src/utils.scala b/integration/test/src/utils.scala index 40ef244..42b45af 100644 --- a/integration/test/src/utils.scala +++ b/integration/test/src/utils.scala @@ -10,7 +10,7 @@ import sttp.client3.HttpClientFutureBackend private def runMillCommand(command: String) = os .proc( "./mill", - // adding `-i` breaks the ability to close unitd processes + "-i", "--no-build-lock", "--ticker", "false", From dba5bc209963e374f2142efa3d03aeab58b92b36 Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Wed, 11 Feb 2026 10:35:35 +0100 Subject: [PATCH 04/11] Update cache action --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f363919..3e87357 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,7 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 + - uses: coursier/cache-action@v8 - name: Check Binary Compatibility run: ./mill __.mimaReportBinaryIssues @@ -55,7 +56,7 @@ jobs: LC_ALL: "en_US.UTF-8" steps: - uses: actions/checkout@v5 - - uses: coursier/cache-action@v7 + - uses: coursier/cache-action@v8 - name: Publish to Maven Central run: | if [[ $(git tag --points-at HEAD) != '' ]]; then From c874b276ed704c2c5d840cee75589517ca9f7884 Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Wed, 11 Feb 2026 11:07:52 +0100 Subject: [PATCH 05/11] . --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e87357..6f1258e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: - name: Publish libraries locally run: ./mill __.publishLocal - name: Link all test binaries - run: ./mill __.nativeLink + run: ./mill integration.tests.__.nativeLink - name: Run Unit Tests run: ./mill snunit.test - name: Run Integration Tests From 14245547592a988306525d4fe11dee3d12c9796f Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Wed, 11 Feb 2026 11:58:03 +0100 Subject: [PATCH 06/11] . --- .github/workflows/build.yml | 8 ++++++++ build.mill | 1 + 2 files changed, 9 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f1258e..ece11c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,10 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" - uses: coursier/cache-action@v7 - name: Install Dependencies run: | @@ -56,6 +60,10 @@ jobs: LC_ALL: "en_US.UTF-8" steps: - uses: actions/checkout@v5 + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "17" - uses: coursier/cache-action@v8 - name: Publish to Maven Central run: | diff --git a/build.mill b/build.mill index 09be438..7aec151 100644 --- a/build.mill +++ b/build.mill @@ -1,4 +1,5 @@ //| mill-version: 1.1.2 +//| mill-jvm-version: system //| mvnDeps: //| - com.goyeau::mill-scalafix::0.6.0 //| - com.lihaoyi::mill-contrib-buildinfo:$MILL_VERSION From 84aa1fe3f1ca8744f63453a71ad6dbc2f2d363fa Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Wed, 11 Feb 2026 12:21:05 +0100 Subject: [PATCH 07/11] . --- .../src/io/undertow/server/util/HeaderValues.scala | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/snunit-undertow/src/io/undertow/server/util/HeaderValues.scala b/snunit-undertow/src/io/undertow/server/util/HeaderValues.scala index 0485470..9102c62 100644 --- a/snunit-undertow/src/io/undertow/server/util/HeaderValues.scala +++ b/snunit-undertow/src/io/undertow/server/util/HeaderValues.scala @@ -7,8 +7,16 @@ final class HeaderValues private[undertow] (key: String, value: String) with java.util.Deque[String] with java.util.List[String] { def getHeaderName(): String = key - def descendingIterator(): java.util.Iterator[String] = ??? - def element(): String = ??? + def descendingIterator(): java.util.Iterator[String] = Array(value).iterator.asJava + def element(): String = value + + // Deque methods (single-element: get/peek return value; add/remove throw) + def addFirst(x$0: String): Unit = throw new UnsupportedOperationException + def addLast(x$0: String): Unit = throw new UnsupportedOperationException + def getFirst(): String = value + def getLast(): String = value + def removeFirst(): String = throw new UnsupportedOperationException + def removeLast(): String = throw new UnsupportedOperationException // Members declared in java.util.List def add(x$1: Int, x$2: String): Unit = ??? @@ -37,5 +45,5 @@ final class HeaderValues private[undertow] (key: String, value: String) def remove(): String = ??? def removeFirstOccurrence(x$0: Object): Boolean = ??? def removeLastOccurrence(x$0: Object): Boolean = ??? - override def reversed(): HeaderValues = ??? + def reversed(): HeaderValues = this } From e9c4cd0e8687a435b50b0ea82fc3db6a657a1755 Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Fri, 13 Feb 2026 18:08:56 +0100 Subject: [PATCH 08/11] Multithreading improvements and bug-fixes --- .../scala-native/snunit/nxt_unit_embed.c | 264 ++++++++++++++++-- snunit/src/snunit/Request.scala | 2 +- snunit/src/snunit/unsafe/stringUtils.scala | 39 ++- snunit/src/snunit/unsafe/unsafe.scala | 6 +- 4 files changed, 264 insertions(+), 47 deletions(-) diff --git a/snunit/resources/scala-native/snunit/nxt_unit_embed.c b/snunit/resources/scala-native/snunit/nxt_unit_embed.c index f13b65b..b7d9bf4 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_embed.c +++ b/snunit/resources/scala-native/snunit/nxt_unit_embed.c @@ -125,6 +125,13 @@ static void base64_encode(const unsigned char *in, size_t inlen, char *out, size } /* --- Embed context --- */ +typedef struct conn conn_t; + +#ifdef SCALANATIVE_MULTITHREADING_ENABLED +/* Sentinel udata for handler wake pipe (main loop never blocks on a single request, like NGINX Unit). */ +static void *const handler_pipe_udata = (void *) (intptr_t) 1; +#endif + typedef struct { nxt_unit_t unit; nxt_unit_ctx_t ctx; @@ -134,6 +141,10 @@ typedef struct { char listen_addr[64]; int quit; int ev_fd; /* epoll fd (Linux) or kqueue fd (BSD/macOS); -1 when not running */ +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + int handler_pipe[2]; /* [0]=read (in event loop), [1]=write (worker signals) */ + conn_t **conns; /* set in run loop so pipe handler can iterate */ +#endif #if EMB_USE_KQUEUE struct kevent *kq_changes; /* batched changes; applied in main loop kevent() */ int kq_nchanges; @@ -152,7 +163,6 @@ typedef struct { #define RESPONSE_POOL_RESERVE 8192 #define REQ_POOL_MAX (REQ_POOL_SIZE - RESPONSE_POOL_RESERVE) -typedef struct conn conn_t; struct conn { conn_t *next; conn_t **prev; /* & of the pointer that points to this node; O(1) unlink (from nxt_unit_mmap_buf); NULL when removed */ @@ -167,10 +177,12 @@ struct conn { int response_sent; embed_ctx_t *emb; #ifdef SCALANATIVE_MULTITHREADING_ENABLED - /* Sync when handler runs on another thread (e.g. BlockingHandler): main thread waits until handler calls response_send or request_done. */ + /* Handler runs on another thread (like NGINX Unit: invoke and continue, no blocking). */ pthread_mutex_t dispatch_mutex; pthread_cond_t dispatch_cond; - int handler_done; + int handler_done; /* set by worker when response_send/request_done */ + int handler_started; /* main thread dispatched this request */ + int client_closed; /* client hung up while handler running; free when handler_done */ #endif /* Request/response structs point into req_pool */ nxt_unit_request_info_t *req_info; @@ -181,6 +193,7 @@ struct conn { nxt_unit_buf_t content_buf; char req_pool[REQ_POOL_SIZE]; size_t req_pool_used; + size_t response_pool_end; /* when > 0, response allocated from end of pool (worker after cleanup) */ int is_websocket; /* WebSocket frame state (when is_websocket) */ nxt_websocket_header_t ws_header; @@ -212,6 +225,9 @@ static int conn_dispatch_request(conn_t *c); static int conn_send_response(conn_t *c); static int conn_parse_websocket_frames(embed_ctx_t *emb, conn_t *c); static uint16_t field_hash(const char *name, size_t len); +#ifdef SCALANATIVE_MULTITHREADING_ENABLED +static void conn_handler_done_cleanup(conn_t *c); +#endif #if EMB_USE_EPOLL static int embed_ev_add_listen(embed_ctx_t *emb); @@ -227,6 +243,7 @@ static int embed_ev_add_listen(embed_ctx_t *emb); static int embed_ev_add_conn(embed_ctx_t *emb, conn_t *c); static void embed_ev_remove_conn(embed_ctx_t *emb, conn_t *c); static void embed_ev_want_write(embed_ctx_t *emb, conn_t *c, int want); +static void embed_ev_rearm_conn(embed_ctx_t *emb, conn_t *c); #endif #if EMB_USE_EPOLL @@ -313,6 +330,14 @@ static void embed_ev_want_write(embed_ctx_t *emb, conn_t *c, int want) { else EV_SET(kev, c->fd, EVFILT_WRITE, EV_DISABLE, 0, 0, c); } +/* Re-arm after processing (EV_CLEAR consumes one event; re-enable so we get more). */ +static void embed_ev_rearm_conn(embed_ctx_t *emb, conn_t *c) { + struct kevent *kev; + kev = embed_kq_change(emb); + EV_SET(kev, c->fd, EVFILT_READ, EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, c); + kev = embed_kq_change(emb); + EV_SET(kev, c->fd, EVFILT_WRITE, EV_ADD | (c->send_len > 0 ? EV_ENABLE : EV_DISABLE) | EV_CLEAR | EMB_KQ_WRITE_ONESHOT, 0, 0, c); +} #endif /* --- nxt_unit_init --- */ @@ -388,6 +413,17 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) return NULL; } emb->listen_fd = fd; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + emb->handler_pipe[0] = -1; + emb->handler_pipe[1] = -1; + if (pipe(emb->handler_pipe) == 0) { + fcntl(emb->handler_pipe[0], F_SETFL, O_NONBLOCK); + } else { + emb->handler_pipe[0] = -1; + emb->handler_pipe[1] = -1; + } + emb->conns = NULL; +#endif global_emb = emb; return &emb->ctx; } @@ -400,6 +436,20 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int int n; (void) emb; if (nxt_slow_path(err_or_hup)) { +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Client closed connection while async handler may still be running. Defer unlink/free + * until handler_done (pipe wake) to avoid use-after-free in nxt_unit_response_send. */ + if (c->handler_started && !c->handler_done) { + c->client_closed = 1; +#if EMB_USE_EPOLL || EMB_USE_KQUEUE + embed_ev_remove_conn(emb, c); +#if EMB_USE_KQUEUE + embed_kq_flush(emb); +#endif +#endif + return 0; + } +#endif conn_unlink(c); c->prev = NULL; c->next = *pending_free; @@ -541,7 +591,19 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int c->request_ready = 1; } if (c->request_ready) { - c->response_sent = 0; /* allow send for this request (keep-alive: next request on same conn) */ +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Like NGINX Unit: invoke handler and continue; cleanup on pipe wake. */ + if (!c->handler_started) { + c->response_sent = 0; + conn_dispatch_request(c); +#if EMB_USE_EPOLL || EMB_USE_KQUEUE + if (c->send_len > 0) embed_ev_want_write(emb, c, 1); +#endif + return 0; + } + /* handler_started: cleanup when pipe is read; fall through to allow send drain */ +#else + c->response_sent = 0; conn_dispatch_request(c); conn_send_response(c); #if EMB_USE_EPOLL || EMB_USE_KQUEUE @@ -549,18 +611,12 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int #endif c->request_ready = 0; c->response_sent = 1; - /* Calculate consumed: end of headers + body length. - * recv_parsed may have been advanced by nxt_unit_request_read during handler, - * but we need to consume the entire request including the body, even if the app didn't read it. */ size_t consumed; if (c->request != NULL && c->request->content_length > 0) { - /* Consume full request: headers_end (stored when parsed) + body length */ consumed = c->headers_end + (size_t) c->request->content_length; if (consumed > c->recv_len) consumed = c->recv_len; - /* Also ensure we consume at least what was read (if body was partially read) */ if (consumed < c->recv_parsed) consumed = c->recv_parsed; } else { - /* No body, consumed is just what was parsed (end of headers) */ consumed = c->recv_parsed; } if (consumed > 0 && consumed < c->recv_len) { @@ -577,6 +633,7 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int c->request = NULL; c->response = NULL; } +#endif } } } @@ -608,7 +665,19 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { emb->ev_fd = -1; return NXT_UNIT_ERROR; } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + if (emb->handler_pipe[0] >= 0) { + struct epoll_event ev; + memset(&ev, 0, sizeof(ev)); + ev.events = EPOLLIN; + ev.data.ptr = handler_pipe_udata; + epoll_ctl(emb->ev_fd, EPOLL_CTL_ADD, emb->handler_pipe[0], &ev); + } +#endif while (!emb->quit) { +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + emb->conns = &conns; +#endif /* -1: block until an event. No spinning (0) or long wait (1000ms). */ nevents = epoll_wait(emb->ev_fd, events, EMB_EPOLL_MAXEV, -1); if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } @@ -617,6 +686,29 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { pending_free = NULL; for (i = 0; i < nevents; i++) { int rev = events[i].events; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + if (events[i].data.ptr == handler_pipe_udata) { + char buf[256]; + conn_t *dc; + while (read(emb->handler_pipe[0], buf, sizeof(buf)) > 0) ; + for (dc = *emb->conns; dc != NULL; dc = dc->next) { + pthread_mutex_lock(&dc->dispatch_mutex); + if (dc->handler_done) { + conn_handler_done_cleanup(dc); + if (dc->client_closed) { + conn_unlink(dc); + dc->prev = NULL; + dc->next = pending_free; + pending_free = dc; + } else if (dc->send_len > 0) { + embed_ev_want_write(emb, dc, 1); + } + } + pthread_mutex_unlock(&dc->dispatch_mutex); + } + continue; + } +#endif if (nxt_slow_path(events[i].data.ptr == NULL)) { /* listen fd */ peer_len = sizeof(peer); @@ -650,6 +742,10 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { conn_free(c); } } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + if (emb->handler_pipe[0] >= 0) { close(emb->handler_pipe[0]); emb->handler_pipe[0] = -1; } + if (emb->handler_pipe[1] >= 0) { close(emb->handler_pipe[1]); emb->handler_pipe[1] = -1; } +#endif close(emb->ev_fd); emb->ev_fd = -1; } @@ -677,8 +773,17 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { emb->ev_fd = -1; return NXT_UNIT_ERROR; } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + if (emb->handler_pipe[0] >= 0) { + struct kevent *kev = embed_kq_change(emb); + EV_SET(kev, emb->handler_pipe[0], EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, handler_pipe_udata); + } +#endif /* Single kevent(apply+wait) for low latency. EV_DISPATCH/EV_ONESHOT on write gives one event per enable so we don't spin. */ while (!emb->quit) { +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + emb->conns = &conns; +#endif nevents = kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, events, EMB_KQUEUE_MAXEV, NULL); emb->kq_nchanges = 0; if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } @@ -689,6 +794,29 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { int filter = events[i].filter; int flags = events[i].flags; int err_or_hup = (flags & EV_ERROR) != 0 || (flags & EV_EOF) != 0; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + if (events[i].udata == handler_pipe_udata) { + char buf[256]; + conn_t *dc; + while (read(emb->handler_pipe[0], buf, sizeof(buf)) > 0) ; + for (dc = *emb->conns; dc != NULL; dc = dc->next) { + pthread_mutex_lock(&dc->dispatch_mutex); + if (dc->handler_done) { + conn_handler_done_cleanup(dc); + if (dc->client_closed) { + conn_unlink(dc); + dc->prev = NULL; + dc->next = pending_free; + pending_free = dc; + } else if (dc->send_len > 0) { + embed_ev_want_write(emb, dc, 1); + } + } + pthread_mutex_unlock(&dc->dispatch_mutex); + } + continue; + } +#endif if (nxt_slow_path(events[i].udata == NULL)) { /* listen fd */ if (nxt_fast_path(filter == EVFILT_READ && !err_or_hup)) { @@ -712,17 +840,26 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { } c = (conn_t *) events[i].udata; if (nxt_slow_path(c->prev == NULL)) continue; /* already removed this batch */ - if (nxt_fast_path(filter == EVFILT_READ)) - run_loop_process_conn(emb, c, 1, 0, err_or_hup, &pending_free); - else if (filter == EVFILT_WRITE) - run_loop_process_conn(emb, c, 0, 1, err_or_hup, &pending_free); + if (nxt_fast_path(filter == EVFILT_READ)) { + if (!run_loop_process_conn(emb, c, 1, 0, err_or_hup, &pending_free)) + embed_ev_rearm_conn(emb, c); + } else if (filter == EVFILT_WRITE) { + if (!run_loop_process_conn(emb, c, 0, 1, err_or_hup, &pending_free)) + embed_ev_rearm_conn(emb, c); + } } + /* Apply EV_DELETE for removed conns before closing fds; otherwise next kevent() can get EBADF and exit the loop. */ + embed_kq_flush(emb); while (pending_free != NULL) { c = pending_free; pending_free = c->next; conn_free(c); } } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + if (emb->handler_pipe[0] >= 0) { close(emb->handler_pipe[0]); emb->handler_pipe[0] = -1; } + if (emb->handler_pipe[1] >= 0) { close(emb->handler_pipe[1]); emb->handler_pipe[1] = -1; } +#endif free(emb->kq_changes); emb->kq_changes = NULL; close(emb->ev_fd); @@ -877,6 +1014,9 @@ static conn_t *req_to_conn(nxt_unit_request_info_t *req) { if (nxt_slow_path(req == NULL)) return NULL; c = *(conn_t **)((char *) req - sizeof(conn_t *)); if (nxt_fast_path(c != NULL && c->req_info == req)) return c; + /* Worker thread: req_info may have been cleared by main-thread cleanup; if req is in c's pool, use c. */ + if (c != NULL && (char *)req >= (char *)c->req_pool && (char *)req < (char *)c->req_pool + REQ_POOL_SIZE) + return c; return current_dispatch_conn; } @@ -892,12 +1032,22 @@ int nxt_unit_response_init(nxt_unit_request_info_t *req, uint16_t status, uint32_t max_fields_count, uint32_t max_fields_size) { conn_t *c = req_to_conn(req); uint32_t alloc_count; + size_t alloc_size; if (nxt_slow_path(c == NULL || c->response != NULL)) return NXT_UNIT_ERROR; alloc_count = max_fields_count < RESPONSE_MIN_FIELDS ? RESPONSE_MIN_FIELDS : max_fields_count; - pool_align_8(c); - c->response = (nxt_unit_response_t *) (c->req_pool + c->req_pool_used); - c->req_pool_used += sizeof(nxt_unit_response_t) + (size_t) alloc_count * sizeof(nxt_unit_field_t); - if (nxt_slow_path(c->req_pool_used > REQ_POOL_SIZE)) return NXT_UNIT_ERROR; + alloc_size = sizeof(nxt_unit_response_t) + (size_t) alloc_count * sizeof(nxt_unit_field_t); + alloc_size = (alloc_size + 7u) & (size_t)~(size_t)7; /* align up to 8 */ + if (c->req_pool_used == 0 && c->response_pool_end == 0) { + /* Cleanup ran; allocate from end of pool so we don't overwrite request_info (req). */ + if (nxt_slow_path(alloc_size >= REQ_POOL_SIZE)) return NXT_UNIT_ERROR; + c->response_pool_end = REQ_POOL_SIZE - alloc_size; + c->response = (nxt_unit_response_t *) (c->req_pool + c->response_pool_end); + } else { + pool_align_8(c); + c->response = (nxt_unit_response_t *) (c->req_pool + c->req_pool_used); + c->req_pool_used += alloc_size; + if (nxt_slow_path(c->req_pool_used > REQ_POOL_SIZE)) return NXT_UNIT_ERROR; + } c->response->content_length = 0; c->response->fields_count = 0; c->response->piggyback_content_length = 0; @@ -926,11 +1076,22 @@ int nxt_unit_response_add_field(nxt_unit_request_info_t *req, conn_t *c = req_to_conn(req); nxt_unit_field_t *f; char *dst; + size_t need; if (nxt_slow_path(c == NULL || c->response == NULL)) return NXT_UNIT_ERROR; if (nxt_slow_path(c->response->fields_count >= req->response_max_fields)) return NXT_UNIT_ERROR; - if (nxt_slow_path(c->req_pool_used + (size_t) name_length + (size_t) value_length + 32 > REQ_POOL_SIZE)) return NXT_UNIT_ERROR; + need = (size_t) name_length + (size_t) value_length + 2; + if (c->response_pool_end != 0) { + /* Response allocated from end; allocate name/value from end too. */ + if (nxt_slow_path(need > c->response_pool_end || c->response_pool_end - need < c->req_pool_used)) + return NXT_UNIT_ERROR; + c->response_pool_end -= need; + dst = c->req_pool + c->response_pool_end; + } else { + if (nxt_slow_path(c->req_pool_used + need + 32 > REQ_POOL_SIZE)) return NXT_UNIT_ERROR; + dst = c->req_pool + c->req_pool_used; + c->req_pool_used += need; + } f = &c->response->fields[c->response->fields_count]; - dst = c->req_pool + c->req_pool_used; f->hash = (uint16_t) field_hash(name, (size_t) name_length); f->skip = 0; f->hopbyhop = 0; @@ -939,11 +1100,9 @@ int nxt_unit_response_add_field(nxt_unit_request_info_t *req, f->name.offset = (uint32_t) ((uintptr_t) dst - (uintptr_t) &f->name); memcpy(dst, name, (size_t) name_length); dst[name_length] = '\0'; - c->req_pool_used += (size_t) name_length + 1; - f->value.offset = (uint32_t) ((uintptr_t) (c->req_pool + c->req_pool_used) - (uintptr_t) &f->value); - memcpy(c->req_pool + c->req_pool_used, value, (size_t) value_length); - *(c->req_pool + c->req_pool_used + (size_t) value_length) = '\0'; - c->req_pool_used += (size_t) value_length + 1; + f->value.offset = (uint32_t) ((uintptr_t) (dst + (size_t) name_length + 1) - (uintptr_t) &f->value); + memcpy(dst + (size_t) name_length + 1, value, (size_t) value_length); + dst[(size_t) name_length + 1 + (size_t) value_length] = '\0'; c->response->fields_count++; return NXT_UNIT_OK; } @@ -965,6 +1124,11 @@ int nxt_unit_response_send(nxt_unit_request_info_t *req) { c->handler_done = 1; pthread_cond_signal(&c->dispatch_cond); pthread_mutex_unlock(&c->dispatch_mutex); + /* Wake main loop so it can run conn_handler_done_cleanup (like Unit: no blocking). */ + if (c->emb->handler_pipe[1] >= 0) { + char b = 1; + (void) write(c->emb->handler_pipe[1], &b, 1); + } #endif return r; } @@ -1150,6 +1314,10 @@ void nxt_unit_request_done(nxt_unit_request_info_t *req, int rc) { c->handler_done = 1; pthread_cond_signal(&c->dispatch_cond); pthread_mutex_unlock(&c->dispatch_mutex); + if (c->emb->handler_pipe[1] >= 0) { + char b = 1; + (void) write(c->emb->handler_pipe[1], &b, 1); + } #endif } } @@ -1291,6 +1459,7 @@ static conn_t *conn_new(int fd, embed_ctx_t *emb) { #ifdef SCALANATIVE_MULTITHREADING_ENABLED pthread_mutex_init(&c->dispatch_mutex, NULL); pthread_cond_init(&c->dispatch_cond, NULL); + c->handler_started = 0; #endif return c; } @@ -1563,20 +1732,50 @@ static int conn_parse_request(conn_t *c) { return 0; } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED +/* Called from main loop when worker has set handler_done (after pipe wake). */ +static void conn_handler_done_cleanup(conn_t *c) { + size_t consumed; + if (c->request != NULL && c->request->content_length > 0) { + consumed = c->headers_end + (size_t) c->request->content_length; + if (consumed > c->recv_len) consumed = c->recv_len; + if (consumed < c->recv_parsed) consumed = c->recv_parsed; + } else { + consumed = c->recv_parsed; + } + if (consumed > 0 && consumed < c->recv_len) { + memmove(c->recv_buf, c->recv_buf + consumed, c->recv_len - consumed); + c->recv_len -= consumed; + } else if (consumed >= c->recv_len) { + c->recv_len = 0; + } + c->recv_parsed = 0; + c->headers_end = 0; + c->req_pool_used = 0; + c->response_pool_end = 0; + c->request_ready = 0; + c->response_sent = 1; + c->handler_started = 0; + c->handler_done = 0; + if (!c->is_websocket) { + c->req_info = NULL; + c->request = NULL; + c->response = NULL; + } +} +#endif + static int conn_dispatch_request(conn_t *c) { conn_t *prev = current_dispatch_conn; current_dispatch_conn = c; #ifdef SCALANATIVE_MULTITHREADING_ENABLED c->handler_done = 0; + c->handler_started = 1; #endif if (c->emb->init->callbacks.request_handler != NULL) c->emb->init->callbacks.request_handler(c->req_info); #ifdef SCALANATIVE_MULTITHREADING_ENABLED - /* If handler runs on another thread (e.g. BlockingHandler), wait until it calls response_send or request_done. */ - pthread_mutex_lock(&c->dispatch_mutex); - while (!c->handler_done) - pthread_cond_wait(&c->dispatch_cond, &c->dispatch_mutex); - pthread_mutex_unlock(&c->dispatch_mutex); + /* Like NGINX Unit: do not wait. Worker will call response_send/request_done and write to handler_pipe. */ #endif current_dispatch_conn = prev; return 0; @@ -1632,6 +1831,10 @@ static int conn_send_response(conn_t *c) { } c->send_len = len; send: +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Don't block worker on write(); main loop drains send_buf when pipe wakes. */ + c->response_sent = 1; +#else n = (int) write(c->fd, c->send_buf, c->send_len); if (n > 0) { memmove(c->send_buf, c->send_buf + (size_t) n, c->send_len - (size_t) n); @@ -1639,5 +1842,6 @@ static int conn_send_response(conn_t *c) { } if (c->send_len == 0) c->response_sent = 1; +#endif return NXT_UNIT_OK; } diff --git a/snunit/src/snunit/Request.scala b/snunit/src/snunit/Request.scala index 4ee6e8b..4ab4223 100644 --- a/snunit/src/snunit/Request.scala +++ b/snunit/src/snunit/Request.scala @@ -34,7 +34,7 @@ extension (req: Request) { @inline def headersLength: Int = req.request.fields_count private inline def checkIndex(index: Int): Unit = { - if (index < 0 && index >= req.request.fields_count) + if (index < 0 || index >= req.request.fields_count) throw new IndexOutOfBoundsException(s"Index $index out of bounds for length ${req.request.fields_count}") } def headerName(index: Int): String = { diff --git a/snunit/src/snunit/unsafe/stringUtils.scala b/snunit/src/snunit/unsafe/stringUtils.scala index 1e72285..a2f6e94 100644 --- a/snunit/src/snunit/unsafe/stringUtils.scala +++ b/snunit/src/snunit/unsafe/stringUtils.scala @@ -4,6 +4,8 @@ import java.nio.ByteBuffer import java.nio.CharBuffer import java.nio._ import java.nio.charset.Charset +import java.nio.charset.CharsetDecoder +import java.nio.charset.CharsetEncoder import java.nio.charset.CoderResult import scala.scalanative.memory.PointerBuffer import scala.scalanative.runtime.GC @@ -19,19 +21,28 @@ import scala.scalanative.runtime.toRawPtr import scala.scalanative.unsafe._ private val charset = Charset.defaultCharset() -private val encoder = charset.newEncoder() -private val decoder = charset.newDecoder() + +/* CharsetEncoder/Decoder are not thread-safe. Handlers can run on worker threads. */ +private val encoder = new ThreadLocal[CharsetEncoder] { + override def initialValue(): CharsetEncoder = charset.newEncoder() +} +private val decoder = new ThreadLocal[CharsetDecoder] { + override def initialValue(): CharsetDecoder = charset.newDecoder() +} private final val sharedBufferSize = 4000 -private final val sharedBuffer = ByteBuffer.allocate(sharedBufferSize) +private val sharedBuffer = new ThreadLocal[ByteBuffer] { + override def initialValue(): ByteBuffer = ByteBuffer.allocate(sharedBufferSize) +} private[snunit] def fromCStringAndSize(cstr: CString, size: Int): String = { if (size > 0) { val inputBuffer = PointerBuffer.wrap(cstr, size) val output: CharBuffer = CharBuffer.allocate(size) - decoder.reset() - decoder.decode(inputBuffer, output, true) + val dec = decoder.get() + dec.reset() + dec.decode(inputBuffer, output, true) // write String fields val result = new String(Array.emptyCharArray) @@ -47,22 +58,24 @@ private[snunit] def fromCStringAndSize(cstr: CString, size: Int): String = { private[snunit] inline def readStringBytesWith(string: String)(inline f: ByteBuffer => Unit) = { val input: CharBuffer = newCharBuffer(string) + val enc = encoder.get() + val buf = sharedBuffer.get() var result = CoderResult.OVERFLOW while (result.isOverflow()) { - encoder.reset() - sharedBuffer.clear() - result = encoder.encode(input, sharedBuffer, true) - sharedBuffer.flip() - f(sharedBuffer) + enc.reset() + buf.clear() + result = enc.encode(input, buf, true) + buf.flip() + f(buf) } } private[snunit] def stringBytes(string: String): ByteBuffer = { val input: CharBuffer = newCharBuffer(string) - - encoder.reset() - encoder.encode(input) + val enc = encoder.get() + enc.reset() + enc.encode(input) } extension (buffer: ByteBuffer) diff --git a/snunit/src/snunit/unsafe/unsafe.scala b/snunit/src/snunit/unsafe/unsafe.scala index 78a7750..9a76d75 100644 --- a/snunit/src/snunit/unsafe/unsafe.scala +++ b/snunit/src/snunit/unsafe/unsafe.scala @@ -133,9 +133,9 @@ object externs { * * The normally function returns when QUIT message received from Unit. */ - def nxt_unit_run(ctx: nxt_unit_ctx_t_*): CInt = extern + @blocking def nxt_unit_run(ctx: nxt_unit_ctx_t_*): CInt = extern - def nxt_unit_run_once(ctx: nxt_unit_ctx_t_*): CInt = extern + @blocking def nxt_unit_run_once(ctx: nxt_unit_ctx_t_*): CInt = extern def nxt_unit_process_port_msg(ctx: nxt_unit_ctx_t_*, port: nxt_unit_port_t_*): CInt = extern @@ -163,7 +163,7 @@ object externs { def nxt_unit_response_add_content(req: nxt_unit_request_info_t_*, src: CString, size: Int): CInt = extern - def nxt_unit_response_send(req: nxt_unit_request_info_t_*): CInt = extern + @blocking def nxt_unit_response_send(req: nxt_unit_request_info_t_*): CInt = extern def nxt_unit_response_buf_alloc(req: nxt_unit_request_info_t_*, size: CInt): nxt_unit_buf_t_* = extern From 15fe9fcb5af33886da9ff0fd22d0d7718e53220a Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Fri, 13 Feb 2026 19:57:34 +0100 Subject: [PATCH 09/11] Cask and multithreading fixes --- .../scala-native/snunit/nxt_unit_embed.c | 123 +++++++++++------- snunit/src/snunit/unsafe/unsafe.scala | 29 +++-- 2 files changed, 93 insertions(+), 59 deletions(-) diff --git a/snunit/resources/scala-native/snunit/nxt_unit_embed.c b/snunit/resources/scala-native/snunit/nxt_unit_embed.c index b7d9bf4..2dc18e1 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_embed.c +++ b/snunit/resources/scala-native/snunit/nxt_unit_embed.c @@ -142,8 +142,7 @@ typedef struct { int quit; int ev_fd; /* epoll fd (Linux) or kqueue fd (BSD/macOS); -1 when not running */ #ifdef SCALANATIVE_MULTITHREADING_ENABLED - int handler_pipe[2]; /* [0]=read (in event loop), [1]=write (worker signals) */ - conn_t **conns; /* set in run loop so pipe handler can iterate */ + int handler_pipe[2]; /* [0]=read (in event loop), [1]=write (worker sends conn ptr) */ #endif #if EMB_USE_KQUEUE struct kevent *kq_changes; /* batched changes; applied in main loop kevent() */ @@ -422,7 +421,6 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) emb->handler_pipe[0] = -1; emb->handler_pipe[1] = -1; } - emb->conns = NULL; #endif global_emb = emb; return &emb->ctx; @@ -675,9 +673,6 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { } #endif while (!emb->quit) { -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - emb->conns = &conns; -#endif /* -1: block until an event. No spinning (0) or long wait (1000ms). */ nevents = epoll_wait(emb->ev_fd, events, EMB_EPOLL_MAXEV, -1); if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } @@ -688,23 +683,32 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { int rev = events[i].events; #ifdef SCALANATIVE_MULTITHREADING_ENABLED if (events[i].data.ptr == handler_pipe_udata) { - char buf[256]; conn_t *dc; - while (read(emb->handler_pipe[0], buf, sizeof(buf)) > 0) ; - for (dc = *emb->conns; dc != NULL; dc = dc->next) { - pthread_mutex_lock(&dc->dispatch_mutex); - if (dc->handler_done) { - conn_handler_done_cleanup(dc); - if (dc->client_closed) { - conn_unlink(dc); - dc->prev = NULL; - dc->next = pending_free; - pending_free = dc; - } else if (dc->send_len > 0) { - embed_ev_want_write(emb, dc, 1); + char *dcp = (char *) &dc; + size_t need = sizeof(dc); + ssize_t n; + while (need > 0 && (n = read(emb->handler_pipe[0], dcp, need)) > 0) { + dcp += (size_t) n; + need -= (size_t) n; + if (need == 0) { + if (dc != NULL && dc->prev != NULL) { + pthread_mutex_lock(&dc->dispatch_mutex); + if (dc->handler_done) { + conn_handler_done_cleanup(dc); + if (dc->client_closed) { + conn_unlink(dc); + dc->prev = NULL; + dc->next = pending_free; + pending_free = dc; + } else if (dc->send_len > 0) { + embed_ev_want_write(emb, dc, 1); + } + } + pthread_mutex_unlock(&dc->dispatch_mutex); } + dcp = (char *) &dc; + need = sizeof(dc); } - pthread_mutex_unlock(&dc->dispatch_mutex); } continue; } @@ -781,9 +785,6 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { #endif /* Single kevent(apply+wait) for low latency. EV_DISPATCH/EV_ONESHOT on write gives one event per enable so we don't spin. */ while (!emb->quit) { -#ifdef SCALANATIVE_MULTITHREADING_ENABLED - emb->conns = &conns; -#endif nevents = kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, events, EMB_KQUEUE_MAXEV, NULL); emb->kq_nchanges = 0; if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } @@ -796,23 +797,32 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { int err_or_hup = (flags & EV_ERROR) != 0 || (flags & EV_EOF) != 0; #ifdef SCALANATIVE_MULTITHREADING_ENABLED if (events[i].udata == handler_pipe_udata) { - char buf[256]; conn_t *dc; - while (read(emb->handler_pipe[0], buf, sizeof(buf)) > 0) ; - for (dc = *emb->conns; dc != NULL; dc = dc->next) { - pthread_mutex_lock(&dc->dispatch_mutex); - if (dc->handler_done) { - conn_handler_done_cleanup(dc); - if (dc->client_closed) { - conn_unlink(dc); - dc->prev = NULL; - dc->next = pending_free; - pending_free = dc; - } else if (dc->send_len > 0) { - embed_ev_want_write(emb, dc, 1); + char *dcp = (char *) &dc; + size_t need = sizeof(dc); + ssize_t n; + while (need > 0 && (n = read(emb->handler_pipe[0], dcp, need)) > 0) { + dcp += (size_t) n; + need -= (size_t) n; + if (need == 0) { + if (dc != NULL && dc->prev != NULL) { + pthread_mutex_lock(&dc->dispatch_mutex); + if (dc->handler_done) { + conn_handler_done_cleanup(dc); + if (dc->client_closed) { + conn_unlink(dc); + dc->prev = NULL; + dc->next = pending_free; + pending_free = dc; + } else if (dc->send_len > 0) { + embed_ev_want_write(emb, dc, 1); + } + } + pthread_mutex_unlock(&dc->dispatch_mutex); } + dcp = (char *) &dc; + need = sizeof(dc); } - pthread_mutex_unlock(&dc->dispatch_mutex); } continue; } @@ -1124,10 +1134,9 @@ int nxt_unit_response_send(nxt_unit_request_info_t *req) { c->handler_done = 1; pthread_cond_signal(&c->dispatch_cond); pthread_mutex_unlock(&c->dispatch_mutex); - /* Wake main loop so it can run conn_handler_done_cleanup (like Unit: no blocking). */ + /* Wake main loop with this conn only (avoids locking every conn on each completion). */ if (c->emb->handler_pipe[1] >= 0) { - char b = 1; - (void) write(c->emb->handler_pipe[1], &b, 1); + (void) write(c->emb->handler_pipe[1], &c, sizeof(c)); } #endif return r; @@ -1315,8 +1324,7 @@ void nxt_unit_request_done(nxt_unit_request_info_t *req, int rc) { pthread_cond_signal(&c->dispatch_cond); pthread_mutex_unlock(&c->dispatch_mutex); if (c->emb->handler_pipe[1] >= 0) { - char b = 1; - (void) write(c->emb->handler_pipe[1], &b, 1); + (void) write(c->emb->handler_pipe[1], &c, sizeof(c)); } #endif } @@ -1806,13 +1814,32 @@ static int conn_send_response(conn_t *c) { if (len + (size_t) n <= SEND_BUF_SIZE) memcpy(c->send_buf + len, status_line, (size_t) n); len += (size_t) n; - /* Add Content-Length so the client knows when the body ends (avoids curl hanging on empty body) */ - if (len + 32 <= SEND_BUF_SIZE) { - n = snprintf(c->send_buf + len, SEND_BUF_SIZE - len, "Content-Length: %zu\r\n", body_len); - if (n > 0) len += (size_t) n; + /* + * Add Content-Length only when app did not provide it or app's field is skipped + * (match NGINX Unit: nxt_http_request.c content_length_n != -1 && + * (r->resp.content_length == NULL || r->resp.content_length->skip)). + */ + { + int add_cl = 1; /* add our Content-Length unless app already sent a non-skipped one */ + for (i = 0; i < (int) r->fields_count; i++) { + nxt_unit_field_t *f = &r->fields[i]; + if (f->name_length == 14 + && strncasecmp((char *) nxt_unit_sptr_get(&f->name), "Content-Length", 14) == 0 + && !f->skip) + { + add_cl = 0; + break; + } + } + if (add_cl && len + 32 <= SEND_BUF_SIZE) { + n = snprintf(c->send_buf + len, SEND_BUF_SIZE - len, "Content-Length: %zu\r\n", body_len); + if (n > 0) len += (size_t) n; + } } for (i = 0; i < (int) r->fields_count && len < SEND_BUF_SIZE - 4; i++) { nxt_unit_field_t *f = &r->fields[i]; + if (f->skip) + continue; char *name = (char *) nxt_unit_sptr_get(&f->name); char *value = (char *) nxt_unit_sptr_get(&f->value); n = snprintf(c->send_buf + len, SEND_BUF_SIZE - len, "%.*s: %.*s\r\n", @@ -1834,6 +1861,10 @@ static int conn_send_response(conn_t *c) { #ifdef SCALANATIVE_MULTITHREADING_ENABLED /* Don't block worker on write(); main loop drains send_buf when pipe wakes. */ c->response_sent = 1; + /* Request write event immediately so response is sent on next kevent (avoids extra + * round-trip via pipe when handler runs on main thread; pipe wake still does cleanup). */ + if (c->send_len > 0) + embed_ev_want_write(c->emb, c, 1); #else n = (int) write(c->fd, c->send_buf, c->send_len); if (n > 0) { diff --git a/snunit/src/snunit/unsafe/unsafe.scala b/snunit/src/snunit/unsafe/unsafe.scala index 9a76d75..bdc3b09 100644 --- a/snunit/src/snunit/unsafe/unsafe.scala +++ b/snunit/src/snunit/unsafe/unsafe.scala @@ -145,15 +145,18 @@ object externs { * Allocate response structure capable to store limited numer of fields. * The structure may be accessed directly via req->response pointer or * filled step-by-step using functions add_field and add_content. + * + * Marked @blocking so handlers run on separate threads (e.g. ExecutionContext) + * can call these without preventing the GC from reaching safepoints. */ - def nxt_unit_response_init( + @blocking def nxt_unit_response_init( req: nxt_unit_request_info_t_*, status: CShort, max_fields_count: CInt, max_fields_size: CInt ): CInt = extern - def nxt_unit_response_add_field( + @blocking def nxt_unit_response_add_field( req: nxt_unit_request_info_t_*, name: CString, name_length: Byte, @@ -161,32 +164,32 @@ object externs { value_length: Int ): CInt = extern - def nxt_unit_response_add_content(req: nxt_unit_request_info_t_*, src: CString, size: Int): CInt = extern + @blocking def nxt_unit_response_add_content(req: nxt_unit_request_info_t_*, src: CString, size: Int): CInt = extern @blocking def nxt_unit_response_send(req: nxt_unit_request_info_t_*): CInt = extern - def nxt_unit_response_buf_alloc(req: nxt_unit_request_info_t_*, size: CInt): nxt_unit_buf_t_* = extern + @blocking def nxt_unit_response_buf_alloc(req: nxt_unit_request_info_t_*, size: CInt): nxt_unit_buf_t_* = extern - def nxt_unit_request_is_websocket_handshake(req: nxt_unit_request_info_t_*): CInt = extern + @blocking def nxt_unit_request_is_websocket_handshake(req: nxt_unit_request_info_t_*): CInt = extern - def nxt_unit_response_upgrade(req: nxt_unit_request_info_t_*): CInt = extern + @blocking def nxt_unit_response_upgrade(req: nxt_unit_request_info_t_*): CInt = extern - def nxt_unit_response_write_nb( + @blocking def nxt_unit_response_write_nb( req: nxt_unit_request_info_t_*, start: CString, size: CSize, min_size: CSize ): CSSize = extern - def nxt_unit_buf_send(buf: nxt_unit_buf_t_*): CInt = extern + @blocking def nxt_unit_buf_send(buf: nxt_unit_buf_t_*): CInt = extern - def nxt_unit_request_read(req: nxt_unit_request_info_t_*, dst: CVoidPtr, size: CSize): CSSize = extern + @blocking def nxt_unit_request_read(req: nxt_unit_request_info_t_*, dst: CVoidPtr, size: CSize): CSSize = extern - def nxt_unit_request_done(req: nxt_unit_request_info_t_*, rc: CInt): Unit = extern + @blocking def nxt_unit_request_done(req: nxt_unit_request_info_t_*, rc: CInt): Unit = extern - def nxt_unit_websocket_read(ws: nxt_unit_websocket_frame_t_*, dest: CVoidPtr, size: CSize): CSSize = extern + @blocking def nxt_unit_websocket_read(ws: nxt_unit_websocket_frame_t_*, dest: CVoidPtr, size: CSize): CSSize = extern - def nxt_unit_websocket_send( + @blocking def nxt_unit_websocket_send( req: nxt_unit_request_info_t_*, opcode: Byte, last: Byte, @@ -194,7 +197,7 @@ object externs { size: CSize ): CInt = extern - def nxt_unit_websocket_done(ws: nxt_unit_websocket_frame_t_*): Unit = extern + @blocking def nxt_unit_websocket_done(ws: nxt_unit_websocket_frame_t_*): Unit = extern def nxt_unit_log(ctx: nxt_unit_ctx_t_*, level: Int, fmt: CString): Unit = extern } From 0f22dc631114dc5218a0c0fcc7a50a622194da20 Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Sat, 14 Feb 2026 12:09:37 +0100 Subject: [PATCH 10/11] Fix multithreading path --- integration/test/src/BaseTests.scala | 73 +++- integration/test/src/utils.scala | 20 +- .../src/snunit/tests/HelloWorld.scala | 10 + .../resources/scala-native/snunit/README.md | 8 + .../scala-native/snunit/nxt_auto_config.h | 9 + .../resources/scala-native/snunit/nxt_unit.h | 318 ++++++++++++++---- .../scala-native/snunit/nxt_unit_embed.c | 233 ++++++++----- .../scala-native/snunit/nxt_unit_field.h | 18 +- .../scala-native/snunit/nxt_unit_request.h | 9 +- .../scala-native/snunit/nxt_unit_response.h | 7 +- .../scala-native/snunit/nxt_unit_sptr.h | 21 +- .../scala-native/snunit/nxt_unit_typedefs.h | 8 +- .../scala-native/snunit/nxt_unit_websocket.h | 19 +- .../snunit/nxt_websocket_header.h | 13 +- 14 files changed, 581 insertions(+), 185 deletions(-) diff --git a/integration/test/src/BaseTests.scala b/integration/test/src/BaseTests.scala index c7d95d8..3293e3e 100644 --- a/integration/test/src/BaseTests.scala +++ b/integration/test/src/BaseTests.scala @@ -1,43 +1,68 @@ package snunit.test import utest._ +import scala.concurrent.duration._ object BaseTests extends TestSuite { val tests = Tests { + test("hello-world") { - withDeployedExample("hello-world") { - locally { + val helloWorldExample = Example("hello-world") + test("hello") { + helloWorldExample.running { val result = request.get(baseUrl).text() val expectedResult = "Hello world!\n" assert(result == expectedResult) } - locally { + } + test("version") { + helloWorldExample.running { val result = request.get(uri"$baseUrl/version").text() val expectedResult = "HTTP/1.1" assert(result == expectedResult) } - locally { + } + test("target") { + helloWorldExample.running { val result = request.get(baseUrl.withPath("target", "%2F%2f%5C%5c").pathSegmentsEncoding(identity)).text() val expectedResult = "/target/%2F%2f%5C%5c" assert(result == expectedResult) } - locally { + } + test("path") { + helloWorldExample.running { val result = request.get(baseUrl.withPath("path", "foo%2Fbar%2f%5C%5c").pathSegmentsEncoding(identity)).text() val expectedResult = """/path/foo/bar/\\""" assert(result == expectedResult) } - locally { + } + test("empty") { + helloWorldExample.running { val result = request.get(uri"$baseUrl/empty").text() val expectedResult = "" assert(result == expectedResult) } - locally { + } + test("async") { + helloWorldExample.running { + /* Hit /async multiple times on same client to exercise keep-alive and handler_done pipe (no double wake). */ + val expectedResult = "Hello world!\n" + (1 to 50).foreach { _ => + val result = request.get(uri"$baseUrl/async").text() + assert(result == expectedResult) + } + } + } + test("echo") { + helloWorldExample.running { val result = request.post(uri"$baseUrl/echo").body("hello").text() val expectedResult = "hello" assert(result == expectedResult) } - locally { + } + test("headers") { + helloWorldExample.running { val responseHeaders = request .get(uri"$baseUrl/headers") .header("foo", "bar") @@ -51,6 +76,38 @@ object BaseTests extends TestSuite { assert(responseHeaders.contains(Header("bla", "bal"))) } } + test("close") { + helloWorldExample.running { + /* Reproduce wrk-style termination: connect, send GET /async, close socket without reading. + * Server sees client disconnect (FIN/RST) while async handler may still be running. */ + val host = baseUrl.host.get + val port = baseUrl.port.get + val requestBytes = + s"GET /async HTTP/1.1\r\nHost: $host:$port\r\nConnection: keep-alive\r\n\r\n".getBytes( + java.nio.charset.StandardCharsets.US_ASCII + ) + + (1 to 2).foreach { _ => + val socket = new java.net.Socket() + try { + socket.connect(new java.net.InetSocketAddress(host, port), 1000) + socket.setSoTimeout(1000) + socket.getOutputStream.write(requestBytes) + socket.getOutputStream.flush() + // Close immediately without reading response (like wrk on process exit). + socket.close() + } catch { case _: Exception => /* ignore */ } + finally + if (!socket.isClosed) + try socket.close() + catch { case _: Exception => } + } + + // Server should still be alive: a normal request must succeed. + val result = request.get(uri"$baseUrl/async").readTimeout(1.second).text() + assert(result == "Hello world!\n") + } + } } test("multiple-handlers") { withDeployedExample("multiple-handlers") { diff --git a/integration/test/src/utils.scala b/integration/test/src/utils.scala index 42b45af..4abe892 100644 --- a/integration/test/src/utils.scala +++ b/integration/test/src/utils.scala @@ -20,15 +20,21 @@ private def runMillCommand(command: String) = os .call( cwd = os.Path(sys.env("MILL_WORKSPACE_ROOT")) ) +class Example(projectName: String, crossSuffix: String = "") { + private val Vector(s"\"$_:$_:$_:$nativeBinary\"") = + runMillCommand(s"integration.tests.$projectName$crossSuffix.nativeLink").out.lines(): @unchecked + private val workspace = os.Path(sys.env("MILL_WORKSPACE_ROOT")) + + def running[T](f: => T): T = { + val process2 = os.proc(nativeBinary).spawn(cwd = workspace) + Thread.sleep(1000) + try { f } + finally { process2.close() } + } +} def withDeployedExample[T](projectName: String, crossSuffix: String = "")(f: => T): T = { - val Vector(s"\"$_:$_:$_:$nativeBinary\"") = - runMillCommand(s"integration.tests.$projectName$crossSuffix.nativeLink").out.lines(): @unchecked - val workspace = os.Path(sys.env("MILL_WORKSPACE_ROOT")) - val process2 = os.proc(nativeBinary).spawn(cwd = workspace) - Thread.sleep(1000) - try { f } - finally { process2.close() } + Example(projectName, crossSuffix).running(f) } def withDeployedExampleHttp4s(projectName: String)(f: => Unit) = { BuildInfo.http4sVersions.split(':').foreach { versions => diff --git a/integration/tests/hello-world/src/snunit/tests/HelloWorld.scala b/integration/tests/hello-world/src/snunit/tests/HelloWorld.scala index 5af5b19..590146a 100644 --- a/integration/tests/hello-world/src/snunit/tests/HelloWorld.scala +++ b/integration/tests/hello-world/src/snunit/tests/HelloWorld.scala @@ -18,6 +18,16 @@ object MyHandler extends RequestHandler { content = "Request headers", headers = req.headers ) + + case Method.GET -> "/async" => + concurrent.ExecutionContext.global.execute(() => { + req.send( + statusCode = StatusCode.OK, + content = "Hello world!\n", + headers = Headers("Content-Type" -> "text/plain") + ) + }) + case Method.GET -> path => val content = if (path.startsWith("/path")) req.path diff --git a/snunit/resources/scala-native/snunit/README.md b/snunit/resources/scala-native/snunit/README.md index 24e6075..55041d5 100644 --- a/snunit/resources/scala-native/snunit/README.md +++ b/snunit/resources/scala-native/snunit/README.md @@ -9,6 +9,14 @@ This directory contains a **minimal in-process** implementation of the NGINX Uni - `nxt_unit_typedefs.h`, `nxt_unit_sptr.h`, `nxt_unit_field.h`, `nxt_unit_request.h`, `nxt_unit_response.h`, `nxt_unit.h` - **nxt_auto_config.h**, **nxt_version.h** – Minimal stubs for the embed build (no Unit `configure`). +## Vendoring from NGINX Unit + +To refresh the Unit API headers from a local Unit tree (e.g. `/Users/lorenzo/scala/unit`), copy from `unit/src/` into this directory: + +- `nxt_unit.h`, `nxt_unit_typedefs.h`, `nxt_unit_request.h`, `nxt_unit_response.h`, `nxt_unit_field.h`, `nxt_unit_sptr.h`, `nxt_unit_websocket.h`, `nxt_websocket_header.h` + +Do **not** overwrite `nxt_auto_config.h` or `nxt_version.h` (snunit keeps minimal stubs). The embed uses `nxt_unit_init(init, host, port)` (3 args); Unit’s public API uses 1 arg and reads from `NXT_UNIT_INIT` env, so `nxt_unit.h` is adjusted for the embed. `NXT_UNIT_HASH_HOST` in `nxt_unit_field.h` is added for the embed’s Host-header parsing. + ## Build Scala Native compiles all `.c` (and `.cpp`) files under `src/main/resources/scala-native` (or `resources/scala-native` for the snunit library) and links them into the final binary. No extra build step is needed. diff --git a/snunit/resources/scala-native/snunit/nxt_auto_config.h b/snunit/resources/scala-native/snunit/nxt_auto_config.h index 82b99a8..2a6b945 100644 --- a/snunit/resources/scala-native/snunit/nxt_auto_config.h +++ b/snunit/resources/scala-native/snunit/nxt_auto_config.h @@ -21,6 +21,15 @@ #define NXT_DEBUG 0 +/* Endianness for nxt_websocket_header.h (from Unit). */ +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define NXT_HAVE_BIG_ENDIAN 1 +#define NXT_HAVE_LITTLE_ENDIAN 0 +#else +#define NXT_HAVE_BIG_ENDIAN 0 +#define NXT_HAVE_LITTLE_ENDIAN 1 +#endif + /* Branch prediction hints (from unit src/nxt_clang.h). */ #if defined(__GNUC__) || defined(__clang__) #define nxt_expect(c, x) __builtin_expect((long) (x), (c)) diff --git a/snunit/resources/scala-native/snunit/nxt_unit.h b/snunit/resources/scala-native/snunit/nxt_unit.h index 145785d..831ea96 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit.h +++ b/snunit/resources/scala-native/snunit/nxt_unit.h @@ -18,6 +18,7 @@ #ifndef _NXT_UNIT_H_INCLUDED_ #define _NXT_UNIT_H_INCLUDED_ + #include #include #include @@ -26,8 +27,7 @@ #include "nxt_auto_config.h" #include "nxt_version.h" #include "nxt_unit_typedefs.h" -#include "nxt_unit_request.h" -#include "nxt_unit_response.h" + enum { NXT_UNIT_OK = 0, @@ -46,164 +46,368 @@ enum { }; #define NXT_UNIT_INIT_ENV "NXT_UNIT_INIT" + #define NXT_UNIT_SHARED_PORT_ID ((uint16_t) 0xFFFFu) -struct nxt_unit_s { void *data; }; +/* + * Mostly opaque structure with library state. + * + * Only the user defined 'data' pointer is exposed here. The rest is unit + * implementation specific and hidden. + */ +struct nxt_unit_s { + void *data; /* User defined data. */ +}; +/* + * Thread context. + * + * First (main) context is provided 'for free'. To receive and process + * requests in other threads, one needs to allocate a new context and use it + * further in that thread. + */ struct nxt_unit_ctx_s { - void *data; - nxt_unit_t *unit; + void *data; /* User context-specific data. */ + nxt_unit_t *unit; }; +/* + * Unit port identification structure. + * + * Each port can be uniquely identified by listen process id (pid) and port id. + * This identification is required to refer the port from different process. + */ struct nxt_unit_port_id_s { - pid_t pid; - uint32_t hash; - uint16_t id; + pid_t pid; + uint32_t hash; + uint16_t id; }; +/* + * Unit provides port storage which is able to store and find the following + * data structures. + */ struct nxt_unit_port_s { - nxt_unit_port_id_t id; - int in_fd; - int out_fd; - void *data; + nxt_unit_port_id_t id; + + int in_fd; + int out_fd; + + void *data; }; + struct nxt_unit_buf_s { - char *start; - char *free; - char *end; + char *start; + char *free; + char *end; }; + struct nxt_unit_request_info_s { - nxt_unit_t *unit; - nxt_unit_ctx_t *ctx; - nxt_unit_port_t *response_port; - nxt_unit_request_t *request; - nxt_unit_buf_t *request_buf; - nxt_unit_response_t *response; - nxt_unit_buf_t *response_buf; - uint32_t response_max_fields; - nxt_unit_buf_t *content_buf; - uint64_t content_length; - int content_fd; - void *data; + nxt_unit_t *unit; + nxt_unit_ctx_t *ctx; + + nxt_unit_port_t *response_port; + + nxt_unit_request_t *request; + nxt_unit_buf_t *request_buf; + + nxt_unit_response_t *response; + nxt_unit_buf_t *response_buf; + uint32_t response_max_fields; + + nxt_unit_buf_t *content_buf; + uint64_t content_length; + int content_fd; + + void *data; }; + +/* + * Set of application-specific callbacks. The application may leave all + * optional callbacks as NULL. + */ struct nxt_unit_callbacks_s { + /* + * Process request. Unlike all other callbacks, this callback is required + * and needs to be defined by the application. + */ void (*request_handler)(nxt_unit_request_info_t *req); + void (*data_handler)(nxt_unit_request_info_t *req); + + /* Process websocket frame. */ void (*websocket_handler)(nxt_unit_websocket_frame_t *ws); + + /* Connection closed. */ void (*close_handler)(nxt_unit_request_info_t *req); + + /* Add new Unit port to communicate with process pid. Optional. */ int (*add_port)(nxt_unit_ctx_t *, nxt_unit_port_t *port); - void (*remove_port)(nxt_unit_t *, nxt_unit_ctx_t *, nxt_unit_port_t *port); + + /* Remove previously added port. Optional. */ + void (*remove_port)(nxt_unit_t *, nxt_unit_ctx_t *, + nxt_unit_port_t *port); + + /* Remove all data associated with process pid including ports. Optional. */ void (*remove_pid)(nxt_unit_t *, pid_t pid); + + /* Gracefully quit the application. Optional. */ void (*quit)(nxt_unit_ctx_t *); + + /* Shared memory release acknowledgement. Optional. */ void (*shm_ack_handler)(nxt_unit_ctx_t *); + + /* Send data and control to process pid using port id. Optional. */ ssize_t (*port_send)(nxt_unit_ctx_t *, nxt_unit_port_t *port, const void *buf, size_t buf_size, const void *oob, size_t oob_size); + + /* Receive data on port id. Optional. */ ssize_t (*port_recv)(nxt_unit_ctx_t *, nxt_unit_port_t *port, void *buf, size_t buf_size, void *oob, size_t *oob_size); + int (*ready_handler)(nxt_unit_ctx_t *); }; + struct nxt_unit_init_s { - void *data; - void *ctx_data; - int max_pending_requests; - uint32_t request_data_size; - uint32_t shm_limit; - uint32_t request_limit; - nxt_unit_callbacks_t callbacks; - nxt_unit_port_t ready_port; - uint32_t ready_stream; - nxt_unit_port_t router_port; - nxt_unit_port_t read_port; - int shared_port_fd; - int shared_queue_fd; - int log_fd; + void *data; /* Opaque pointer to user-defined data. */ + void *ctx_data; /* Opaque pointer to user-defined data. */ + int max_pending_requests; + + uint32_t request_data_size; + uint32_t shm_limit; + uint32_t request_limit; + + nxt_unit_callbacks_t callbacks; + + nxt_unit_port_t ready_port; + uint32_t ready_stream; + nxt_unit_port_t router_port; + nxt_unit_port_t read_port; + int shared_port_fd; + int shared_queue_fd; + int log_fd; }; + typedef ssize_t (*nxt_unit_read_func_t)(nxt_unit_read_info_t *read_info, void *dst, size_t size); + struct nxt_unit_read_info_s { - nxt_unit_read_func_t read; - int eof; - uint32_t buf_size; - void *data; + nxt_unit_read_func_t read; + int eof; + uint32_t buf_size; + void *data; }; -nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port); -int nxt_unit_run(nxt_unit_ctx_t *ctx); + +/* + * Initialize Unit application library with necessary callbacks and + * ready/reply port parameters, send 'READY' response to main. + * SNUnit embed: host and port are passed here (libunit uses NXT_UNIT_INIT env). + */ +nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *, const char *host, int port); + +/* + * Main function, useful in case the application does not have its own event + * loop. nxt_unit_run() starts an infinite message wait and process loop. + * + * for (;;) { + * app_lib->port_recv(...); + * nxt_unit_process_msg(...); + * } + * + * The function returns normally when a QUIT message is received from Unit. + */ +int nxt_unit_run(nxt_unit_ctx_t *); + int nxt_unit_run_ctx(nxt_unit_ctx_t *ctx); + int nxt_unit_run_shared(nxt_unit_ctx_t *ctx); + nxt_unit_request_info_t *nxt_unit_dequeue_request(nxt_unit_ctx_t *ctx); + +/* + * Receive and process one message, and invoke configured callbacks. + * + * If the application implements its own event loop, each datagram received + * from the port socket should be initially processed by unit. This function + * may invoke other application-defined callback for message processing. + */ int nxt_unit_run_once(nxt_unit_ctx_t *ctx); + int nxt_unit_process_port_msg(nxt_unit_ctx_t *ctx, nxt_unit_port_t *port); -void nxt_unit_done(nxt_unit_ctx_t *ctx); + +/* Destroy application library object. */ +void nxt_unit_done(nxt_unit_ctx_t *); + +/* + * Allocate and initialize a new execution context with a new listen port to + * process requests in another thread. + */ nxt_unit_ctx_t *nxt_unit_ctx_alloc(nxt_unit_ctx_t *, void *); + +/* Initialize port_id, calculate hash. */ void nxt_unit_port_id_init(nxt_unit_port_id_t *port_id, pid_t pid, uint16_t id); -uint16_t nxt_unit_field_hash(const char *name, size_t name_length); + +/* Calculates hash for given field name. */ +uint16_t nxt_unit_field_hash(const char* name, size_t name_length); + +/* Split host for server name and port. */ void nxt_unit_split_host(char *host_start, uint32_t host_length, char **name, uint32_t *name_length, char **port, uint32_t *port_length); + +/* Group duplicate fields for easy enumeration. */ void nxt_unit_request_group_dup_fields(nxt_unit_request_info_t *req); +/* + * Allocate response structure capable of storing a limited number of fields. + * The structure may be accessed directly via req->response pointer or + * filled step-by-step using functions add_field and add_content. + */ int nxt_unit_response_init(nxt_unit_request_info_t *req, uint16_t status, uint32_t max_fields_count, uint32_t max_fields_size); + int nxt_unit_response_realloc(nxt_unit_request_info_t *req, uint32_t max_fields_count, uint32_t max_fields_size); + int nxt_unit_response_is_init(nxt_unit_request_info_t *req); + int nxt_unit_response_add_field(nxt_unit_request_info_t *req, - const char *name, uint8_t name_length, - const char *value, uint32_t value_length); + const char* name, uint8_t name_length, + const char* value, uint32_t value_length); + int nxt_unit_response_add_content(nxt_unit_request_info_t *req, - const void *src, uint32_t size); + const void* src, uint32_t size); + +/* + * Send the prepared response to the Unit server. The Response structure is + * destroyed during this call. + */ int nxt_unit_response_send(nxt_unit_request_info_t *req); + int nxt_unit_response_is_sent(nxt_unit_request_info_t *req); + nxt_unit_buf_t *nxt_unit_response_buf_alloc(nxt_unit_request_info_t *req, uint32_t size); + int nxt_unit_request_is_websocket_handshake(nxt_unit_request_info_t *req); + int nxt_unit_response_upgrade(nxt_unit_request_info_t *req); + int nxt_unit_response_is_websocket(nxt_unit_request_info_t *req); + nxt_unit_request_info_t *nxt_unit_get_request_info_from_data(void *data); + int nxt_unit_buf_send(nxt_unit_buf_t *buf); + void nxt_unit_buf_free(nxt_unit_buf_t *buf); + nxt_unit_buf_t *nxt_unit_buf_next(nxt_unit_buf_t *buf); + uint32_t nxt_unit_buf_max(void); + uint32_t nxt_unit_buf_min(void); + int nxt_unit_response_write(nxt_unit_request_info_t *req, const void *start, size_t size); + ssize_t nxt_unit_response_write_nb(nxt_unit_request_info_t *req, const void *start, size_t size, size_t min_size); + int nxt_unit_response_write_cb(nxt_unit_request_info_t *req, nxt_unit_read_info_t *read_info); + ssize_t nxt_unit_request_read(nxt_unit_request_info_t *req, void *dst, size_t size); + ssize_t nxt_unit_request_readline_size(nxt_unit_request_info_t *req, size_t max_size); + void nxt_unit_request_done(nxt_unit_request_info_t *req, int rc); + int nxt_unit_websocket_send(nxt_unit_request_info_t *req, uint8_t opcode, uint8_t last, const void *start, size_t size); + int nxt_unit_websocket_sendv(nxt_unit_request_info_t *req, uint8_t opcode, uint8_t last, const struct iovec *iov, int iovcnt); + ssize_t nxt_unit_websocket_read(nxt_unit_websocket_frame_t *ws, void *dst, size_t size); + int nxt_unit_websocket_retain(nxt_unit_websocket_frame_t *ws); + void nxt_unit_websocket_done(nxt_unit_websocket_frame_t *ws); + void *nxt_unit_malloc(nxt_unit_ctx_t *ctx, size_t size); + void nxt_unit_free(nxt_unit_ctx_t *ctx, void *p); -#if defined __has_attribute && __has_attribute(format) +#if defined __has_attribute + +#if __has_attribute(format) + #define NXT_ATTR_FORMAT __attribute__((format(printf, 3, 4))) -#else + +#endif + +#endif + + +#if !defined(NXT_ATTR_FORMAT) + #define NXT_ATTR_FORMAT + #endif -void nxt_unit_log(nxt_unit_ctx_t *ctx, int level, const char *fmt, ...) NXT_ATTR_FORMAT; + +void nxt_unit_log(nxt_unit_ctx_t *ctx, int level, const char* fmt, ...) + NXT_ATTR_FORMAT; + void nxt_unit_req_log(nxt_unit_request_info_t *req, int level, - const char *fmt, ...) NXT_ATTR_FORMAT; + const char* fmt, ...) NXT_ATTR_FORMAT; + +#if (NXT_DEBUG) + +#define nxt_unit_debug(ctx, fmt, ARGS...) \ + nxt_unit_log(ctx, NXT_UNIT_LOG_DEBUG, fmt, ##ARGS) + +#define nxt_unit_req_debug(req, fmt, ARGS...) \ + nxt_unit_req_log(req, NXT_UNIT_LOG_DEBUG, fmt, ##ARGS) + +#else + +#define nxt_unit_debug(ctx, fmt, ARGS...) + +#define nxt_unit_req_debug(req, fmt, ARGS...) + +#endif + + +#define nxt_unit_warn(ctx, fmt, ARGS...) \ + nxt_unit_log(ctx, NXT_UNIT_LOG_WARN, fmt, ##ARGS) + +#define nxt_unit_req_warn(req, fmt, ARGS...) \ + nxt_unit_req_log(req, NXT_UNIT_LOG_WARN, fmt, ##ARGS) + +#define nxt_unit_error(ctx, fmt, ARGS...) \ + nxt_unit_log(ctx, NXT_UNIT_LOG_ERR, fmt, ##ARGS) + +#define nxt_unit_req_error(req, fmt, ARGS...) \ + nxt_unit_req_log(req, NXT_UNIT_LOG_ERR, fmt, ##ARGS) + +#define nxt_unit_alert(ctx, fmt, ARGS...) \ + nxt_unit_log(ctx, NXT_UNIT_LOG_ALERT, fmt, ##ARGS) + +#define nxt_unit_req_alert(req, fmt, ARGS...) \ + nxt_unit_req_log(req, NXT_UNIT_LOG_ALERT, fmt, ##ARGS) + #endif /* _NXT_UNIT_H_INCLUDED_ */ diff --git a/snunit/resources/scala-native/snunit/nxt_unit_embed.c b/snunit/resources/scala-native/snunit/nxt_unit_embed.c index 2dc18e1..7da219b 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_embed.c +++ b/snunit/resources/scala-native/snunit/nxt_unit_embed.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,8 @@ #endif #include "nxt_unit.h" +#include "nxt_unit_request.h" +#include "nxt_unit_response.h" #include "nxt_unit_websocket.h" /* Minimal SHA-1 for WebSocket accept key (RFC 6455). Public domain. */ @@ -299,11 +302,12 @@ static int embed_ev_add_listen(embed_ctx_t *emb) { EV_SET(kev, emb->listen_fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL); return 0; } -/* EV_CLEAR: reset after retrieval. EMB_KQ_WRITE_ONESHOT: one write event per enable so single kevent(apply+wait) doesn't spin. */ +/* Read: level-triggered (no EV_CLEAR) so data already in buffer when we register still yields an event on macOS. + * Write: EV_CLEAR + EMB_KQ_WRITE_ONESHOT so one write event per enable. */ static int embed_ev_add_conn(embed_ctx_t *emb, conn_t *c) { struct kevent *kev; kev = embed_kq_change(emb); - EV_SET(kev, c->fd, EVFILT_READ, EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, c); + EV_SET(kev, c->fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, c); kev = embed_kq_change(emb); EV_SET(kev, c->fd, EVFILT_WRITE, EV_ADD | (c->send_len > 0 ? EV_ENABLE : EV_DISABLE) | EV_CLEAR | EMB_KQ_WRITE_ONESHOT, 0, 0, c); return 0; @@ -329,11 +333,11 @@ static void embed_ev_want_write(embed_ctx_t *emb, conn_t *c, int want) { else EV_SET(kev, c->fd, EVFILT_WRITE, EV_DISABLE, 0, 0, c); } -/* Re-arm after processing (EV_CLEAR consumes one event; re-enable so we get more). */ +/* Re-arm after processing; read stays level-triggered (no EV_CLEAR). */ static void embed_ev_rearm_conn(embed_ctx_t *emb, conn_t *c) { struct kevent *kev; kev = embed_kq_change(emb); - EV_SET(kev, c->fd, EVFILT_READ, EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, c); + EV_SET(kev, c->fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, c); kev = embed_kq_change(emb); EV_SET(kev, c->fd, EVFILT_WRITE, EV_ADD | (c->send_len > 0 ? EV_ENABLE : EV_DISABLE) | EV_CLEAR | EMB_KQ_WRITE_ONESHOT, 0, 0, c); } @@ -345,6 +349,9 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) int fd, opt = 1; struct sockaddr_in sa; + /* Avoid SIGPIPE when writing to a closed client socket (e.g. wrk-style close). */ + (void) signal(SIGPIPE, SIG_IGN); + if (nxt_slow_path(init == NULL)) { fprintf(stderr, "nxt_unit_embed: init is NULL\n"); fflush(stderr); @@ -427,9 +434,8 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) } /* --- nxt_unit_run --- */ -/* Returns 1 if conn was removed (caller must not advance p), 0 otherwise. - * Removed conns are appended to *pending_free for deferred free (avoids use-after-free when - * epoll/kqueue delivers multiple events for the same fd in one batch). */ +/* Returns: 0 = conn still active (caller should rearm); 1 = conn removed and in *pending_free; + * 2 = conn removed from event set only (client_closed, handler still running; do not rearm, do not free). */ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int can_write, int err_or_hup, conn_t **pending_free) { int n; (void) emb; @@ -445,7 +451,7 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int embed_kq_flush(emb); #endif #endif - return 0; + return 2; /* removed from ev set; do not rearm (fd may be closed); free on pipe wake */ } #endif conn_unlink(c); @@ -481,6 +487,30 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int } if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) break; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Write failed (e.g. EPIPE): defer unlink/free if handler still running, same as err_or_hup and read==0. */ + if (c->handler_started && !c->handler_done) { + c->client_closed = 1; +#if EMB_USE_EPOLL || EMB_USE_KQUEUE + embed_ev_remove_conn(emb, c); +#if EMB_USE_KQUEUE + embed_kq_flush(emb); +#endif +#endif + return 2; + } + /* Handler already wrote this conn to handler_pipe; do not free here or pipe read will use-after-free. */ + if (c->handler_done) { + c->client_closed = 1; +#if EMB_USE_EPOLL || EMB_USE_KQUEUE + embed_ev_remove_conn(emb, c); +#if EMB_USE_KQUEUE + embed_kq_flush(emb); +#endif +#endif + return 2; + } +#endif conn_unlink(c); c->prev = NULL; c->next = *pending_free; @@ -511,6 +541,19 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int continue; } if (n == 0) { +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* EOF: client closed. Defer unlink/free if handler still running. */ + if (c->handler_started && !c->handler_done) { + c->client_closed = 1; +#if EMB_USE_EPOLL || EMB_USE_KQUEUE + embed_ev_remove_conn(emb, c); +#if EMB_USE_KQUEUE + embed_kq_flush(emb); +#endif +#endif + return 2; + } +#endif conn_unlink(c); c->prev = NULL; c->next = *pending_free; @@ -590,13 +633,20 @@ static int run_loop_process_conn(embed_ctx_t *emb, conn_t *c, int can_read, int } if (c->request_ready) { #ifdef SCALANATIVE_MULTITHREADING_ENABLED - /* Like NGINX Unit: invoke handler and continue; cleanup on pipe wake. */ + /* Like NGINX Unit: invoke handler and continue; cleanup on pipe wake or immediately if sync. */ if (!c->handler_started) { c->response_sent = 0; conn_dispatch_request(c); #if EMB_USE_EPOLL || EMB_USE_KQUEUE if (c->send_len > 0) embed_ev_want_write(emb, c, 1); #endif + /* If handler already finished (ran sync on this thread), send response and do cleanup now. */ + if (c->handler_done) { + if (!c->response_sent) + conn_send_response(c); + conn_handler_done_cleanup(c); + if (c->send_len > 0) embed_ev_want_write(emb, c, 1); + } return 0; } /* handler_started: cleanup when pipe is read; fall through to allow send drain */ @@ -679,10 +729,44 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { if (nxt_slow_path(nevents == 0)) continue; pending_free = NULL; + /* Process conn/listen events first so write EPIPE sets client_closed before we drain the pipe and free. */ for (i = 0; i < nevents; i++) { int rev = events[i].events; #ifdef SCALANATIVE_MULTITHREADING_ENABLED - if (events[i].data.ptr == handler_pipe_udata) { + if (events[i].data.ptr == handler_pipe_udata) continue; /* drain in second pass */ +#endif + if (nxt_slow_path(events[i].data.ptr == NULL)) { + /* listen fd */ + peer_len = sizeof(peer); + new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); + if (nxt_fast_path(new_fd >= 0)) { + fcntl(new_fd, F_SETFL, O_NONBLOCK); + setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); + c = conn_new(new_fd, emb); + if (nxt_fast_path(c != NULL)) { + conn_insert(&conns, c); + if (nxt_slow_path(embed_ev_add_conn(emb, c) != 0)) { + conn_unlink(c); + conn_free(c); + } + } else + close(new_fd); + } + continue; + } + c = (conn_t *) events[i].data.ptr; + if (nxt_slow_path(c->prev == NULL)) continue; /* already removed this batch */ + if (run_loop_process_conn(emb, c, + (rev & EPOLLIN) != 0, + (rev & EPOLLOUT) != 0, + (rev & (EPOLLERR | EPOLLHUP)) != 0, &pending_free) == 0) + embed_ev_rearm_conn(emb, c); + } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Second pass: drain handler_pipe (free client_closed conns set by write EPIPE in first pass). */ + for (i = 0; i < nevents; i++) { + if (events[i].data.ptr != handler_pipe_udata) continue; + { conn_t *dc; char *dcp = (char *) &dc; size_t need = sizeof(dc); @@ -694,6 +778,8 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { if (dc != NULL && dc->prev != NULL) { pthread_mutex_lock(&dc->dispatch_mutex); if (dc->handler_done) { + if (!dc->response_sent) + conn_send_response(dc); conn_handler_done_cleanup(dc); if (dc->client_closed) { conn_unlink(dc); @@ -710,36 +796,9 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { need = sizeof(dc); } } - continue; - } -#endif - if (nxt_slow_path(events[i].data.ptr == NULL)) { - /* listen fd */ - peer_len = sizeof(peer); - new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); - if (nxt_fast_path(new_fd >= 0)) { - fcntl(new_fd, F_SETFL, O_NONBLOCK); - setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); - c = conn_new(new_fd, emb); - if (nxt_fast_path(c != NULL)) { - conn_insert(&conns, c); - if (nxt_slow_path(embed_ev_add_conn(emb, c) != 0)) { - conn_unlink(c); - conn_free(c); - } - } else - close(new_fd); - } - continue; } - c = (conn_t *) events[i].data.ptr; - if (nxt_slow_path(c->prev == NULL)) continue; /* already removed this batch */ - if (nxt_slow_path(!run_loop_process_conn(emb, c, - (rev & EPOLLIN) != 0, - (rev & EPOLLOUT) != 0, - (rev & (EPOLLERR | EPOLLHUP)) != 0, &pending_free))) - embed_ev_rearm_conn(emb, c); } +#endif while (pending_free != NULL) { c = pending_free; pending_free = c->next; @@ -783,6 +842,8 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { EV_SET(kev, emb->handler_pipe[0], EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, handler_pipe_udata); } #endif + /* Apply initial set (listen_fd + handler_pipe) so first wait sees them; apply+wait in one kevent() can miss events on some platforms. */ + embed_kq_flush(emb); /* Single kevent(apply+wait) for low latency. EV_DISPATCH/EV_ONESHOT on write gives one event per enable so we don't spin. */ while (!emb->quit) { nevents = kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, events, EMB_KQUEUE_MAXEV, NULL); @@ -791,12 +852,50 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { if (nxt_slow_path(nevents == 0)) continue; pending_free = NULL; + /* Process conn/listen events first so write EPIPE sets client_closed before we drain the pipe and free. */ for (i = 0; i < nevents; i++) { int filter = events[i].filter; int flags = events[i].flags; int err_or_hup = (flags & EV_ERROR) != 0 || (flags & EV_EOF) != 0; #ifdef SCALANATIVE_MULTITHREADING_ENABLED - if (events[i].udata == handler_pipe_udata) { + if (events[i].udata == handler_pipe_udata) continue; /* drain in second pass */ +#endif + if (nxt_slow_path(events[i].ident == (uintptr_t) emb->listen_fd)) { + /* listen fd (match Unit: identify by fd, not udata) */ + if (nxt_fast_path(filter == EVFILT_READ && !err_or_hup)) { + peer_len = sizeof(peer); + new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); + if (nxt_fast_path(new_fd >= 0)) { + fcntl(new_fd, F_SETFL, O_NONBLOCK); + setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); + c = conn_new(new_fd, emb); + if (nxt_fast_path(c != NULL)) { + conn_insert(&conns, c); + if (nxt_slow_path(embed_ev_add_conn(emb, c) != 0)) { + conn_unlink(c); + conn_free(c); + } + } else + close(new_fd); + } + } + continue; + } + c = (conn_t *) events[i].udata; + if (nxt_slow_path(c->prev == NULL)) continue; /* already removed this batch */ + if (nxt_fast_path(filter == EVFILT_READ)) { + if (run_loop_process_conn(emb, c, 1, 0, err_or_hup, &pending_free) == 0) + embed_ev_rearm_conn(emb, c); + } else if (filter == EVFILT_WRITE) { + if (run_loop_process_conn(emb, c, 0, 1, err_or_hup, &pending_free) == 0) + embed_ev_rearm_conn(emb, c); + } + } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Second pass: drain handler_pipe (free client_closed conns set by write EPIPE in first pass). */ + for (i = 0; i < nevents; i++) { + if (events[i].udata != handler_pipe_udata) continue; + { conn_t *dc; char *dcp = (char *) &dc; size_t need = sizeof(dc); @@ -808,6 +907,8 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { if (dc != NULL && dc->prev != NULL) { pthread_mutex_lock(&dc->dispatch_mutex); if (dc->handler_done) { + if (!dc->response_sent) + conn_send_response(dc); conn_handler_done_cleanup(dc); if (dc->client_closed) { conn_unlink(dc); @@ -824,40 +925,9 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { need = sizeof(dc); } } - continue; - } -#endif - if (nxt_slow_path(events[i].udata == NULL)) { - /* listen fd */ - if (nxt_fast_path(filter == EVFILT_READ && !err_or_hup)) { - peer_len = sizeof(peer); - new_fd = accept(emb->listen_fd, (struct sockaddr *) &peer, &peer_len); - if (nxt_fast_path(new_fd >= 0)) { - fcntl(new_fd, F_SETFL, O_NONBLOCK); - setsockopt(new_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); - c = conn_new(new_fd, emb); - if (nxt_fast_path(c != NULL)) { - conn_insert(&conns, c); - if (nxt_slow_path(embed_ev_add_conn(emb, c) != 0)) { - conn_unlink(c); - conn_free(c); - } - } else - close(new_fd); - } - } - continue; - } - c = (conn_t *) events[i].udata; - if (nxt_slow_path(c->prev == NULL)) continue; /* already removed this batch */ - if (nxt_fast_path(filter == EVFILT_READ)) { - if (!run_loop_process_conn(emb, c, 1, 0, err_or_hup, &pending_free)) - embed_ev_rearm_conn(emb, c); - } else if (filter == EVFILT_WRITE) { - if (!run_loop_process_conn(emb, c, 0, 1, err_or_hup, &pending_free)) - embed_ev_rearm_conn(emb, c); } } +#endif /* Apply EV_DELETE for removed conns before closing fds; otherwise next kevent() can get EBADF and exit the loop. */ embed_kq_flush(emb); while (pending_free != NULL) { @@ -928,11 +998,14 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { c = *p; for (i = 1; i < nfds && pfds[i].fd != c->fd; i++) ; if (nxt_slow_path(i >= nfds)) { p = &c->next; continue; } - if (nxt_slow_path(!run_loop_process_conn(emb, c, - (pfds[i].revents & POLLIN) != 0, - (pfds[i].revents & POLLOUT) != 0, - (pfds[i].revents & (POLLERR | POLLHUP)) != 0, &pending_free))) - p = &c->next; + { + int pr = run_loop_process_conn(emb, c, + (pfds[i].revents & POLLIN) != 0, + (pfds[i].revents & POLLOUT) != 0, + (pfds[i].revents & (POLLERR | POLLHUP)) != 0, &pending_free); + /* Advance only when conn not removed (0) or removed from ev only (2); when 1, unlink already updated *p */ + if (pr != 1) p = &c->next; + } } while (pending_free != NULL) { c = pending_free; @@ -1134,7 +1207,8 @@ int nxt_unit_response_send(nxt_unit_request_info_t *req) { c->handler_done = 1; pthread_cond_signal(&c->dispatch_cond); pthread_mutex_unlock(&c->dispatch_mutex); - /* Wake main loop with this conn only (avoids locking every conn on each completion). */ + /* Wake main loop so it can drain send_buf and re-enable write; request_done also + * writes so we may get two pipe reads per request (second is a no-op if already cleaned). */ if (c->emb->handler_pipe[1] >= 0) { (void) write(c->emb->handler_pipe[1], &c, sizeof(c)); } @@ -1861,9 +1935,8 @@ static int conn_send_response(conn_t *c) { #ifdef SCALANATIVE_MULTITHREADING_ENABLED /* Don't block worker on write(); main loop drains send_buf when pipe wakes. */ c->response_sent = 1; - /* Request write event immediately so response is sent on next kevent (avoids extra - * round-trip via pipe when handler runs on main thread; pipe wake still does cleanup). */ - if (c->send_len > 0) + /* Request write event only if client still connected; if client_closed main loop will free on pipe drain and we must not touch kq_changes from this thread. */ + if (c->send_len > 0 && !c->client_closed) embed_ev_want_write(c->emb, c, 1); #else n = (int) write(c->fd, c->send_buf, c->send_len); diff --git a/snunit/resources/scala-native/snunit/nxt_unit_field.h b/snunit/resources/scala-native/snunit/nxt_unit_field.h index 0d81f6b..b833e5c 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_field.h +++ b/snunit/resources/scala-native/snunit/nxt_unit_field.h @@ -18,22 +18,30 @@ #ifndef _NXT_UNIT_FIELD_H_INCLUDED_ #define _NXT_UNIT_FIELD_H_INCLUDED_ + #include + #include "nxt_unit_sptr.h" -#define NXT_UNIT_HASH_CONTENT_LENGTH 0x1EA0 -#define NXT_UNIT_HASH_CONTENT_TYPE 0x5F7D -#define NXT_UNIT_HASH_COOKIE 0x23F2 -#define NXT_UNIT_HASH_HOST 0x69C8 +enum { + NXT_UNIT_HASH_CONTENT_LENGTH = 0x1EA0, + NXT_UNIT_HASH_CONTENT_TYPE = 0x5F7D, + NXT_UNIT_HASH_COOKIE = 0x23F2, + NXT_UNIT_HASH_HOST = 0xE6EB, /* "Host" (embed server_name from Host header) */ +}; + +/* Name and Value field aka HTTP header. */ struct nxt_unit_field_s { uint16_t hash; uint8_t skip:1; uint8_t hopbyhop:1; uint8_t name_length; uint32_t value_length; + nxt_unit_sptr_t name; nxt_unit_sptr_t value; }; -#endif + +#endif /* _NXT_UNIT_FIELD_H_INCLUDED_ */ diff --git a/snunit/resources/scala-native/snunit/nxt_unit_request.h b/snunit/resources/scala-native/snunit/nxt_unit_request.h index bc43475..6a256db 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_request.h +++ b/snunit/resources/scala-native/snunit/nxt_unit_request.h @@ -18,7 +18,9 @@ #ifndef _NXT_UNIT_REQUEST_H_INCLUDED_ #define _NXT_UNIT_REQUEST_H_INCLUDED_ + #include + #include "nxt_unit_sptr.h" #include "nxt_unit_field.h" @@ -38,11 +40,14 @@ struct nxt_unit_request_s { uint32_t path_length; uint32_t query_length; uint32_t fields_count; + uint32_t content_length_field; uint32_t content_type_field; uint32_t cookie_field; uint32_t authorization_field; + uint64_t content_length; + nxt_unit_sptr_t method; nxt_unit_sptr_t version; nxt_unit_sptr_t remote; @@ -53,7 +58,9 @@ struct nxt_unit_request_s { nxt_unit_sptr_t path; nxt_unit_sptr_t query; nxt_unit_sptr_t preread_content; + nxt_unit_field_t fields[]; }; -#endif + +#endif /* _NXT_UNIT_REQUEST_H_INCLUDED_ */ diff --git a/snunit/resources/scala-native/snunit/nxt_unit_response.h b/snunit/resources/scala-native/snunit/nxt_unit_response.h index fe3854b..670d09f 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_response.h +++ b/snunit/resources/scala-native/snunit/nxt_unit_response.h @@ -18,7 +18,9 @@ #ifndef _NXT_UNIT_RESPONSE_H_INCLUDED_ #define _NXT_UNIT_RESPONSE_H_INCLUDED_ + #include + #include "nxt_unit_sptr.h" #include "nxt_unit_field.h" @@ -27,8 +29,11 @@ struct nxt_unit_response_s { uint32_t fields_count; uint32_t piggyback_content_length; uint16_t status; + nxt_unit_sptr_t piggyback_content; + nxt_unit_field_t fields[]; }; -#endif + +#endif /* _NXT_UNIT_RESPONSE_H_INCLUDED_ */ diff --git a/snunit/resources/scala-native/snunit/nxt_unit_sptr.h b/snunit/resources/scala-native/snunit/nxt_unit_sptr.h index a5936c3..378b7b7 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_sptr.h +++ b/snunit/resources/scala-native/snunit/nxt_unit_sptr.h @@ -18,24 +18,33 @@ #ifndef _NXT_UNIT_SPTR_H_INCLUDED_ #define _NXT_UNIT_SPTR_H_INCLUDED_ + #include #include #include #include "nxt_unit_typedefs.h" + +/* Serialized pointer. */ union nxt_unit_sptr_u { uint8_t base[1]; uint32_t offset; }; -static inline void nxt_unit_sptr_set(nxt_unit_sptr_t *sptr, void *ptr) { - sptr->offset = (uint32_t) ((uint8_t *) ptr - (uint8_t *) sptr); + +static inline void +nxt_unit_sptr_set(nxt_unit_sptr_t *sptr, void *ptr) +{ + sptr->offset = (uint8_t *) ptr - sptr->base; } -/* Offset is from the address of the sptr itself (matches Scala: sptr + !sptr). */ -static inline void *nxt_unit_sptr_get(nxt_unit_sptr_t *sptr) { - return (uint8_t *) sptr + sptr->offset; + +static inline void * +nxt_unit_sptr_get(nxt_unit_sptr_t *sptr) +{ + return sptr->base + sptr->offset; } -#endif + +#endif /* _NXT_UNIT_SPTR_H_INCLUDED_ */ diff --git a/snunit/resources/scala-native/snunit/nxt_unit_typedefs.h b/snunit/resources/scala-native/snunit/nxt_unit_typedefs.h index a412a21..e704002 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_typedefs.h +++ b/snunit/resources/scala-native/snunit/nxt_unit_typedefs.h @@ -18,10 +18,11 @@ #ifndef _NXT_UNIT_TYPEDEFS_H_INCLUDED_ #define _NXT_UNIT_TYPEDEFS_H_INCLUDED_ + typedef struct nxt_unit_s nxt_unit_t; typedef struct nxt_unit_ctx_s nxt_unit_ctx_t; typedef struct nxt_unit_port_id_s nxt_unit_port_id_t; -typedef struct nxt_unit_port_s nxt_unit_port_t; +typedef struct nxt_unit_port_s nxt_unit_port_t; typedef struct nxt_unit_buf_s nxt_unit_buf_t; typedef struct nxt_unit_request_info_s nxt_unit_request_info_t; typedef struct nxt_unit_callbacks_s nxt_unit_callbacks_t; @@ -30,7 +31,8 @@ typedef union nxt_unit_sptr_u nxt_unit_sptr_t; typedef struct nxt_unit_field_s nxt_unit_field_t; typedef struct nxt_unit_request_s nxt_unit_request_t; typedef struct nxt_unit_response_s nxt_unit_response_t; -typedef struct nxt_unit_read_info_s nxt_unit_read_info_t; +typedef struct nxt_unit_read_info_s nxt_unit_read_info_t; typedef struct nxt_unit_websocket_frame_s nxt_unit_websocket_frame_t; -#endif + +#endif /* _NXT_UNIT_TYPEDEFS_H_INCLUDED_ */ diff --git a/snunit/resources/scala-native/snunit/nxt_unit_websocket.h b/snunit/resources/scala-native/snunit/nxt_unit_websocket.h index 353ee01..87ea619 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_websocket.h +++ b/snunit/resources/scala-native/snunit/nxt_unit_websocket.h @@ -18,16 +18,21 @@ #define _NXT_UNIT_WEBSOCKET_H_INCLUDED_ #include + #include "nxt_unit_typedefs.h" #include "nxt_websocket_header.h" + struct nxt_unit_websocket_frame_s { - nxt_unit_request_info_t *req; - uint64_t payload_len; - nxt_websocket_header_t *header; - uint8_t *mask; - nxt_unit_buf_t *content_buf; - uint64_t content_length; + nxt_unit_request_info_t *req; + + uint64_t payload_len; + nxt_websocket_header_t *header; + uint8_t *mask; + + nxt_unit_buf_t *content_buf; + uint64_t content_length; }; -#endif + +#endif /* _NXT_UNIT_WEBSOCKET_H_INCLUDED_ */ diff --git a/snunit/resources/scala-native/snunit/nxt_websocket_header.h b/snunit/resources/scala-native/snunit/nxt_websocket_header.h index 8b3c5d1..5bfd433 100644 --- a/snunit/resources/scala-native/snunit/nxt_websocket_header.h +++ b/snunit/resources/scala-native/snunit/nxt_websocket_header.h @@ -17,15 +17,8 @@ #ifndef _NXT_WEBSOCKET_HEADER_H_INCLUDED_ #define _NXT_WEBSOCKET_HEADER_H_INCLUDED_ -#include +#include -#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -#define NXT_HAVE_BIG_ENDIAN 1 -#define NXT_HAVE_LITTLE_ENDIAN 0 -#else -#define NXT_HAVE_BIG_ENDIAN 0 -#define NXT_HAVE_LITTLE_ENDIAN 1 -#endif typedef struct { #if (NXT_HAVE_BIG_ENDIAN) @@ -77,8 +70,8 @@ enum { NXT_WEBSOCKET_CR_INVALID_DATA = 1007, NXT_WEBSOCKET_CR_POLICY_VIOLATION = 1008, NXT_WEBSOCKET_CR_MESSAGE_TOO_BIG = 1009, - NXT_WEBSOCKET_CR_EXTENSION_REQUIRED = 1010, - NXT_WEBSOCKET_CR_INTERNAL_SERVER_ERROR = 1011, + NXT_WEBSOCKET_CR_EXTENSION_REQUIRED = 1010, + NXT_WEBSOCKET_CR_INTERNAL_SERVER_ERROR = 1011, NXT_WEBSOCKET_CR_TLS_HANDSHAKE_FAILED = 1015, }; From b717bd2b65db658266717bb0501888e9c22d3388 Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Sat, 14 Feb 2026 19:49:22 +0100 Subject: [PATCH 11/11] Fix more hangs --- integration/test/src/BaseTests.scala | 34 +- .../scala-native/snunit/nxt_unit_embed.c | 308 ++++++++++++------ 2 files changed, 238 insertions(+), 104 deletions(-) diff --git a/integration/test/src/BaseTests.scala b/integration/test/src/BaseTests.scala index 3293e3e..a7325e5 100644 --- a/integration/test/src/BaseTests.scala +++ b/integration/test/src/BaseTests.scala @@ -79,7 +79,8 @@ object BaseTests extends TestSuite { test("close") { helloWorldExample.running { /* Reproduce wrk-style termination: connect, send GET /async, close socket without reading. - * Server sees client disconnect (FIN/RST) while async handler may still be running. */ + * Server sees client disconnect (FIN/RST) while async handler may still be running. + * Use many concurrent connections (like wrk -d 1) so the server gets a burst of closes. */ val host = baseUrl.host.get val port = baseUrl.port.get val requestBytes = @@ -87,10 +88,10 @@ object BaseTests extends TestSuite { java.nio.charset.StandardCharsets.US_ASCII ) - (1 to 2).foreach { _ => + def connectSendAndClose(): Unit = { val socket = new java.net.Socket() try { - socket.connect(new java.net.InetSocketAddress(host, port), 1000) + socket.connect(new java.net.InetSocketAddress(host, port), 5000) socket.setSoTimeout(1000) socket.getOutputStream.write(requestBytes) socket.getOutputStream.flush() @@ -103,8 +104,31 @@ object BaseTests extends TestSuite { catch { case _: Exception => } } - // Server should still be alive: a normal request must succeed. - val result = request.get(uri"$baseUrl/async").readTimeout(1.second).text() + // Many concurrent connections that all close without reading (like wrk exiting after -d 1). + val n = 15 + val threads = (1 to n).map(_ => + new Thread(() => connectSendAndClose(), "close-test-client") + ) + threads.foreach(_.start()) + threads.foreach(_.join()) + + // Give async handlers time to complete and server to drain handler_pipe. + Thread.sleep(500) + + // Server must still be alive: a normal request must succeed. + val result = request.get(uri"$baseUrl/async").readTimeout(5.seconds).text() + assert(result == "Hello world!\n") + } + } + test("closeAfterWrk") { + /* Reproduce exact user scenario: run real wrk -d 1, then server must still answer. + * This test fails when the server gets stuck after wrk exits (closes all connections). + * Requires: port 8080 free (kill any stuck server first), wrk installed. */ + helloWorldExample.running { + /* Run wrk; ignore exit code (e.g. connection refused if server slow to start). */ + os.proc("wrk", "-d", "1", "-t", "2", "-c", "10", s"http://localhost:8080/async").call(check = false) + Thread.sleep(3000) /* let async handlers complete and server drain handler_pipe */ + val result = request.get(uri"$baseUrl/async").readTimeout(10.seconds).text() assert(result == "Hello world!\n") } } diff --git a/snunit/resources/scala-native/snunit/nxt_unit_embed.c b/snunit/resources/scala-native/snunit/nxt_unit_embed.c index 7da219b..d19a532 100644 --- a/snunit/resources/scala-native/snunit/nxt_unit_embed.c +++ b/snunit/resources/scala-native/snunit/nxt_unit_embed.c @@ -139,13 +139,14 @@ typedef struct { nxt_unit_t unit; nxt_unit_ctx_t ctx; nxt_unit_init_t *init; + nxt_unit_init_t init_copy; /* owned copy – immune to Scala GC relocation */ int listen_fd; int port; char listen_addr[64]; int quit; int ev_fd; /* epoll fd (Linux) or kqueue fd (BSD/macOS); -1 when not running */ #ifdef SCALANATIVE_MULTITHREADING_ENABLED - int handler_pipe[2]; /* [0]=read (in event loop), [1]=write (worker sends conn ptr) */ + int handler_pipe[2]; /* [0]=read (main loop via kqueue/epoll/poll), [1]=write (worker sends conn ptr) */ #endif #if EMB_USE_KQUEUE struct kevent *kq_changes; /* batched changes; applied in main loop kevent() */ @@ -229,6 +230,9 @@ static int conn_parse_websocket_frames(embed_ctx_t *emb, conn_t *c); static uint16_t field_hash(const char *name, size_t len); #ifdef SCALANATIVE_MULTITHREADING_ENABLED static void conn_handler_done_cleanup(conn_t *c); +static void drain_handler_pipe(embed_ctx_t *emb, conn_t **pending_free); +/* Scan all connections for handler_done (fallback when pipe wake is lost). */ +static void scan_handler_done(embed_ctx_t *emb, conn_t **conns, conn_t **pending_free); #endif #if EMB_USE_EPOLL @@ -375,7 +379,11 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) emb->unit.data = init->data; emb->ctx.data = init->ctx_data; emb->ctx.unit = &emb->unit; - emb->init = init; + /* Copy the init struct so we own the callbacks: the Scala-side init is backed + * by a GC-managed Array[Byte] and may be relocated by the collector, making + * the original pointer dangling. After this, emb->init points to our copy. */ + memcpy(&emb->init_copy, init, sizeof(nxt_unit_init_t)); + emb->init = &emb->init_copy; strncpy(emb->listen_addr, host, sizeof(emb->listen_addr) - 1); emb->listen_addr[sizeof(emb->listen_addr) - 1] = '\0'; @@ -423,7 +431,9 @@ nxt_unit_ctx_t *nxt_unit_init(nxt_unit_init_t *init, const char *host, int port) emb->handler_pipe[0] = -1; emb->handler_pipe[1] = -1; if (pipe(emb->handler_pipe) == 0) { + /* Read end: non-blocking for kqueue/epoll/poll integration. */ fcntl(emb->handler_pipe[0], F_SETFL, O_NONBLOCK); + /* Write end stays blocking so worker threads never silently lose pipe writes. */ } else { emb->handler_pipe[0] = -1; emb->handler_pipe[1] = -1; @@ -714,18 +724,32 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { return NXT_UNIT_ERROR; } #ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Register handler_pipe read end in epoll so main loop wakes when workers complete. */ if (emb->handler_pipe[0] >= 0) { - struct epoll_event ev; - memset(&ev, 0, sizeof(ev)); - ev.events = EPOLLIN; - ev.data.ptr = handler_pipe_udata; - epoll_ctl(emb->ev_fd, EPOLL_CTL_ADD, emb->handler_pipe[0], &ev); + struct epoll_event hev; + memset(&hev, 0, sizeof(hev)); + hev.events = EPOLLIN; + hev.data.ptr = handler_pipe_udata; + epoll_ctl(emb->ev_fd, EPOLL_CTL_ADD, emb->handler_pipe[0], &hev); } #endif while (!emb->quit) { - /* -1: block until an event. No spinning (0) or long wait (1000ms). */ + pending_free = NULL; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + drain_handler_pipe(emb, &pending_free); +#endif + while (pending_free != NULL) { + c = pending_free; + pending_free = c->next; + conn_free(c); + } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* 100ms timeout: poll handler_done even if pipe wake was lost. */ + nevents = epoll_wait(emb->ev_fd, events, EMB_EPOLL_MAXEV, 100); +#else nevents = epoll_wait(emb->ev_fd, events, EMB_EPOLL_MAXEV, -1); - if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } +#endif + if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; continue; } if (nxt_slow_path(nevents == 0)) continue; pending_free = NULL; @@ -733,7 +757,10 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { for (i = 0; i < nevents; i++) { int rev = events[i].events; #ifdef SCALANATIVE_MULTITHREADING_ENABLED - if (events[i].data.ptr == handler_pipe_udata) continue; /* drain in second pass */ + if (events[i].data.ptr == handler_pipe_udata) { + drain_handler_pipe(emb, &pending_free); + continue; + } #endif if (nxt_slow_path(events[i].data.ptr == NULL)) { /* listen fd */ @@ -763,41 +790,8 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { embed_ev_rearm_conn(emb, c); } #ifdef SCALANATIVE_MULTITHREADING_ENABLED - /* Second pass: drain handler_pipe (free client_closed conns set by write EPIPE in first pass). */ - for (i = 0; i < nevents; i++) { - if (events[i].data.ptr != handler_pipe_udata) continue; - { - conn_t *dc; - char *dcp = (char *) &dc; - size_t need = sizeof(dc); - ssize_t n; - while (need > 0 && (n = read(emb->handler_pipe[0], dcp, need)) > 0) { - dcp += (size_t) n; - need -= (size_t) n; - if (need == 0) { - if (dc != NULL && dc->prev != NULL) { - pthread_mutex_lock(&dc->dispatch_mutex); - if (dc->handler_done) { - if (!dc->response_sent) - conn_send_response(dc); - conn_handler_done_cleanup(dc); - if (dc->client_closed) { - conn_unlink(dc); - dc->prev = NULL; - dc->next = pending_free; - pending_free = dc; - } else if (dc->send_len > 0) { - embed_ev_want_write(emb, dc, 1); - } - } - pthread_mutex_unlock(&dc->dispatch_mutex); - } - dcp = (char *) &dc; - need = sizeof(dc); - } - } - } - } + drain_handler_pipe(emb, &pending_free); + scan_handler_done(emb, &conns, &pending_free); #endif while (pending_free != NULL) { c = pending_free; @@ -806,8 +800,8 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { } } #ifdef SCALANATIVE_MULTITHREADING_ENABLED - if (emb->handler_pipe[0] >= 0) { close(emb->handler_pipe[0]); emb->handler_pipe[0] = -1; } if (emb->handler_pipe[1] >= 0) { close(emb->handler_pipe[1]); emb->handler_pipe[1] = -1; } + if (emb->handler_pipe[0] >= 0) { close(emb->handler_pipe[0]); emb->handler_pipe[0] = -1; } #endif close(emb->ev_fd); emb->ev_fd = -1; @@ -837,18 +831,41 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { return NXT_UNIT_ERROR; } #ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Register handler_pipe read end in kqueue so main loop wakes when workers complete. */ if (emb->handler_pipe[0] >= 0) { struct kevent *kev = embed_kq_change(emb); EV_SET(kev, emb->handler_pipe[0], EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, handler_pipe_udata); } #endif - /* Apply initial set (listen_fd + handler_pipe) so first wait sees them; apply+wait in one kevent() can miss events on some platforms. */ embed_kq_flush(emb); - /* Single kevent(apply+wait) for low latency. EV_DISPATCH/EV_ONESHOT on write gives one event per enable so we don't spin. */ while (!emb->quit) { - nevents = kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, events, EMB_KQUEUE_MAXEV, NULL); + pending_free = NULL; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + drain_handler_pipe(emb, &pending_free); +#endif + while (pending_free != NULL) { + c = pending_free; + pending_free = c->next; + conn_free(c); + } + { +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* 100 ms timeout: poll handler_done even if pipe wake was lost. */ + struct timespec kq_ts = { 0, 100000000 }; + nevents = kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, + events, EMB_KQUEUE_MAXEV, &kq_ts); +#else + nevents = kevent(emb->ev_fd, emb->kq_changes, emb->kq_nchanges, + events, EMB_KQUEUE_MAXEV, NULL); +#endif + } emb->kq_nchanges = 0; - if (nxt_slow_path(nevents < 0)) { if (errno == EINTR) continue; break; } + if (nxt_slow_path(nevents < 0)) { + if (errno == EINTR) continue; + /* Transient kevent error (e.g. EBADF from stale change): + * discard pending changes and retry instead of exiting. */ + continue; + } if (nxt_slow_path(nevents == 0)) continue; pending_free = NULL; @@ -858,7 +875,10 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { int flags = events[i].flags; int err_or_hup = (flags & EV_ERROR) != 0 || (flags & EV_EOF) != 0; #ifdef SCALANATIVE_MULTITHREADING_ENABLED - if (events[i].udata == handler_pipe_udata) continue; /* drain in second pass */ + if (events[i].udata == handler_pipe_udata) { + drain_handler_pipe(emb, &pending_free); + continue; + } #endif if (nxt_slow_path(events[i].ident == (uintptr_t) emb->listen_fd)) { /* listen fd (match Unit: identify by fd, not udata) */ @@ -892,41 +912,9 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { } } #ifdef SCALANATIVE_MULTITHREADING_ENABLED - /* Second pass: drain handler_pipe (free client_closed conns set by write EPIPE in first pass). */ - for (i = 0; i < nevents; i++) { - if (events[i].udata != handler_pipe_udata) continue; - { - conn_t *dc; - char *dcp = (char *) &dc; - size_t need = sizeof(dc); - ssize_t n; - while (need > 0 && (n = read(emb->handler_pipe[0], dcp, need)) > 0) { - dcp += (size_t) n; - need -= (size_t) n; - if (need == 0) { - if (dc != NULL && dc->prev != NULL) { - pthread_mutex_lock(&dc->dispatch_mutex); - if (dc->handler_done) { - if (!dc->response_sent) - conn_send_response(dc); - conn_handler_done_cleanup(dc); - if (dc->client_closed) { - conn_unlink(dc); - dc->prev = NULL; - dc->next = pending_free; - pending_free = dc; - } else if (dc->send_len > 0) { - embed_ev_want_write(emb, dc, 1); - } - } - pthread_mutex_unlock(&dc->dispatch_mutex); - } - dcp = (char *) &dc; - need = sizeof(dc); - } - } - } - } + drain_handler_pipe(emb, &pending_free); + /* Fallback scan: catch handler completions whose pipe signal was lost. */ + scan_handler_done(emb, &conns, &pending_free); #endif /* Apply EV_DELETE for removed conns before closing fds; otherwise next kevent() can get EBADF and exit the loop. */ embed_kq_flush(emb); @@ -937,8 +925,8 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { } } #ifdef SCALANATIVE_MULTITHREADING_ENABLED - if (emb->handler_pipe[0] >= 0) { close(emb->handler_pipe[0]); emb->handler_pipe[0] = -1; } if (emb->handler_pipe[1] >= 0) { close(emb->handler_pipe[1]); emb->handler_pipe[1] = -1; } + if (emb->handler_pipe[0] >= 0) { close(emb->handler_pipe[0]); emb->handler_pipe[0] = -1; } #endif free(emb->kq_changes); emb->kq_changes = NULL; @@ -949,6 +937,10 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { { struct pollfd *pfds; int nfds, cap, i; + int first_conn_pfd; /* first pollfd index used for conns (1 or 2 if handler_pipe at 1) */ +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + int handler_pipe_pfd; /* pollfd index for handler_pipe[0]; -1 if not used */ +#endif conn_t *pending_free; cap = 64; @@ -956,11 +948,35 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { if (nxt_slow_path(pfds == NULL)) return NXT_UNIT_ERROR; while (!emb->quit) { + pending_free = NULL; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + drain_handler_pipe(emb, &pending_free); +#endif + while (pending_free != NULL) { + c = pending_free; + pending_free = c->next; + conn_free(c); + } nfds = 0; pfds[nfds].fd = emb->listen_fd; pfds[nfds].events = POLLIN; nfds++; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + /* Register handler_pipe read end in poll so main loop wakes when workers complete. */ + handler_pipe_pfd = -1; + if (emb->handler_pipe[0] >= 0) { + handler_pipe_pfd = nfds; + pfds[nfds].fd = emb->handler_pipe[0]; + pfds[nfds].events = POLLIN; + nfds++; + } +#endif + first_conn_pfd = nfds; for (c = conns; c != NULL; c = c->next) { +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + if (c->client_closed) + continue; /* wait for handler_done; pipe wake will unlink and free */ +#endif if (nxt_slow_path(nfds >= cap)) { cap *= 2; struct pollfd *np = (struct pollfd *) realloc(pfds, (size_t) cap * sizeof(struct pollfd)); @@ -973,9 +989,13 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { nfds++; } - /* -1: block until an event. No spinning (0) or long wait (1000ms). */ + /* Block until event; 100ms timeout under MT so we can poll handler_done. */ +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + n = poll(pfds, (nfds_t) nfds, 100); +#else n = poll(pfds, (nfds_t) nfds, -1); - if (nxt_slow_path(n < 0)) { if (errno == EINTR) continue; break; } +#endif + if (nxt_slow_path(n < 0)) { if (errno == EINTR) continue; continue; } if (nxt_slow_path(n == 0)) continue; if (nxt_fast_path(pfds[0].revents & POLLIN)) { @@ -992,11 +1012,15 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { } } - pending_free = NULL; +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + if (handler_pipe_pfd >= 0 && (pfds[handler_pipe_pfd].revents & POLLIN)) + drain_handler_pipe(emb, &pending_free); +#endif + p = &conns; while (*p != NULL) { c = *p; - for (i = 1; i < nfds && pfds[i].fd != c->fd; i++) ; + for (i = first_conn_pfd; i < nfds && pfds[i].fd != c->fd; i++) ; if (nxt_slow_path(i >= nfds)) { p = &c->next; continue; } { int pr = run_loop_process_conn(emb, c, @@ -1007,6 +1031,9 @@ int nxt_unit_run(nxt_unit_ctx_t *ctx) { if (pr != 1) p = &c->next; } } +#ifdef SCALANATIVE_MULTITHREADING_ENABLED + scan_handler_done(emb, &conns, &pending_free); +#endif while (pending_free != NULL) { c = pending_free; pending_free = c->next; @@ -1207,10 +1234,12 @@ int nxt_unit_response_send(nxt_unit_request_info_t *req) { c->handler_done = 1; pthread_cond_signal(&c->dispatch_cond); pthread_mutex_unlock(&c->dispatch_mutex); - /* Wake main loop so it can drain send_buf and re-enable write; request_done also - * writes so we may get two pipe reads per request (second is a no-op if already cleaned). */ + /* Wake main loop so it can drain send_buf and re-enable write. + * nxt_unit_request_done is now idempotent, so a subsequent call is a no-op. */ if (c->emb->handler_pipe[1] >= 0) { - (void) write(c->emb->handler_pipe[1], &c, sizeof(c)); + ssize_t w; + do { w = write(c->emb->handler_pipe[1], &c, sizeof(c)); } + while (w < 0 && errno == EINTR); } #endif return r; @@ -1394,11 +1423,19 @@ void nxt_unit_request_done(nxt_unit_request_info_t *req, int rc) { if (nxt_fast_path(c != NULL)) { #ifdef SCALANATIVE_MULTITHREADING_ENABLED pthread_mutex_lock(&c->dispatch_mutex); + if (c->handler_done) { + /* Already signalled (e.g. by nxt_unit_response_send for Array variant). + * Skip duplicate pipe write to avoid double-cleanup race. */ + pthread_mutex_unlock(&c->dispatch_mutex); + return; + } c->handler_done = 1; pthread_cond_signal(&c->dispatch_cond); pthread_mutex_unlock(&c->dispatch_mutex); if (c->emb->handler_pipe[1] >= 0) { - (void) write(c->emb->handler_pipe[1], &c, sizeof(c)); + ssize_t w; + do { w = write(c->emb->handler_pipe[1], &c, sizeof(c)); } + while (w < 0 && errno == EINTR); } #endif } @@ -1845,6 +1882,79 @@ static void conn_handler_done_cleanup(conn_t *c) { c->response = NULL; } } +/* Drain handler_pipe (non-blocking); append client_closed conns to *pending_free. Call before block so we never block with data already in pipe. */ +static void drain_handler_pipe(embed_ctx_t *emb, conn_t **pending_free) { + conn_t *dc; + char *dcp = (char *) &dc; + size_t need = sizeof(dc); + ssize_t n; + if (emb->handler_pipe[0] < 0) return; + for (;;) { + while (need > 0) { + n = read(emb->handler_pipe[0], dcp, need); + if (n > 0) { + dcp += (size_t) n; + need -= (size_t) n; + if (need == 0) { + if (dc != NULL && dc->prev != NULL) { + pthread_mutex_lock(&dc->dispatch_mutex); + if (dc->handler_done) { + if (!dc->response_sent) + conn_send_response(dc); + conn_handler_done_cleanup(dc); + if (dc->client_closed) { + conn_unlink(dc); + dc->prev = NULL; + dc->next = *pending_free; + *pending_free = dc; + } else { +#if EMB_USE_EPOLL || EMB_USE_KQUEUE + /* Re-arm for reads (next request on keep-alive) and writes (drain send_buf). */ + embed_ev_rearm_conn(emb, dc); +#endif + } + } + pthread_mutex_unlock(&dc->dispatch_mutex); + } + dcp = (char *) &dc; + need = sizeof(dc); + } + continue; + } + break; /* n <= 0: EAGAIN, EOF, or error */ + } + if (need > 0) + break; /* partial read or no data */ + } +} +/* Scan all connections for handler_done flags that may have been missed by the pipe + * (e.g. due to a transient kevent error that discarded the pipe event, or a + * signal-interrupted pipe write). Called periodically as a fallback. */ +static void scan_handler_done(embed_ctx_t *emb, conn_t **conns, conn_t **pending_free) { + conn_t *c, *next; + for (c = *conns; c != NULL; c = next) { + next = c->next; + if (!c->handler_started) + continue; + pthread_mutex_lock(&c->dispatch_mutex); + if (c->handler_done) { + if (!c->response_sent) + conn_send_response(c); + conn_handler_done_cleanup(c); + if (c->client_closed) { + conn_unlink(c); + c->prev = NULL; + c->next = *pending_free; + *pending_free = c; + } else { +#if EMB_USE_EPOLL || EMB_USE_KQUEUE + embed_ev_rearm_conn(emb, c); +#endif + } + } + pthread_mutex_unlock(&c->dispatch_mutex); + } +} #endif static int conn_dispatch_request(conn_t *c) { @@ -1933,11 +2043,11 @@ static int conn_send_response(conn_t *c) { c->send_len = len; send: #ifdef SCALANATIVE_MULTITHREADING_ENABLED - /* Don't block worker on write(); main loop drains send_buf when pipe wakes. */ + /* Don't block worker on write(); main loop drains send_buf when pipe wakes. + * Do NOT call embed_ev_want_write here: we are on a worker thread and + * kqueue/epoll state is not thread-safe. The main loop enables write + * events when it processes the handler_done event from handler_pipe. */ c->response_sent = 1; - /* Request write event only if client still connected; if client_closed main loop will free on pipe drain and we must not touch kq_changes from this thread. */ - if (c->send_len > 0 && !c->client_closed) - embed_ev_want_write(c->emb, c, 1); #else n = (int) write(c->fd, c->send_buf, c->send_len); if (n > 0) {