diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/integration/test_shutdown.py | 58 | ||||
| -rw-r--r-- | tests/t_fdstate.c | 12 | ||||
| -rw-r--r-- | tests/t_socket.c | 57 | ||||
| -rw-r--r-- | tests/udp_wake_probe.c | 42 |
4 files changed, 157 insertions, 12 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_fdstate.c b/tests/t_fdstate.c index bf8724a..421f076 100644 --- a/tests/t_fdstate.c +++ b/tests/t_fdstate.c | |||
| @@ -101,6 +101,8 @@ static void case_out_of_range(void) { | |||
| 101 | #define RECYCLE_AFTER 32 | 101 | #define RECYCLE_AFTER 32 |
| 102 | #define STRESS_CLAIMS 200000 | 102 | #define STRESS_CLAIMS 200000 |
| 103 | #define STRESS_GIVEUP (STRESS_CLAIMS * 100) | 103 | #define STRESS_GIVEUP (STRESS_CLAIMS * 100) |
| 104 | /* What the run has to reach for its result to mean anything */ | ||
| 105 | #define STRESS_CYCLES (STRESS_CLAIMS / (RECYCLE_AFTER * 4)) | ||
| 104 | 106 | ||
| 105 | static _Atomic long stress_claims, stress_recycles, stress_violations, stress_attempts; | 107 | static _Atomic long stress_claims, stress_recycles, stress_violations, stress_attempts; |
| 106 | static _Atomic int stress_lives[STRESS_SLOTS]; | 108 | static _Atomic int stress_lives[STRESS_SLOTS]; |
| @@ -116,8 +118,14 @@ static _Atomic uint64_t stress_next_cookie; | |||
| 116 | #define STRESS_CONN_COOKIE(w) ((void *)(uintptr_t)((w) >> 32)) | 118 | #define STRESS_CONN_COOKIE(w) ((void *)(uintptr_t)((w) >> 32)) |
| 117 | #define STRESS_CONN_GEN(w) ((uint32_t)(w)) | 119 | #define STRESS_CONN_GEN(w) ((uint32_t)(w)) |
| 118 | 120 | ||
| 121 | /* The workers reach their claim target on their own, but the recycles that | ||
| 122 | make the run worth anything are the keeper's, and on a loaded machine the | ||
| 123 | keeper is the thread that loses. So the run goes on until both targets are | ||
| 124 | met rather than until the faster of the two is. */ | ||
| 119 | static int stress_running(void) { | 125 | static int stress_running(void) { |
| 120 | return atomic_load(&stress_claims) < STRESS_CLAIMS && atomic_load(&stress_attempts) < STRESS_GIVEUP; | 126 | if (atomic_load(&stress_attempts) >= STRESS_GIVEUP) |
| 127 | return 0; | ||
| 128 | return atomic_load(&stress_claims) < STRESS_CLAIMS || atomic_load(&stress_recycles) <= STRESS_CYCLES; | ||
| 121 | } | 129 | } |
| 122 | 130 | ||
| 123 | static void *stress_worker(void *arg) { | 131 | static void *stress_worker(void *arg) { |
| @@ -203,7 +211,7 @@ static void case_recycling_under_load(void) { | |||
| 203 | 211 | ||
| 204 | test_case("and the run actually recycled descriptors, or it proved nothing"); | 212 | test_case("and the run actually recycled descriptors, or it proved nothing"); |
| 205 | CHECK(atomic_load(&stress_claims) >= STRESS_CLAIMS); | 213 | CHECK(atomic_load(&stress_claims) >= STRESS_CLAIMS); |
| 206 | CHECK(atomic_load(&stress_recycles) > STRESS_CLAIMS / (RECYCLE_AFTER * 4)); | 214 | CHECK(atomic_load(&stress_recycles) > STRESS_CYCLES); |
| 207 | 215 | ||
| 208 | for (i = 0; i < STRESS_SLOTS; ++i) | 216 | for (i = 0; i < STRESS_SLOTS; ++i) |
| 209 | fdstate_close((int)i); | 217 | fdstate_close((int)i); |
diff --git a/tests/t_socket.c b/tests/t_socket.c index 8eef7fd..ab4b785 100644 --- a/tests/t_socket.c +++ b/tests/t_socket.c | |||
| @@ -8,13 +8,18 @@ | |||
| 8 | #include <fcntl.h> | 8 | #include <fcntl.h> |
| 9 | #include <netinet/in.h> | 9 | #include <netinet/in.h> |
| 10 | #include <poll.h> | 10 | #include <poll.h> |
| 11 | #include <pthread.h> | ||
| 12 | #include <signal.h> | ||
| 13 | #include <stdatomic.h> | ||
| 11 | #include <string.h> | 14 | #include <string.h> |
| 12 | #include <sys/socket.h> | 15 | #include <sys/socket.h> |
| 16 | #include <sys/time.h> | ||
| 13 | #include <unistd.h> | 17 | #include <unistd.h> |
| 14 | 18 | ||
| 15 | /* Opentracker */ | 19 | /* Opentracker */ |
| 16 | #include "ot_ip.h" | 20 | #include "ot_ip.h" |
| 17 | #include "ot_socket.h" | 21 | #include "ot_socket.h" |
| 22 | #include "trackerlogic.h" | ||
| 18 | #include "test.h" | 23 | #include "test.h" |
| 19 | 24 | ||
| 20 | /* These cases talk to the loopback interface rather than to a mock, because | 25 | /* These cases talk to the loopback interface rather than to a mock, because |
| @@ -144,6 +149,57 @@ static void case_udp_roundtrip(void) { | |||
| 144 | close(s); | 149 | close(s); |
| 145 | } | 150 | } |
| 146 | 151 | ||
| 152 | /* udp_workers_deinit and livesync_deinit end a read another thread is parked | ||
| 153 | in by signalling that thread. Which mechanisms wake a blocked reader | ||
| 154 | differs between kernels -- shutting the socket down does so on Linux and | ||
| 155 | FreeBSD but not on Darwin, closing it does so on Darwin but not on Linux -- | ||
| 156 | so the one the tracker relies on is checked rather than assumed. The | ||
| 157 | watchdog is the test's own: where a reader is not woken this fails with a | ||
| 158 | figure to look at instead of hanging the suite. */ | ||
| 159 | static _Atomic int reader_returned; | ||
| 160 | static int reader_socket; | ||
| 161 | |||
| 162 | static void reader_wake(int s) { (void)s; } | ||
| 163 | |||
| 164 | static void *blocked_reader(void *args) { | ||
| 165 | char buf[32], ip[16]; | ||
| 166 | uint16_t port = 0; | ||
| 167 | |||
| 168 | (void)args; | ||
| 169 | socket_recv(reader_socket, buf, sizeof(buf), ip, &port); | ||
| 170 | atomic_store(&reader_returned, 1); | ||
| 171 | return NULL; | ||
| 172 | } | ||
| 173 | |||
| 174 | static void case_a_signal_ends_a_blocking_read(void) { | ||
| 175 | struct sigaction sa; | ||
| 176 | struct timeval watchdog = {2, 0}; | ||
| 177 | pthread_t thread; | ||
| 178 | int waited; | ||
| 179 | |||
| 180 | test_case("a signal ends a read another thread is parked in"); | ||
| 181 | memset(&sa, 0, sizeof(sa)); | ||
| 182 | sa.sa_handler = reader_wake; | ||
| 183 | sa.sa_flags = 0; /* SA_RESTART would resume the read instead of ending it */ | ||
| 184 | CHECK_INT(sigaction(OT_SIGNAL_WAKE, &sa, NULL), 0); | ||
| 185 | |||
| 186 | atomic_store(&reader_returned, 0); | ||
| 187 | reader_socket = socket_udp_bind(ip_loopback, 0, SOCKET_EXCLUSIVE); | ||
| 188 | CHECK(reader_socket != -1); | ||
| 189 | CHECK_INT(socket_set_blocking(reader_socket, 1), 0); | ||
| 190 | CHECK_INT(setsockopt(reader_socket, SOL_SOCKET, SO_RCVTIMEO, &watchdog, sizeof(watchdog)), 0); | ||
| 191 | |||
| 192 | CHECK_INT(pthread_create(&thread, NULL, blocked_reader, NULL), 0); | ||
| 193 | usleep(100000); /* long enough to be sure it reached the read */ | ||
| 194 | CHECK_INT(pthread_kill(thread, OT_SIGNAL_WAKE), 0); | ||
| 195 | |||
| 196 | for (waited = 0; waited < 1000 && !atomic_load(&reader_returned); ++waited) | ||
| 197 | usleep(1000); | ||
| 198 | CHECK(waited < 1000); | ||
| 199 | pthread_join(thread, NULL); | ||
| 200 | close(reader_socket); | ||
| 201 | } | ||
| 202 | |||
| 147 | /* An IPv4 peer must arrive in the v4 mapped form opentracker stores, however | 203 | /* 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 | 204 | 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 | 205 | and the BSDs fill a plain sockaddr_in, so this is the case that tells the |
| @@ -293,6 +349,7 @@ void test_socket(void) { | |||
| 293 | case_sharing_is_opt_in(); | 349 | case_sharing_is_opt_in(); |
| 294 | #endif | 350 | #endif |
| 295 | case_udp_roundtrip(); | 351 | case_udp_roundtrip(); |
| 352 | case_a_signal_ends_a_blocking_read(); | ||
| 296 | case_udp_v4mapped(); | 353 | case_udp_v4mapped(); |
| 297 | case_tcp_roundtrip(); | 354 | case_tcp_roundtrip(); |
| 298 | case_connect_refused(); | 355 | case_connect_refused(); |
diff --git a/tests/udp_wake_probe.c b/tests/udp_wake_probe.c index 8a0f7a8..e948c34 100644 --- a/tests/udp_wake_probe.c +++ b/tests/udp_wake_probe.c | |||
| @@ -1,13 +1,17 @@ | |||
| 1 | /* Three ways to get a thread out of a blocking recvfrom, tested rather than | 1 | /* Four ways to get a thread out of a blocking recvfrom, tested rather than |
| 2 | assumed: close the socket, shut it down, or give it a receive timeout. | 2 | assumed: close the socket, shut it down, give it a receive timeout, or |
| 3 | send it a signal. | ||
| 3 | 4 | ||
| 4 | This decides how udp workers can be stopped. They sit in recvfrom and are | 5 | This decides how udp workers are stopped. The sockets of a SO_REUSEPORT |
| 5 | never joined today, and the sockets of a SO_REUSEPORT group all share one | 6 | group all share one address, so a worker cannot be singled out by sending |
| 6 | address, so a worker cannot be singled out by sending it a datagram. | 7 | it a datagram, and the two mechanisms differ in what they cost while the |
| 8 | tracker is running: a receive timeout wakes every worker on every tick | ||
| 9 | forever, a signal and a shutdown cost nothing until the one moment they | ||
| 10 | are used. | ||
| 7 | 11 | ||
| 8 | cc -O2 -pthread -o /tmp/wake tests/udp_wake_probe.c && /tmp/wake | 12 | cc -O2 -pthread -o /tmp/wake tests/udp_wake_probe.c && /tmp/wake |
| 9 | 13 | ||
| 10 | On Linux 6.1: close does not wake it, shutdown does, SO_RCVTIMEO does. */ | 14 | On Linux 6.1: close does not wake it, the other three do. */ |
| 11 | #include <errno.h> | 15 | #include <errno.h> |
| 12 | #include <netinet/in.h> | 16 | #include <netinet/in.h> |
| 13 | #include <pthread.h> | 17 | #include <pthread.h> |
| @@ -15,9 +19,12 @@ | |||
| 15 | #include <stdio.h> | 19 | #include <stdio.h> |
| 16 | #include <string.h> | 20 | #include <string.h> |
| 17 | #include <sys/socket.h> | 21 | #include <sys/socket.h> |
| 22 | #include <signal.h> | ||
| 18 | #include <sys/time.h> | 23 | #include <sys/time.h> |
| 19 | #include <unistd.h> | 24 | #include <unistd.h> |
| 20 | 25 | ||
| 26 | #define WAKE_SIGNAL SIGUSR1 | ||
| 27 | |||
| 21 | static _Atomic int returned; | 28 | static _Atomic int returned; |
| 22 | static _Atomic int saved_errno; | 29 | static _Atomic int saved_errno; |
| 23 | static int sock; | 30 | static int sock; |
| @@ -31,6 +38,11 @@ static void *reader(void *args) { | |||
| 31 | return NULL; | 38 | return NULL; |
| 32 | } | 39 | } |
| 33 | 40 | ||
| 41 | /* A handler that does nothing is all it takes: the point is the return from | ||
| 42 | recvfrom with EINTR, not anything the handler does. SA_RESTART must stay | ||
| 43 | off or the kernel resumes the call instead of reporting it. */ | ||
| 44 | static void noop(int s) { (void)s; } | ||
| 45 | |||
| 34 | static int bind_udp(int timeout_ms) { | 46 | static int bind_udp(int timeout_ms) { |
| 35 | struct sockaddr_in address; | 47 | struct sockaddr_in address; |
| 36 | int s = socket(AF_INET, SOCK_DGRAM, 0); | 48 | int s = socket(AF_INET, SOCK_DGRAM, 0); |
| @@ -50,7 +62,7 @@ static int bind_udp(int timeout_ms) { | |||
| 50 | return s; | 62 | return s; |
| 51 | } | 63 | } |
| 52 | 64 | ||
| 53 | static void try_it(const char *name, int timeout_ms, int do_close, int do_shutdown) { | 65 | static void try_it(const char *name, int timeout_ms, int do_close, int do_shutdown, int do_signal) { |
| 54 | pthread_t thread; | 66 | pthread_t thread; |
| 55 | int waited; | 67 | int waited; |
| 56 | 68 | ||
| @@ -62,6 +74,8 @@ static void try_it(const char *name, int timeout_ms, int do_close, int do_shutdo | |||
| 62 | 74 | ||
| 63 | if (do_shutdown) | 75 | if (do_shutdown) |
| 64 | shutdown(sock, SHUT_RD); | 76 | shutdown(sock, SHUT_RD); |
| 77 | if (do_signal) | ||
| 78 | pthread_kill(thread, WAKE_SIGNAL); | ||
| 65 | if (do_close) | 79 | if (do_close) |
| 66 | close(sock); | 80 | close(sock); |
| 67 | 81 | ||
| @@ -92,8 +106,16 @@ static void try_it(const char *name, int timeout_ms, int do_close, int do_shutdo | |||
| 92 | } | 106 | } |
| 93 | 107 | ||
| 94 | int main(void) { | 108 | int main(void) { |
| 95 | try_it("close() the socket", 0, 1, 0); | 109 | struct sigaction sa; |
| 96 | try_it("shutdown(SHUT_RD)", 0, 0, 1); | 110 | |
| 97 | try_it("SO_RCVTIMEO 500 ms", 500, 0, 0); | 111 | memset(&sa, 0, sizeof(sa)); |
| 112 | sa.sa_handler = noop; | ||
| 113 | sa.sa_flags = 0; | ||
| 114 | sigaction(WAKE_SIGNAL, &sa, NULL); | ||
| 115 | |||
| 116 | try_it("close() the socket", 0, 1, 0, 0); | ||
| 117 | try_it("shutdown(SHUT_RD)", 0, 0, 1, 0); | ||
| 118 | try_it("SO_RCVTIMEO 500 ms", 500, 0, 0, 0); | ||
| 119 | try_it("pthread_kill(SIGUSR1)", 0, 0, 0, 1); | ||
| 98 | return 0; | 120 | return 0; |
| 99 | } | 121 | } |
