diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-28 23:31:07 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-28 23:31:07 +0100 |
| commit | 772873104658182a5b76f5c0eed76047c62c6651 (patch) | |
| tree | 431ba9c063fab10b4356cd8c7e63c9d2e957e2d9 /tests | |
| parent | dd0ad26ea9824b75accced2b56328cbb222c4c6d (diff) | |
With more workers blockingly waiting on udp sockets, we need to keep track of all sockets to shut them down on exit
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/integration/test_shutdown.py | 58 | ||||
| -rw-r--r-- | tests/t_socket.c | 40 |
2 files changed, 98 insertions, 0 deletions
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 | |||
| 3 | The rest of the suite stops a tracker too, so a thread left behind already | ||
| 4 | shows up in teardown as the complaint that the tracker had to be killed. | ||
| 5 | These ask for the arrangements that park a thread in a blocking read in the | ||
| 6 | first place, and they time the exit, because a reader woken only by its own | ||
| 7 | receive timeout passes a correctness check while taking a second over it. | ||
| 8 | """ | ||
| 9 | |||
| 10 | import time | ||
| 11 | import unittest | ||
| 12 | |||
| 13 | from 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 | ||
| 18 | PATIENCE = 3.0 | ||
| 19 | |||
| 20 | |||
| 21 | def 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 | |||
| 37 | class 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 | |||
| 48 | class 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. */ | ||
| 152 | static 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(); |
