summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-28 23:31:07 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-28 23:31:07 +0100
commit772873104658182a5b76f5c0eed76047c62c6651 (patch)
tree431ba9c063fab10b4356cd8c7e63c9d2e957e2d9
parentdd0ad26ea9824b75accced2b56328cbb222c4c6d (diff)
With more workers blockingly waiting on udp sockets, we need to keep track of all sockets to shut them down on exit
-rw-r--r--opentracker.c9
-rw-r--r--ot_socket.c7
-rw-r--r--ot_socket.h4
-rw-r--r--ot_udp.c42
-rw-r--r--ot_udp.h3
-rw-r--r--tests/integration/test_shutdown.py58
-rw-r--r--tests/t_socket.c40
7 files changed, 156 insertions, 7 deletions
diff --git a/opentracker.c b/opentracker.c
index f579b92..ab4a934 100644
--- a/opentracker.c
+++ b/opentracker.c
@@ -397,7 +397,6 @@ static void listen_start_udp_workers(void) {
397 if (entry->proto != FLAG_UDP || entry->workers <= 0) 397 if (entry->proto != FLAG_UDP || entry->workers <= 0)
398 continue; 398 continue;
399 399
400 socket_set_blocking((int)entry->sock, 1);
401 event_del((int)entry->sock); 400 event_del((int)entry->sock);
402 udp_worker_install(entry->sock); 401 udp_worker_install(entry->sock);
403 402
@@ -409,8 +408,7 @@ static void listen_start_udp_workers(void) {
409 if (sock == -1) { 408 if (sock == -1) {
410 fprintf(stderr, "Warning: could not bind another socket for udp worker %d: %s. Sharing the first instead.\n", worker, strerror(errno)); 409 fprintf(stderr, "Warning: could not bind another socket for udp worker %d: %s. Sharing the first instead.\n", worker, strerror(errno));
411 sock = entry->sock; 410 sock = entry->sock;
412 } else 411 }
413 socket_set_blocking((int)sock, 1);
414 } 412 }
415 udp_worker_install(sock); 413 udp_worker_install(sock);
416 } 414 }
@@ -548,9 +546,10 @@ int main(int argc, char **argv) {
548 listen_start_udp_workers(); 546 listen_start_udp_workers();
549 547
550 server_mainloop(0); 548 server_mainloop(0);
549 udp_workers_deinit();
551 550
552 /* The loop has left, so no thread is serving any more and the teardown 551 /* No thread is serving any more, so the teardown can run where a debugger
553 can run where a debugger and the sanitizers can follow it */ 552 and the sanitizers can follow it */
554 trackerlogic_deinit(); 553 trackerlogic_deinit();
555#ifdef WANT_SYSLOGS 554#ifdef WANT_SYSLOGS
556 closelog(); 555 closelog();
diff --git a/ot_socket.c b/ot_socket.c
index 158459f..c73738c 100644
--- a/ot_socket.c
+++ b/ot_socket.c
@@ -9,6 +9,7 @@
9#include <netinet/in.h> 9#include <netinet/in.h>
10#include <string.h> 10#include <string.h>
11#include <sys/socket.h> 11#include <sys/socket.h>
12#include <sys/time.h>
12#include <unistd.h> 13#include <unistd.h>
13 14
14/* Opentracker */ 15/* Opentracker */
@@ -48,6 +49,12 @@ int socket_set_blocking(int s, int blocking) {
48 return fcntl(s, F_SETFL, blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK)); 49 return fcntl(s, F_SETFL, blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK));
49} 50}
50 51
52int socket_set_timeout(int s, int seconds) {
53 struct timeval patience = {seconds, 0};
54
55 return setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &patience, sizeof(patience));
56}
57
51static int set_nonblocking(int s) { return socket_set_blocking(s, 0); } 58static int set_nonblocking(int s) { return socket_set_blocking(s, 0); }
52 59
53/* Close without letting the failure we are about to report be overwritten */ 60/* Close without letting the failure we are about to report be overwritten */
diff --git a/ot_socket.h b/ot_socket.h
index f73a5c9..9340d0d 100644
--- a/ot_socket.h
+++ b/ot_socket.h
@@ -39,6 +39,10 @@ int socket_is_connected(int s);
39int socket_accept(int s, ot_ip6 ip, uint16_t *port); 39int socket_accept(int s, ot_ip6 ip, uint16_t *port);
40int socket_set_blocking(int s, int blocking); 40int socket_set_blocking(int s, int blocking);
41 41
42/* Bound how long a blocking read waits, so the thread sitting in one gets to
43 look at the running flag now and then */
44int socket_set_timeout(int s, int seconds);
45
42ssize_t socket_recv(int s, void *buf, size_t len, ot_ip6 ip, uint16_t *port); 46ssize_t socket_recv(int s, void *buf, size_t len, ot_ip6 ip, uint16_t *port);
43ssize_t socket_send(int s, const void *buf, size_t len, const ot_ip6 ip, uint16_t port); 47ssize_t socket_send(int s, const void *buf, size_t len, const ot_ip6 ip, uint16_t port);
44 48
diff --git a/ot_udp.c b/ot_udp.c
index 87741da..7aa0e61 100644
--- a/ot_udp.c
+++ b/ot_udp.c
@@ -9,6 +9,7 @@
9#include <stdio.h> 9#include <stdio.h>
10#include <stdlib.h> 10#include <stdlib.h>
11#include <string.h> 11#include <string.h>
12#include <sys/socket.h>
12 13
13/* Opentracker */ 14/* Opentracker */
14#include "ot_ip.h" 15#include "ot_ip.h"
@@ -19,6 +20,17 @@
19#include "ot_udp.h" 20#include "ot_udp.h"
20#include "trackerlogic.h" 21#include "trackerlogic.h"
21 22
23/* How long a worker may sit in a read before it looks at the running flag */
24#define OT_UDP_WORKER_PATIENCE 1
25
26struct udp_worker {
27 pthread_t thread;
28 int64_t sock;
29};
30
31static struct udp_worker *g_workers;
32static size_t g_worker_count;
33
22static uint32_t g_rijndael_round_key[44] = {0}; 34static uint32_t g_rijndael_round_key[44] = {0};
23static volatile uint32_t g_key_of_the_hour[2] = {0}; 35static volatile uint32_t g_key_of_the_hour[2] = {0};
24static volatile ot_time g_hour_of_the_key; 36static volatile ot_time g_hour_of_the_key;
@@ -236,11 +248,37 @@ void udp_init(void) {
236} 248}
237 249
238void udp_worker_install(int64_t sock) { 250void udp_worker_install(int64_t sock) {
239 pthread_t thread_id; 251 struct udp_worker *grown = realloc(g_workers, (g_worker_count + 1) * sizeof(*g_workers));
252
253 if (!grown) {
254 fprintf(stderr, "Warning: out of memory installing a udp worker, this socket stays with the workers already running.\n");
255 return;
256 }
257 g_workers = grown;
240 258
241#ifdef _DEBUG 259#ifdef _DEBUG
242 fprintf(stderr, "Installing a worker on udp socket %ld\n", (unsigned long)sock); 260 fprintf(stderr, "Installing a worker on udp socket %ld\n", (unsigned long)sock);
243#endif 261#endif
244 pthread_create(&thread_id, NULL, udp_worker, (void *)sock); 262 socket_set_blocking((int)sock, 1);
263 socket_set_timeout((int)sock, OT_UDP_WORKER_PATIENCE);
264
265 g_workers[g_worker_count].sock = sock;
266 if (!pthread_create(&g_workers[g_worker_count].thread, NULL, udp_worker, (void *)sock))
267 ++g_worker_count;
268}
269
270void udp_workers_deinit(void) {
271 size_t i;
272
273 /* The receive timeout bounds this on its own, shutting the socket down
274 cuts it short wherever the platform wakes a blocked reader */
275 for (i = 0; i < g_worker_count; ++i)
276 shutdown((int)g_workers[i].sock, SHUT_RD);
277 for (i = 0; i < g_worker_count; ++i)
278 pthread_join(g_workers[i].thread, NULL);
279
280 free(g_workers);
281 g_workers = NULL;
282 g_worker_count = 0;
245} 283}
246 284
diff --git a/ot_udp.h b/ot_udp.h
index 7d4432f..b731aca 100644
--- a/ot_udp.h
+++ b/ot_udp.h
@@ -13,6 +13,9 @@
13void udp_init(void); 13void udp_init(void);
14/* One thread reading one socket. Called once per worker. */ 14/* One thread reading one socket. Called once per worker. */
15void udp_worker_install(int64_t sock); 15void udp_worker_install(int64_t sock);
16
17/* Stop every worker installed above and wait for it to be gone */
18void udp_workers_deinit(void);
16int handle_udp6(int64_t serversocket, struct ot_workstruct *ws); 19int handle_udp6(int64_t serversocket, struct ot_workstruct *ws);
17 20
18#endif 21#endif
diff --git a/tests/integration/test_shutdown.py b/tests/integration/test_shutdown.py
new file mode 100644
index 0000000..063d779
--- /dev/null
+++ b/tests/integration/test_shutdown.py
@@ -0,0 +1,58 @@
1"""That every thread the tracker starts also ends when it is asked to stop.
2
3The rest of the suite stops a tracker too, so a thread left behind already
4shows up in teardown as the complaint that the tracker had to be killed.
5These ask for the arrangements that park a thread in a blocking read in the
6first place, and they time the exit, because a reader woken only by its own
7receive timeout passes a correctness check while taking a second over it.
8"""
9
10import time
11import unittest
12
13from harness import Tracker, TrackerTestCase
14
15# Generous enough for a platform where shutting the socket down does not wake
16# the reader and the receive timeout has to expire, tight enough that a reader
17# which is never woken at all fails instead of hanging the suite
18PATIENCE = 3.0
19
20
21def time_the_exit(extra_config):
22 tracker = Tracker(extra_config)
23 try:
24 # Serve one request of each kind first, so the threads that are about
25 # to be joined have actually run
26 tracker.announce(b"shutdown-probe\0\0\0\0\0\0")
27 sock, connection_id = tracker.udp_connect()
28 tracker.udp_announce(sock, connection_id, b"shutdown-probe\0\0\0\0\0\0")
29 sock.close()
30 started = time.time()
31 complaints = tracker.stop()
32 return time.time() - started, complaints
33 finally:
34 tracker._cleanup()
35
36
37class UdpWorkers(TrackerTestCase):
38 config = "listen.udp.workers 8\n"
39
40 def test_announce_reaches_one_of_eight_workers(self):
41 """Keeps the workers from being merely started and never used."""
42 sock, connection_id = self.tracker.udp_connect()
43 answer = self.tracker.udp_announce(sock, connection_id, self.hash_for("eight-workers"))
44 sock.close()
45 self.assertEqual(answer["seeders"], 1)
46
47
48class ShutdownEndsEveryThread(unittest.TestCase):
49 def test_eight_udp_workers_are_joined(self):
50 took, complaints = time_the_exit("listen.udp.workers 8\n")
51 self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints))
52 self.assertLess(took, PATIENCE, "stopping eight udp workers took %.1f s" % took)
53
54 def test_udp_in_the_event_loop_still_stops(self):
55 """Worker count zero leaves no thread to join, which must also work."""
56 took, complaints = time_the_exit("listen.udp.workers 0\n")
57 self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints))
58 self.assertLess(took, PATIENCE, "stopping without workers took %.1f s" % took)
diff --git a/tests/t_socket.c b/tests/t_socket.c
index 8eef7fd..ede8ae0 100644
--- a/tests/t_socket.c
+++ b/tests/t_socket.c
@@ -10,6 +10,7 @@
10#include <poll.h> 10#include <poll.h>
11#include <string.h> 11#include <string.h>
12#include <sys/socket.h> 12#include <sys/socket.h>
13#include <time.h>
13#include <unistd.h> 14#include <unistd.h>
14 15
15/* Opentracker */ 16/* Opentracker */
@@ -144,6 +145,44 @@ static void case_udp_roundtrip(void) {
144 close(s); 145 close(s);
145} 146}
146 147
148/* udp_workers_deinit relies on a worker parked in a read coming back. The
149 receive timeout is what guarantees that; shutting the socket down is what
150 makes it prompt where the platform allows it. Neither is uniform across the
151 systems opentracker runs on, so both are checked rather than assumed. */
152static void case_blocking_read_comes_back(void) {
153 char buf[32], ip[16], own_ip[16];
154 uint16_t port = 0, own_port = 0;
155 time_t started;
156 int s;
157
158 test_case("a receive timeout does not delay a datagram that is already there");
159 s = socket_udp_bind(ip_loopback, 0, SOCKET_EXCLUSIVE);
160 CHECK(s != -1);
161 CHECK_INT(socket_set_blocking(s, 1), 0);
162 CHECK_INT(socket_set_timeout(s, 1), 0);
163 bound_address(s, own_ip, &own_port);
164 CHECK_INT(socket_send(s, "opentracker", 11, own_ip, own_port), 11);
165 CHECK_INT(socket_recv(s, buf, sizeof(buf), ip, &port), 11);
166
167 test_case("and ends a read that would otherwise never return");
168 started = time(NULL);
169 CHECK_INT(socket_recv(s, buf, sizeof(buf), ip, &port), -1);
170 CHECK(errno == EAGAIN || errno == EWOULDBLOCK);
171 CHECK(time(NULL) - started <= 3);
172 close(s);
173
174 test_case("shutting the socket down ends one too");
175 s = socket_udp_bind(ip_loopback, 0, SOCKET_EXCLUSIVE);
176 CHECK(s != -1);
177 CHECK_INT(socket_set_blocking(s, 1), 0);
178 CHECK_INT(socket_set_timeout(s, 1), 0);
179 shutdown(s, SHUT_RD);
180 started = time(NULL);
181 CHECK(socket_recv(s, buf, sizeof(buf), ip, &port) <= 0);
182 CHECK(time(NULL) - started <= 3);
183 close(s);
184}
185
147/* An IPv4 peer must arrive in the v4 mapped form opentracker stores, however 186/* An IPv4 peer must arrive in the v4 mapped form opentracker stores, however
148 the kernel chose to report it. Linux fills a v4 mapped sockaddr_in6 here 187 the kernel chose to report it. Linux fills a v4 mapped sockaddr_in6 here
149 and the BSDs fill a plain sockaddr_in, so this is the case that tells the 188 and the BSDs fill a plain sockaddr_in, so this is the case that tells the
@@ -293,6 +332,7 @@ void test_socket(void) {
293 case_sharing_is_opt_in(); 332 case_sharing_is_opt_in();
294#endif 333#endif
295 case_udp_roundtrip(); 334 case_udp_roundtrip();
335 case_blocking_read_comes_back();
296 case_udp_v4mapped(); 336 case_udp_v4mapped();
297 case_tcp_roundtrip(); 337 case_tcp_roundtrip();
298 case_connect_refused(); 338 case_connect_refused();