summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-31 15:59:41 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-31 15:59:41 +0100
commit4d030871c004c917458b5e1c296edcfc689a2e66 (patch)
treec6c057e47a17d8b03ea28a611ff029d950acfa8c
parent440cfeee71829308fa1cc9b5aef63beb7ab756dc (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.416
-rw-r--r--opentracker.c64
-rw-r--r--opentracker.conf.sample7
-rw-r--r--ot_fdstate.c9
-rw-r--r--ot_http.c38
-rw-r--r--ot_http.h7
-rw-r--r--ot_tcp.c83
-rw-r--r--ot_tcp.h6
-rw-r--r--ot_udp.c21
-rw-r--r--tests/integration/test_tcpworkers.py67
-rw-r--r--trackerlogic.c29
-rw-r--r--trackerlogic.h6
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
43before raising it. 43before raising it.
44.Pp 44.Pp
45.It listen.tcp.workers Ar threads
46Specifies how many threads accept and answer HTTP connections, for every
47listen statement that does not carry a
48.Cm workers
49option of its own. Defaults to 0, which serves them from the event loop. See
50.Sx TUNING
51before raising it.
52.Pp
45.It listen.shared Ar boolean 53.It listen.shared Ar boolean
46Permits another process to bind the same address, for every listen statement 54Permits another process to bind the same address, for every listen statement
47that does not carry a 55that does not carry a
@@ -101,6 +109,14 @@ group only where the answer is yes.
101in the source distribution sweeps the setting against a running tracker. 109in the source distribution sweeps the setting against a running tracker.
102Measure on the machine that will carry the traffic. 110Measure on the machine that will carry the traffic.
103.Pp 111.Pp
112A TCP worker accepts a connection and answers it on its own stack, without
113the event loop seeing it at all; only a connection it cannot finish is handed
114over. One worker serves less than none, because accepting leaves the event
115loop's thread without anything gained in return, so the setting is worth
116either 0 or at least 2.
117.Pa tests/http_load.c
118in the source distribution drives the tracker over HTTP to measure it.
119.Pp
104On FreeBSD the 120On FreeBSD the
105.Xr accf_data 9 121.Xr accf_data 9
106filter has the kernel hold a new connection until data arrives on it, so the 122filter 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. */
190static socket_sharing listen_sharing_for(const ot_listen *entry) { 173static 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. */
234static 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
251static void listen_start_udp_workers(void) { 265static 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) {
183void *fdstate_cookie(int fd) { 183void *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
189void fdstate_set_cookie(int fd, void *cookie) { 190void 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
196void fdstate_set_expiry(int fd, ot_time expiry) { 199void fdstate_set_expiry(int fd, ot_time expiry) {
diff --git a/ot_http.c b/ot_http.c
index bb6ff24..602ff94 100644
--- a/ot_http.c
+++ b/ot_http.c
@@ -39,22 +39,30 @@ ssize_t g_stats_path_len;
39 39
40enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 }; 40enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 };
41 41
42int http_adopt(const int64_t sock, struct http_data *cookie) { 42struct 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
diff --git a/ot_http.h b/ot_http.h
index 82d645b..9cacd21 100644
--- a/ot_http.h
+++ b/ot_http.h
@@ -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 */
41void http_close(const int64_t s); 41void 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
45int http_adopt(const int64_t s, struct http_data *cookie); 45 already on the heap. Returns the connection's state, or NULL. */
46struct http_data *http_adopt(const int64_t s);
46 47
47ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); 48ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws);
48ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); 49ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial);
diff --git a/ot_tcp.c b/ot_tcp.c
index 8647a47..47697ca 100644
--- a/ot_tcp.c
+++ b/ot_tcp.c
@@ -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. */
169struct tcp_worker {
170 pthread_t thread;
171 int64_t sock;
172 _Atomic int left;
173};
174
175static struct tcp_worker **g_workers;
176static 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. */
181static 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
199void 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
220void 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}
diff --git a/ot_tcp.h b/ot_tcp.h
index b0a93f4..96b6ec1 100644
--- a/ot_tcp.h
+++ b/ot_tcp.h
@@ -19,4 +19,10 @@ void tcp_accept(const int64_t serversocket, struct ot_workstruct *ws);
19void tcp_read(const int64_t sock, struct ot_workstruct *ws); 19void tcp_read(const int64_t sock, struct ot_workstruct *ws);
20void tcp_write(const int64_t sock); 20void tcp_write(const int64_t sock);
21 21
22/* One thread accepting on one socket. Called once per worker. */
23void tcp_worker_install(int64_t sock);
24
25/* Stop every worker installed above and wait for it to be gone */
26void tcp_workers_deinit(void);
27
22#endif 28#endif
diff --git a/ot_udp.c b/ot_udp.c
index 1ec5148..00ec566 100644
--- a/ot_udp.c
+++ b/ot_udp.c
@@ -220,26 +220,17 @@ int handle_udp6(int64_t serversocket, struct ot_workstruct *ws) {
220static void *udp_worker(void *args) { 220static 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
3With workers configured, a connection is accepted and answered on a worker's
4own stack. Anything it cannot finish there is copied to the heap and handed
5to the event loop, which is the one place where a worker and the main thread
6touch the same structures, so these drive all three outcomes: answered
7inline, held open, and parked waiting for another thread entirely.
8"""
9
10import threading
11import unittest
12import urllib.parse
13
14from harness import TrackerTestCase, bdecode
15
16WORKERS = 4
17
18
19class 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
570int 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
588void 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
570void trackerlogic_deinit(void) { 599void 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. */
242int workstruct_init(struct ot_workstruct *ws);
243void workstruct_deinit(struct ot_workstruct *ws);
244
239/* 245/*
240 Exported functions 246 Exported functions
241*/ 247*/