diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-31 15:59:41 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-31 15:59:41 +0100 |
| commit | 4d030871c004c917458b5e1c296edcfc689a2e66 (patch) | |
| tree | c6c057e47a17d8b03ea28a611ff029d950acfa8c | |
| parent | 440cfeee71829308fa1cc9b5aef63beb7ab756dc (diff) | |
Make tcp worker multithreaded, too
This shamelessly steals the model established in the UDP workers, i.e.
if the kernel is able to bind multiple listening sockets to the same
endpoint, we will share them across threads. If it can't, all threads
will accept on the same socket.
The fast path handles most of the connections, only the rare case of
no data yet on the socket is offloaded to the slow path (which used to
be the mainloop) for completion. This scales nicely, as the higher the
load is, the higher chances are that an accept happens only after the
network roundtrip to the peer completes, leading to more connections
that already have data waiting for us.
We also consolidate the workstruct init and deinit code, preparation
for later unifying all thread state structs for all protocols (mca,
udc and tcp).
The config file gains a per tcp connection worker count and some docs
explaining what this knob buys and costs, nudging the user to either
keep it 0 or dial it up to at least 2.
| -rw-r--r-- | man4/opentracker.conf.4 | 16 | ||||
| -rw-r--r-- | opentracker.c | 64 | ||||
| -rw-r--r-- | opentracker.conf.sample | 7 | ||||
| -rw-r--r-- | ot_fdstate.c | 9 | ||||
| -rw-r--r-- | ot_http.c | 38 | ||||
| -rw-r--r-- | ot_http.h | 7 | ||||
| -rw-r--r-- | ot_tcp.c | 83 | ||||
| -rw-r--r-- | ot_tcp.h | 6 | ||||
| -rw-r--r-- | ot_udp.c | 21 | ||||
| -rw-r--r-- | tests/integration/test_tcpworkers.py | 67 | ||||
| -rw-r--r-- | trackerlogic.c | 29 | ||||
| -rw-r--r-- | trackerlogic.h | 6 |
12 files changed, 294 insertions, 59 deletions
diff --git a/man4/opentracker.conf.4 b/man4/opentracker.conf.4 index aa82e1d..1bd1c7a 100644 --- a/man4/opentracker.conf.4 +++ b/man4/opentracker.conf.4 | |||
| @@ -42,6 +42,14 @@ in the thread that also serves TCP. See | |||
| 42 | .Sx TUNING | 42 | .Sx TUNING |
| 43 | before raising it. | 43 | before raising it. |
| 44 | .Pp | 44 | .Pp |
| 45 | .It listen.tcp.workers Ar threads | ||
| 46 | Specifies how many threads accept and answer HTTP connections, for every | ||
| 47 | listen statement that does not carry a | ||
| 48 | .Cm workers | ||
| 49 | option of its own. Defaults to 0, which serves them from the event loop. See | ||
| 50 | .Sx TUNING | ||
| 51 | before raising it. | ||
| 52 | .Pp | ||
| 45 | .It listen.shared Ar boolean | 53 | .It listen.shared Ar boolean |
| 46 | Permits another process to bind the same address, for every listen statement | 54 | Permits another process to bind the same address, for every listen statement |
| 47 | that does not carry a | 55 | that does not carry a |
| @@ -101,6 +109,14 @@ group only where the answer is yes. | |||
| 101 | in the source distribution sweeps the setting against a running tracker. | 109 | in the source distribution sweeps the setting against a running tracker. |
| 102 | Measure on the machine that will carry the traffic. | 110 | Measure on the machine that will carry the traffic. |
| 103 | .Pp | 111 | .Pp |
| 112 | A TCP worker accepts a connection and answers it on its own stack, without | ||
| 113 | the event loop seeing it at all; only a connection it cannot finish is handed | ||
| 114 | over. One worker serves less than none, because accepting leaves the event | ||
| 115 | loop's thread without anything gained in return, so the setting is worth | ||
| 116 | either 0 or at least 2. | ||
| 117 | .Pa tests/http_load.c | ||
| 118 | in the source distribution drives the tracker over HTTP to measure it. | ||
| 119 | .Pp | ||
| 104 | On FreeBSD the | 120 | On FreeBSD the |
| 105 | .Xr accf_data 9 | 121 | .Xr accf_data 9 |
| 106 | filter has the kernel hold a new connection until data arrives on it, so the | 122 | filter has the kernel hold a new connection until data arrives on it, so the |
diff --git a/opentracker.c b/opentracker.c index 2eaf988..e5efff5 100644 --- a/opentracker.c +++ b/opentracker.c | |||
| @@ -109,20 +109,9 @@ static void *server_mainloop(void *args) { | |||
| 109 | 109 | ||
| 110 | (void)args; | 110 | (void)args; |
| 111 | 111 | ||
| 112 | /* Initialize our "thread local storage" */ | 112 | if (workstruct_init(&ws) == -1) |
| 113 | memset(&ws, 0, sizeof(ws)); | ||
| 114 | stats_counters_register(&ws); | ||
| 115 | ws.inbuf = malloc(G_INBUF_SIZE); | ||
| 116 | ws.outbuf = malloc(G_OUTBUF_SIZE); | ||
| 117 | #ifdef _DEBUG_HTTPERROR | ||
| 118 | ws.debugbuf = malloc(G_DEBUGBUF_SIZE); | ||
| 119 | #endif | ||
| 120 | |||
| 121 | if (!ws.inbuf || !ws.outbuf) | ||
| 122 | panic("Initializing worker failed"); | 113 | panic("Initializing worker failed"); |
| 123 | 114 | ||
| 124 | random_bytes(ws.rand48_state, sizeof(ws.rand48_state)); | ||
| 125 | |||
| 126 | while (g_opentracker_running) { | 115 | while (g_opentracker_running) { |
| 127 | ot_event ready[OT_EVENT_BATCH]; | 116 | ot_event ready[OT_EVENT_BATCH]; |
| 128 | int64_t sock; | 117 | int64_t sock; |
| @@ -173,13 +162,7 @@ static void *server_mainloop(void *args) { | |||
| 173 | 162 | ||
| 174 | /* This work struct is about to go out of scope, so hand its tally over | 163 | /* This work struct is about to go out of scope, so hand its tally over |
| 175 | before anything can read it there */ | 164 | before anything can read it there */ |
| 176 | stats_counters_retire(&ws); | 165 | workstruct_deinit(&ws); |
| 177 | |||
| 178 | free(ws.inbuf); | ||
| 179 | free(ws.outbuf); | ||
| 180 | #ifdef _DEBUG_HTTPERROR | ||
| 181 | free(ws.debugbuf); | ||
| 182 | #endif | ||
| 183 | return 0; | 166 | return 0; |
| 184 | } | 167 | } |
| 185 | 168 | ||
| @@ -188,7 +171,7 @@ static void *server_mainloop(void *args) { | |||
| 188 | member asked for it before binding, the first one included, so whether | 171 | member asked for it before binding, the first one included, so whether |
| 189 | this entry will have workers has to be known here. */ | 172 | this entry will have workers has to be known here. */ |
| 190 | static socket_sharing listen_sharing_for(const ot_listen *entry) { | 173 | static socket_sharing listen_sharing_for(const ot_listen *entry) { |
| 191 | if (entry->proto == FLAG_UDP && entry->workers > 1 && socket_can_balance()) | 174 | if (entry->proto != FLAG_MCA && entry->workers > 1 && socket_can_balance()) |
| 192 | return SOCKET_BALANCED; | 175 | return SOCKET_BALANCED; |
| 193 | return entry->shared ? SOCKET_SHARED : SOCKET_EXCLUSIVE; | 176 | return entry->shared ? SOCKET_SHARED : SOCKET_EXCLUSIVE; |
| 194 | } | 177 | } |
| @@ -244,10 +227,41 @@ static void listen_bind_all(void) { | |||
| 244 | listen_bind_one(entries + i); | 227 | listen_bind_one(entries + i); |
| 245 | } | 228 | } |
| 246 | 229 | ||
| 247 | /* The socket the event loop was using becomes the first worker's. Where the | 230 | /* The socket the event loop was using is passed to the first worker. |
| 248 | kernel spreads datagrams, every further worker binds one of its own, so | 231 | If start up tests show that the kernel can spread connections over |
| 249 | none of them share a receive queue. | 232 | a group bound to one address, every worker binds one of its own. |
| 250 | All others read from the first */ | 233 | If not, all workers call accept on the first. */ |
| 234 | static void listen_start_tcp_workers(void) { | ||
| 235 | size_t count, i; | ||
| 236 | ot_listen *entries = config_listen_entries(&count); | ||
| 237 | |||
| 238 | for (i = 0; i < count; ++i) { | ||
| 239 | ot_listen *entry = entries + i; | ||
| 240 | int balance = listen_sharing_for(entry) == SOCKET_BALANCED; | ||
| 241 | int worker; | ||
| 242 | |||
| 243 | if (entry->proto != FLAG_TCP || entry->workers <= 0) | ||
| 244 | continue; | ||
| 245 | |||
| 246 | event_del((int)entry->sock); | ||
| 247 | tcp_worker_install(entry->sock); | ||
| 248 | |||
| 249 | for (worker = 1; worker < entry->workers; ++worker) { | ||
| 250 | int64_t sock = entry->sock; | ||
| 251 | |||
| 252 | if (balance) { | ||
| 253 | sock = socket_tcp_listen(entry->ip, entry->port, SOCKET_BALANCED); | ||
| 254 | if (sock == -1) { | ||
| 255 | fprintf(stderr, "Warning: could not bind another socket for tcp worker %d: %s. Sharing the first instead.\n", worker, | ||
| 256 | strerror(errno)); | ||
| 257 | sock = entry->sock; | ||
| 258 | } | ||
| 259 | } | ||
| 260 | tcp_worker_install(sock); | ||
| 261 | } | ||
| 262 | } | ||
| 263 | } | ||
| 264 | |||
| 251 | static void listen_start_udp_workers(void) { | 265 | static void listen_start_udp_workers(void) { |
| 252 | size_t count, i; | 266 | size_t count, i; |
| 253 | ot_listen *entries = config_listen_entries(&count); | 267 | ot_listen *entries = config_listen_entries(&count); |
| @@ -408,9 +422,11 @@ int main(int argc, char **argv) { | |||
| 408 | 422 | ||
| 409 | install_signal_handlers(); | 423 | install_signal_handlers(); |
| 410 | 424 | ||
| 425 | listen_start_tcp_workers(); | ||
| 411 | listen_start_udp_workers(); | 426 | listen_start_udp_workers(); |
| 412 | 427 | ||
| 413 | server_mainloop(0); | 428 | server_mainloop(0); |
| 429 | tcp_workers_deinit(); | ||
| 414 | udp_workers_deinit(); | 430 | udp_workers_deinit(); |
| 415 | 431 | ||
| 416 | /* No thread is serving any more, so the teardown can run where a debugger | 432 | /* No thread is serving any more, so the teardown can run where a debugger |
diff --git a/opentracker.conf.sample b/opentracker.conf.sample index 1d535e8..f075703 100644 --- a/opentracker.conf.sample +++ b/opentracker.conf.sample | |||
| @@ -21,9 +21,9 @@ | |||
| 21 | # | 21 | # |
| 22 | # A listen statement takes options after the address: | 22 | # A listen statement takes options after the address: |
| 23 | # | 23 | # |
| 24 | # workers=<count> handle this udp socket in blocking reads in that | 24 | # workers=<count> handle this socket in that many dedicated worker |
| 25 | # many dedicated worker threads instead of in the | 25 | # threads instead of in the event loop. Defaults to 1 |
| 26 | # event loop. Defaults to 1. Ignored for tcp. | 26 | # for udp and 0 for tcp. |
| 27 | # shared allow another process to bind the same address and | 27 | # shared allow another process to bind the same address and |
| 28 | # let the kernel share the traffic. Only useful if you | 28 | # let the kernel share the traffic. Only useful if you |
| 29 | # deliberately run a second tracker there. | 29 | # deliberately run a second tracker there. |
| @@ -35,6 +35,7 @@ | |||
| 35 | # anywhere. | 35 | # anywhere. |
| 36 | # | 36 | # |
| 37 | # listen.udp.workers 4 | 37 | # listen.udp.workers 4 |
| 38 | # listen.tcp.workers 4 | ||
| 38 | # listen.shared 1 | 39 | # listen.shared 1 |
| 39 | # | 40 | # |
| 40 | # Raising the worker count pays off where the kernel spreads arriving | 41 | # Raising the worker count pays off where the kernel spreads arriving |
diff --git a/ot_fdstate.c b/ot_fdstate.c index 7d742da..09daf50 100644 --- a/ot_fdstate.c +++ b/ot_fdstate.c | |||
| @@ -91,7 +91,7 @@ uint32_t fdstate_open(int fd, void *cookie, ot_time expiry) { | |||
| 91 | } | 91 | } |
| 92 | 92 | ||
| 93 | word = atomic_load_explicit(&slot->state, memory_order_relaxed); | 93 | word = atomic_load_explicit(&slot->state, memory_order_relaxed); |
| 94 | atomic_store_explicit(&slot->cookie, cookie, memory_order_relaxed); | 94 | atomic_store_explicit(&slot->cookie, cookie, memory_order_release); |
| 95 | atomic_store_explicit(&slot->expiry, expiry, memory_order_relaxed); | 95 | atomic_store_explicit(&slot->expiry, expiry, memory_order_relaxed); |
| 96 | /* Publishes the two above to whoever claims the slot next */ | 96 | /* Publishes the two above to whoever claims the slot next */ |
| 97 | atomic_store_explicit(&slot->state, FD_WORD(FD_GEN(word), FD_IDLE), memory_order_release); | 97 | atomic_store_explicit(&slot->state, FD_WORD(FD_GEN(word), FD_IDLE), memory_order_release); |
| @@ -183,14 +183,17 @@ int fdstate_next_expired(ot_time now, int *cursor) { | |||
| 183 | void *fdstate_cookie(int fd) { | 183 | void *fdstate_cookie(int fd) { |
| 184 | ot_fdslot *slot = fdstate_slot(fd); | 184 | ot_fdslot *slot = fdstate_slot(fd); |
| 185 | 185 | ||
| 186 | return slot ? atomic_load_explicit(&slot->cookie, memory_order_relaxed) : NULL; | 186 | /* Pairs with the release in fdstate_set_cookie and fdstate_open */ |
| 187 | return slot ? atomic_load_explicit(&slot->cookie, memory_order_acquire) : NULL; | ||
| 187 | } | 188 | } |
| 188 | 189 | ||
| 189 | void fdstate_set_cookie(int fd, void *cookie) { | 190 | void fdstate_set_cookie(int fd, void *cookie) { |
| 190 | ot_fdslot *slot = fdstate_slot(fd); | 191 | ot_fdslot *slot = fdstate_slot(fd); |
| 191 | 192 | ||
| 193 | /* Publishes what the cookie points at to whoever reads it next, which may | ||
| 194 | be another thread entirely */ | ||
| 192 | if (slot) | 195 | if (slot) |
| 193 | atomic_store_explicit(&slot->cookie, cookie, memory_order_relaxed); | 196 | atomic_store_explicit(&slot->cookie, cookie, memory_order_release); |
| 194 | } | 197 | } |
| 195 | 198 | ||
| 196 | void fdstate_set_expiry(int fd, ot_time expiry) { | 199 | void fdstate_set_expiry(int fd, ot_time expiry) { |
| @@ -39,22 +39,30 @@ ssize_t g_stats_path_len; | |||
| 39 | 39 | ||
| 40 | enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 }; | 40 | enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 }; |
| 41 | 41 | ||
| 42 | int http_adopt(const int64_t sock, struct http_data *cookie) { | 42 | struct http_data *http_adopt(const int64_t sock) { |
| 43 | struct http_data *heap = malloc(sizeof(*heap)); | 43 | struct http_data *cookie = fdstate_cookie(sock), *heap; |
| 44 | int interest; | ||
| 44 | 45 | ||
| 45 | if (!heap) | 46 | if (!cookie || !(cookie->flag & STRUCT_HTTP_FLAG_ONSTACK)) |
| 46 | return -1; | 47 | return cookie; |
| 48 | |||
| 49 | if (!(heap = malloc(sizeof(*heap)))) | ||
| 50 | return NULL; | ||
| 47 | 51 | ||
| 48 | *heap = *cookie; | 52 | *heap = *cookie; |
| 49 | heap->flag &= ~STRUCT_HTTP_FLAG_ONSTACK; | 53 | heap->flag &= ~STRUCT_HTTP_FLAG_ONSTACK; |
| 54 | |||
| 55 | /* Publishing the cookie is what lets another thread reach this connection, | ||
| 56 | so nothing may touch it afterwards, not even to read what it asked for */ | ||
| 57 | interest = heap->interest; | ||
| 50 | fdstate_set_cookie((int)sock, heap); | 58 | fdstate_set_cookie((int)sock, heap); |
| 51 | 59 | ||
| 52 | if (event_add((int)sock, heap->interest, fdstate_generation((int)sock)) == -1) { | 60 | if (event_add((int)sock, interest, fdstate_generation((int)sock)) == -1) { |
| 53 | fdstate_set_cookie((int)sock, cookie); | 61 | fdstate_set_cookie((int)sock, cookie); |
| 54 | free(heap); | 62 | free(heap); |
| 55 | return -1; | 63 | return NULL; |
| 56 | } | 64 | } |
| 57 | return 0; | 65 | return heap; |
| 58 | } | 66 | } |
| 59 | 67 | ||
| 60 | /* Give up a connection: the one place that knows everything hanging off it. */ | 68 | /* Give up a connection: the one place that knows everything hanging off it. */ |
| @@ -348,13 +356,17 @@ static ssize_t http_handle_stats(const int64_t sock, struct ot_workstruct *ws, c | |||
| 348 | } | 356 | } |
| 349 | #endif | 357 | #endif |
| 350 | #endif | 358 | #endif |
| 351 | /* Pass this task to the worker thread */ | ||
| 352 | cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED; | 359 | cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED; |
| 353 | 360 | ||
| 354 | /* Clients waiting for us should not easily timeout */ | 361 | /* Clients waiting for us should not easily timeout */ |
| 355 | fdstate_set_expiry(sock, 0); | 362 | fdstate_set_expiry(sock, 0); |
| 356 | fullscrape_deliver(sock, format); | ||
| 357 | http_interest_set(sock, cookie, 0); | 363 | http_interest_set(sock, cookie, 0); |
| 364 | |||
| 365 | /* Everything this connection needs has been written, so it can be given | ||
| 366 | a home of its own and only then be made reachable by a worker thread */ | ||
| 367 | if (!http_adopt(sock)) | ||
| 368 | HTTPERROR_500; | ||
| 369 | fullscrape_deliver(sock, format); | ||
| 358 | return ws->reply_size = -2; | 370 | return ws->reply_size = -2; |
| 359 | } | 371 | } |
| 360 | #endif | 372 | #endif |
| @@ -366,6 +378,9 @@ static ssize_t http_handle_stats(const int64_t sock, struct ot_workstruct *ws, c | |||
| 366 | 378 | ||
| 367 | /* Complex stats also include expensive memory debugging tools */ | 379 | /* Complex stats also include expensive memory debugging tools */ |
| 368 | fdstate_set_expiry(sock, 0); | 380 | fdstate_set_expiry(sock, 0); |
| 381 | |||
| 382 | if (!http_adopt(sock)) | ||
| 383 | HTTPERROR_500; | ||
| 369 | stats_deliver(sock, mode); | 384 | stats_deliver(sock, mode); |
| 370 | return ws->reply_size = -2; | 385 | return ws->reply_size = -2; |
| 371 | } | 386 | } |
| @@ -448,8 +463,11 @@ static ssize_t http_handle_fullscrape(const int64_t sock, struct ot_workstruct * | |||
| 448 | cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED; | 463 | cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED; |
| 449 | /* Clients waiting for us should not easily timeout */ | 464 | /* Clients waiting for us should not easily timeout */ |
| 450 | fdstate_set_expiry(sock, 0); | 465 | fdstate_set_expiry(sock, 0); |
| 451 | fullscrape_deliver(sock, format); | ||
| 452 | http_interest_set(sock, cookie, 0); | 466 | http_interest_set(sock, cookie, 0); |
| 467 | |||
| 468 | if (!http_adopt(sock)) | ||
| 469 | HTTPERROR_500; | ||
| 470 | fullscrape_deliver(sock, format); | ||
| 453 | return ws->reply_size = -2; | 471 | return ws->reply_size = -2; |
| 454 | } | 472 | } |
| 455 | #endif | 473 | #endif |
| @@ -40,9 +40,10 @@ void http_interest_set(const int64_t s, struct http_data *cookie, int | |||
| 40 | /* Close a connection and release everything that belongs to it */ | 40 | /* Close a connection and release everything that belongs to it */ |
| 41 | void http_close(const int64_t s); | 41 | void http_close(const int64_t s); |
| 42 | 42 | ||
| 43 | /* Promote a stack cookie to a heap one for persistence and handing it over | 43 | /* Promote a stack cookie to a heap one, for persistence and for handing it |
| 44 | to the event loop. */ | 44 | over to the event loop or to another thread. Does nothing to one that is |
| 45 | int http_adopt(const int64_t s, struct http_data *cookie); | 45 | already on the heap. Returns the connection's state, or NULL. */ |
| 46 | struct http_data *http_adopt(const int64_t s); | ||
| 46 | 47 | ||
| 47 | ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); | 48 | ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); |
| 48 | ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); | 49 | ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); |
| @@ -5,6 +5,10 @@ | |||
| 5 | 5 | ||
| 6 | /* System */ | 6 | /* System */ |
| 7 | #include <errno.h> | 7 | #include <errno.h> |
| 8 | #include <pthread.h> | ||
| 9 | #include <stdatomic.h> | ||
| 10 | #include <stdio.h> | ||
| 11 | #include <stdlib.h> | ||
| 8 | #include <string.h> | 12 | #include <string.h> |
| 9 | #include <unistd.h> | 13 | #include <unistd.h> |
| 10 | 14 | ||
| @@ -149,9 +153,86 @@ void tcp_accept(const int64_t serversocket, struct ot_workstruct *ws) { | |||
| 149 | if (fdstate_generation((int)sock) != generation) | 153 | if (fdstate_generation((int)sock) != generation) |
| 150 | continue; | 154 | continue; |
| 151 | 155 | ||
| 152 | if (http_adopt(sock, &cookie) == -1) | 156 | /* Handling it may already have given it a heap home and handed it to |
| 157 | another thread, and then it is no longer this frame's to touch */ | ||
| 158 | if (fdstate_cookie((int)sock) != &cookie) | ||
| 159 | fdstate_release((int)sock); | ||
| 160 | else if (!http_adopt(sock)) | ||
| 153 | http_close(sock); | 161 | http_close(sock); |
| 154 | else | 162 | else |
| 155 | fdstate_release((int)sock); | 163 | fdstate_release((int)sock); |
| 156 | } | 164 | } |
| 157 | } | 165 | } |
| 166 | |||
| 167 | /* One of these per worker, each allocated on its own so that growing the | ||
| 168 | array below never moves the one a running worker holds. */ | ||
| 169 | struct tcp_worker { | ||
| 170 | pthread_t thread; | ||
| 171 | int64_t sock; | ||
| 172 | _Atomic int left; | ||
| 173 | }; | ||
| 174 | |||
| 175 | static struct tcp_worker **g_workers; | ||
| 176 | static size_t g_worker_count; | ||
| 177 | |||
| 178 | /* A worker blocks in accept and nowhere else: the connection it is handed is | ||
| 179 | non blocking, so a client that stops talking is given to the event loop | ||
| 180 | rather than keeping this thread. */ | ||
| 181 | static void *tcp_worker(void *args) { | ||
| 182 | struct tcp_worker *me = args; | ||
| 183 | struct ot_workstruct ws; | ||
| 184 | |||
| 185 | if (workstruct_init(&ws) == -1) { | ||
| 186 | fprintf(stderr, "Warning: a tcp worker could not get its buffers and will not run.\n"); | ||
| 187 | atomic_store(&me->left, 1); | ||
| 188 | return NULL; | ||
| 189 | } | ||
| 190 | |||
| 191 | while (g_opentracker_running) | ||
| 192 | tcp_accept(me->sock, &ws); | ||
| 193 | |||
| 194 | workstruct_deinit(&ws); | ||
| 195 | atomic_store(&me->left, 1); | ||
| 196 | return NULL; | ||
| 197 | } | ||
| 198 | |||
| 199 | void tcp_worker_install(int64_t sock) { | ||
| 200 | struct tcp_worker **grown = realloc(g_workers, (g_worker_count + 1) * sizeof(*g_workers)); | ||
| 201 | struct tcp_worker *worker; | ||
| 202 | |||
| 203 | if (!grown || !(worker = calloc(1, sizeof(*worker)))) { | ||
| 204 | fprintf(stderr, "Warning: out of memory installing a tcp worker, this socket stays with the workers already running.\n"); | ||
| 205 | g_workers = grown ? grown : g_workers; | ||
| 206 | return; | ||
| 207 | } | ||
| 208 | g_workers = grown; | ||
| 209 | worker->sock = sock; | ||
| 210 | |||
| 211 | socket_set_blocking((int)sock, 1); | ||
| 212 | socket_defer_accept((int)sock); | ||
| 213 | |||
| 214 | if (pthread_create(&worker->thread, NULL, tcp_worker, worker)) | ||
| 215 | free(worker); | ||
| 216 | else | ||
| 217 | g_workers[g_worker_count++] = worker; | ||
| 218 | } | ||
| 219 | |||
| 220 | void tcp_workers_deinit(void) { | ||
| 221 | size_t i; | ||
| 222 | |||
| 223 | for (i = 0; i < g_worker_count; ++i) { | ||
| 224 | struct tcp_worker *worker = g_workers[i]; | ||
| 225 | |||
| 226 | /* Repeated because a signal only interrupts a call already under way */ | ||
| 227 | while (!atomic_load(&worker->left)) { | ||
| 228 | pthread_kill(worker->thread, OT_SIGNAL_WAKE); | ||
| 229 | usleep(1000); | ||
| 230 | } | ||
| 231 | pthread_join(worker->thread, NULL); | ||
| 232 | free(worker); | ||
| 233 | } | ||
| 234 | |||
| 235 | free(g_workers); | ||
| 236 | g_workers = NULL; | ||
| 237 | g_worker_count = 0; | ||
| 238 | } | ||
| @@ -19,4 +19,10 @@ void tcp_accept(const int64_t serversocket, struct ot_workstruct *ws); | |||
| 19 | void tcp_read(const int64_t sock, struct ot_workstruct *ws); | 19 | void tcp_read(const int64_t sock, struct ot_workstruct *ws); |
| 20 | void tcp_write(const int64_t sock); | 20 | void tcp_write(const int64_t sock); |
| 21 | 21 | ||
| 22 | /* One thread accepting on one socket. Called once per worker. */ | ||
| 23 | void tcp_worker_install(int64_t sock); | ||
| 24 | |||
| 25 | /* Stop every worker installed above and wait for it to be gone */ | ||
| 26 | void tcp_workers_deinit(void); | ||
| 27 | |||
| 22 | #endif | 28 | #endif |
| @@ -220,26 +220,17 @@ int handle_udp6(int64_t serversocket, struct ot_workstruct *ws) { | |||
| 220 | static void *udp_worker(void *args) { | 220 | static void *udp_worker(void *args) { |
| 221 | struct udp_worker *me = args; | 221 | struct udp_worker *me = args; |
| 222 | struct ot_workstruct ws; | 222 | struct ot_workstruct ws; |
| 223 | memset(&ws, 0, sizeof(ws)); | ||
| 224 | 223 | ||
| 225 | stats_counters_register(&ws); | 224 | if (workstruct_init(&ws) == -1) { |
| 226 | ws.inbuf = malloc(G_INBUF_SIZE); | 225 | fprintf(stderr, "Warning: a udp worker could not get its buffers and will not run.\n"); |
| 227 | ws.outbuf = malloc(G_OUTBUF_SIZE); | 226 | atomic_store(&me->left, 1); |
| 228 | #ifdef _DEBUG_HTTPERROR | 227 | return NULL; |
| 229 | ws.debugbuf = malloc(G_DEBUGBUF_SIZE); | 228 | } |
| 230 | #endif | ||
| 231 | 229 | ||
| 232 | while (g_opentracker_running) | 230 | while (g_opentracker_running) |
| 233 | handle_udp6(me->sock, &ws); | 231 | handle_udp6(me->sock, &ws); |
| 234 | 232 | ||
| 235 | stats_counters_retire(&ws); | 233 | workstruct_deinit(&ws); |
| 236 | |||
| 237 | free(ws.inbuf); | ||
| 238 | free(ws.outbuf); | ||
| 239 | #ifdef _DEBUG_HTTPERROR | ||
| 240 | free(ws.debugbuf); | ||
| 241 | #endif | ||
| 242 | |||
| 243 | atomic_store(&me->left, 1); | 234 | atomic_store(&me->left, 1); |
| 244 | return NULL; | 235 | return NULL; |
| 245 | } | 236 | } |
diff --git a/tests/integration/test_tcpworkers.py b/tests/integration/test_tcpworkers.py new file mode 100644 index 0000000..e43612a --- /dev/null +++ b/tests/integration/test_tcpworkers.py | |||
| @@ -0,0 +1,67 @@ | |||
| 1 | """The tracker with its tcp side handed to worker threads. | ||
| 2 | |||
| 3 | With workers configured, a connection is accepted and answered on a worker's | ||
| 4 | own stack. Anything it cannot finish there is copied to the heap and handed | ||
| 5 | to the event loop, which is the one place where a worker and the main thread | ||
| 6 | touch the same structures, so these drive all three outcomes: answered | ||
| 7 | inline, held open, and parked waiting for another thread entirely. | ||
| 8 | """ | ||
| 9 | |||
| 10 | import threading | ||
| 11 | import unittest | ||
| 12 | import urllib.parse | ||
| 13 | |||
| 14 | from harness import TrackerTestCase, bdecode | ||
| 15 | |||
| 16 | WORKERS = 4 | ||
| 17 | |||
| 18 | |||
| 19 | class TcpWorkers(TrackerTestCase): | ||
| 20 | config = "listen.tcp.workers %d\n" % WORKERS | ||
| 21 | |||
| 22 | def announce_path(self, name, port=6881): | ||
| 23 | return "/announce?info_hash=%s&peer_id=%s&port=%d&left=0" % ( | ||
| 24 | urllib.parse.quote_from_bytes(self.hash_for(name), safe=""), | ||
| 25 | "x" * 20, | ||
| 26 | port, | ||
| 27 | ) | ||
| 28 | |||
| 29 | def test_many_at_once_are_all_answered(self): | ||
| 30 | """Every worker gets something to do, and the swarm adds up.""" | ||
| 31 | info_hash = self.hash_for("workers-swarm") | ||
| 32 | errors = [] | ||
| 33 | |||
| 34 | def announce(index): | ||
| 35 | try: | ||
| 36 | self.tracker.announce(info_hash, peer_id=b"-OT0001-%012d" % index, port=7000 + index, left=0) | ||
| 37 | except Exception as problem: # noqa: BLE001 - reported, not swallowed | ||
| 38 | errors.append("%d: %r" % (index, problem)) | ||
| 39 | |||
| 40 | threads = [threading.Thread(target=announce, args=(i,)) for i in range(32)] | ||
| 41 | for thread in threads: | ||
| 42 | thread.start() | ||
| 43 | for thread in threads: | ||
| 44 | thread.join() | ||
| 45 | |||
| 46 | self.assertEqual(errors, [], "\n".join(errors)) | ||
| 47 | self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"complete"], 32) | ||
| 48 | |||
| 49 | def test_a_kept_connection_outlives_the_worker_that_took_it(self): | ||
| 50 | """Keep-alive is the plain case of a connection a worker hands on.""" | ||
| 51 | connection = self.tracker.connect() | ||
| 52 | try: | ||
| 53 | for i in range(4): | ||
| 54 | connection.send(self.announce_path("workers-kept")) | ||
| 55 | status, _, body = connection.reply() | ||
| 56 | self.assertIn(b"200", status) | ||
| 57 | self.assertIn(b"complete", bdecode(body)) | ||
| 58 | finally: | ||
| 59 | connection.close() | ||
| 60 | |||
| 61 | def test_a_full_scrape_asked_for_by_a_worker_still_arrives(self): | ||
| 62 | """The reply is built by another thread, so the worker parks the | ||
| 63 | connection waiting for nothing at all before handing it over.""" | ||
| 64 | for i in range(20): | ||
| 65 | self.tracker.announce(bytes([i]) + b"\0" * 19, port=7100 + i, left=0) | ||
| 66 | body = self.tracker.get("/scrape") | ||
| 67 | self.assertEqual(len(bdecode(body)[b"files"]), 20) | ||
diff --git a/trackerlogic.c b/trackerlogic.c index 477c4a6..fc0fa56 100644 --- a/trackerlogic.c +++ b/trackerlogic.c | |||
| @@ -567,6 +567,35 @@ void trackerlogic_init(void) { | |||
| 567 | stats_init(); | 567 | stats_init(); |
| 568 | } | 568 | } |
| 569 | 569 | ||
| 570 | int workstruct_init(struct ot_workstruct *ws) { | ||
| 571 | memset(ws, 0, sizeof(*ws)); | ||
| 572 | ws->inbuf = malloc(G_INBUF_SIZE); | ||
| 573 | ws->outbuf = malloc(G_OUTBUF_SIZE); | ||
| 574 | #ifdef _DEBUG_HTTPERROR | ||
| 575 | ws->debugbuf = malloc(G_DEBUGBUF_SIZE); | ||
| 576 | #endif | ||
| 577 | if (!ws->inbuf || !ws->outbuf) | ||
| 578 | return -1; | ||
| 579 | |||
| 580 | /* Picking which peers to hand out starts here, so two workers seeded alike | ||
| 581 | would hand out alike */ | ||
| 582 | random_bytes(ws->rand48_state, sizeof(ws->rand48_state)); | ||
| 583 | |||
| 584 | stats_counters_register(ws); | ||
| 585 | return 0; | ||
| 586 | } | ||
| 587 | |||
| 588 | void workstruct_deinit(struct ot_workstruct *ws) { | ||
| 589 | /* The tally goes to the global before this struct goes out of scope */ | ||
| 590 | stats_counters_retire(ws); | ||
| 591 | |||
| 592 | free(ws->inbuf); | ||
| 593 | free(ws->outbuf); | ||
| 594 | #ifdef _DEBUG_HTTPERROR | ||
| 595 | free(ws->debugbuf); | ||
| 596 | #endif | ||
| 597 | } | ||
| 598 | |||
| 570 | void trackerlogic_deinit(void) { | 599 | void trackerlogic_deinit(void) { |
| 571 | int delta_torrentcount = 0; | 600 | int delta_torrentcount = 0; |
| 572 | size_t bucket, j; | 601 | size_t bucket, j; |
diff --git a/trackerlogic.h b/trackerlogic.h index d269267..53edabd 100644 --- a/trackerlogic.h +++ b/trackerlogic.h | |||
| @@ -236,6 +236,12 @@ struct ot_workstruct { | |||
| 236 | int keep_alive; | 236 | int keep_alive; |
| 237 | }; | 237 | }; |
| 238 | 238 | ||
| 239 | /* Everything a thread that serves requests needs for itself. Set up and torn | ||
| 240 | down in one place so that a new kind of worker cannot miss a piece. | ||
| 241 | Returns -1 if the buffers could not be had. */ | ||
| 242 | int workstruct_init(struct ot_workstruct *ws); | ||
| 243 | void workstruct_deinit(struct ot_workstruct *ws); | ||
| 244 | |||
| 239 | /* | 245 | /* |
| 240 | Exported functions | 246 | Exported functions |
| 241 | */ | 247 | */ |
