diff options
| -rw-r--r-- | DESIGN | 99 | ||||
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | opentracker.c | 127 | ||||
| -rw-r--r-- | ot_config.c | 26 | ||||
| -rw-r--r-- | ot_event.c | 9 | ||||
| -rw-r--r-- | ot_event.h | 5 | ||||
| -rw-r--r-- | ot_fdstate.c | 7 | ||||
| -rw-r--r-- | ot_fdstate.h | 3 | ||||
| -rw-r--r-- | ot_http.c | 29 | ||||
| -rw-r--r-- | ot_http.h | 7 | ||||
| -rw-r--r-- | ot_tcp.c | 157 | ||||
| -rw-r--r-- | ot_tcp.h | 22 | ||||
| -rw-r--r-- | tests/integration/test_config.py | 29 | ||||
| -rw-r--r-- | tests/t_event.c | 32 |
14 files changed, 421 insertions, 135 deletions
| @@ -0,0 +1,99 @@ | |||
| 1 | opentracker design notes | ||
| 2 | ======================== | ||
| 3 | |||
| 4 | Decisions that cost measurement to reach, and the alternatives that were | ||
| 5 | tried and rejected. | ||
| 6 | |||
| 7 | What the tracker is optimised for | ||
| 8 | --------------------------------- | ||
| 9 | |||
| 10 | UDP carries most of the traffic. An HTTP request costs about ten times a UDP | ||
| 11 | announce, 26 microseconds of tracker CPU against 2.5. Nearly all of that is | ||
| 12 | the kernel setting up and tearing down a connection rather than tracker | ||
| 13 | code. | ||
| 14 | |||
| 15 | A connection carries one request and closes. Keep-alive works and is rarely | ||
| 16 | asked for. | ||
| 17 | |||
| 18 | Statistics are approximate on purpose. Counters are per thread and summed | ||
| 19 | when read, so a /stats read can under-report once. Nothing drifts. | ||
| 20 | |||
| 21 | |||
| 22 | Decisions | ||
| 23 | --------- | ||
| 24 | |||
| 25 | One UDP socket per worker, where the kernel spreads datagrams over a group | ||
| 26 | bound to one address. Cost per datagram then stays flat as workers are added, | ||
| 27 | 0.71 to 0.72 microseconds from one worker to eight. Sharing one socket grows | ||
| 28 | it eightfold over the same range, 0.71 to 6.07. | ||
| 29 | |||
| 30 | listen.udp.workers defaults to 1. One worker beats none by 19 to 37 per cent | ||
| 31 | everywhere, because a blocking read costs one system call where the event | ||
| 32 | loop costs two. Beyond one the gain depends on the kernel balancing, so | ||
| 33 | raising it is the operator's decision. | ||
| 34 | |||
| 35 | A thread parked in a blocking read is woken by a signal, not by closing or | ||
| 36 | shutting down its socket: | ||
| 37 | |||
| 38 | mechanism Linux FreeBSD macOS | ||
| 39 | close() blocked blocked 0 ms | ||
| 40 | shutdown(SHUT_RD) 1 ms 1 ms blocked | ||
| 41 | SO_RCVTIMEO works works works | ||
| 42 | pthread_kill 1 ms 1 ms 1 ms | ||
| 43 | |||
| 44 | Only the signal works everywhere. A receive timeout also works, but wakes | ||
| 45 | every thread on every tick for the life of the process to catch something | ||
| 46 | that happens once: at eight workers it doubled idle context switches, 255 to | ||
| 47 | 495 over thirty seconds. | ||
| 48 | |||
| 49 | Each binary is built in one compiler invocation. Without object files a build | ||
| 50 | cannot link objects compiled with different FEATURES, which was a real | ||
| 51 | failure mode. It costs incremental rebuilds, 0.075 seconds becoming 1.56. | ||
| 52 | |||
| 53 | A socket is read or written, never both. http_interest_set replaces an | ||
| 54 | add-and-drop pair so that the invariant cannot be broken by accident, and the | ||
| 55 | event layer hears about a change once instead of twice. | ||
| 56 | |||
| 57 | Accept waits until the client's request has arrived: TCP_DEFER_ACCEPT on | ||
| 58 | Linux, the accf_data filter on FreeBSD. Measured against a flood of silent | ||
| 59 | connections, the filter serves more requests than no filter, not fewer, so it | ||
| 60 | is not the denial of service vector that FreeBSD-SA-02:26 once described. | ||
| 61 | |||
| 62 | |||
| 63 | Paths not taken | ||
| 64 | --------------- | ||
| 65 | |||
| 66 | accf_http, the HTTP-aware accept filter. It duplicates a parser the tracker | ||
| 67 | has anyway, puts more kernel code in the path, and holds a connection that | ||
| 68 | sends "GET /" and stops, which accf_data hands straight over. | ||
| 69 | |||
| 70 | recvmmsg and sendmmsg. No measurable gain on either FreeBSD machine, between | ||
| 71 | -4 and +10 per cent, and they do not exist on Darwin. | ||
| 72 | |||
| 73 | Link time optimisation. It cuts .text by 13 per cent, 93,497 bytes to 81,630, | ||
| 74 | and changes throughput not at all. A tracker waits on memory and system | ||
| 75 | calls, not on instruction fetch: the announce path is 6.6 KB of code against | ||
| 76 | a 32 KB instruction cache at worst. | ||
| 77 | |||
| 78 | A directory for object files. bmake has no pattern rules and the Makefile | ||
| 79 | builds under FreeBSD's make today. Building each binary in one invocation | ||
| 80 | clears the objects out of the tree without giving that up. | ||
| 81 | |||
| 82 | A src/ directory. 52 files sharing an ot_ prefix are a module list rather | ||
| 83 | than clutter, and build output was most of what made the tree look crowded. | ||
| 84 | |||
| 85 | |||
| 86 | Platform differences that cost us code | ||
| 87 | -------------------------------------- | ||
| 88 | |||
| 89 | SO_REUSEPORT means three different things. Linux spreads datagrams over the | ||
| 90 | group; FreeBSD permits the bind and needs SO_REUSEPORT_LB to spread. Darwin | ||
| 91 | hands every datagram to whichever socket bound last, so a group there starves | ||
| 92 | all but one reader. | ||
| 93 | |||
| 94 | Waking a blocked reader differs per kernel. see the table above. | ||
| 95 | |||
| 96 | Deferring accept is TCP_DEFER_ACCEPT, or accf_data, or nothing. | ||
| 97 | |||
| 98 | make is bmake on FreeBSD and GNU make elsewhere, and the Makefile has to | ||
| 99 | build under both. | ||
| @@ -69,8 +69,8 @@ CFLAGS_libowfat=-I$(LIBOWFAT_HEADERS) | |||
| 69 | LDFLAGS_libowfat=-L$(LIBOWFAT_LIBRARY) -lowfat | 69 | LDFLAGS_libowfat=-L$(LIBOWFAT_LIBRARY) -lowfat |
| 70 | 70 | ||
| 71 | BINARY =opentracker | 71 | BINARY =opentracker |
| 72 | HEADERS=trackerlogic.h ot_urlencoded.h ot_batch.h ot_buf.h ot_event.h ot_fdstate.h ot_fmt.h ot_hex.h ot_ip.h ot_config.h ot_scan.h ot_socket.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h | 72 | HEADERS=trackerlogic.h ot_urlencoded.h ot_batch.h ot_buf.h ot_event.h ot_fdstate.h ot_fmt.h ot_hex.h ot_ip.h ot_config.h ot_scan.h ot_socket.h ot_tcp.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h |
| 73 | SOURCES=opentracker.c trackerlogic.c ot_urlencoded.c ot_batch.c ot_buf.c ot_event.c ot_fdstate.c ot_fmt.c ot_hex.c ot_ip.c ot_config.c ot_scan.c ot_socket.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c | 73 | SOURCES=opentracker.c trackerlogic.c ot_urlencoded.c ot_batch.c ot_buf.c ot_event.c ot_fdstate.c ot_fmt.c ot_hex.c ot_ip.c ot_config.c ot_scan.c ot_socket.c ot_tcp.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c |
| 74 | SOURCES_proxy=proxy.c ot_batch.c ot_fdstate.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c | 74 | SOURCES_proxy=proxy.c ot_batch.c ot_fdstate.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c |
| 75 | 75 | ||
| 76 | all: $(BINARY) $(BINARY).debug | 76 | all: $(BINARY) $(BINARY).debug |
diff --git a/opentracker.c b/opentracker.c index ec92fe0..2eaf988 100644 --- a/opentracker.c +++ b/opentracker.c | |||
| @@ -35,6 +35,7 @@ | |||
| 35 | #include "ot_fdstate.h" | 35 | #include "ot_fdstate.h" |
| 36 | #include "ot_socket.h" | 36 | #include "ot_socket.h" |
| 37 | #include "ot_stats.h" | 37 | #include "ot_stats.h" |
| 38 | #include "ot_tcp.h" | ||
| 38 | #include "ot_udp.h" | 39 | #include "ot_udp.h" |
| 39 | #include "trackerlogic.h" | 40 | #include "trackerlogic.h" |
| 40 | 41 | ||
| @@ -96,126 +97,6 @@ static void install_signal_handlers(void) { | |||
| 96 | } | 97 | } |
| 97 | 98 | ||
| 98 | 99 | ||
| 99 | static ssize_t header_complete(char *request, ssize_t byte_count) { | ||
| 100 | ssize_t i = 0, state = 0; | ||
| 101 | |||
| 102 | for (i = 1; i < byte_count; i += 2) | ||
| 103 | if (request[i] <= 13) { | ||
| 104 | i--; | ||
| 105 | for (state = 0; i < byte_count; ++i) { | ||
| 106 | char c = request[i]; | ||
| 107 | if (c == '\r' || c == '\n') | ||
| 108 | state = (state >> 2) | ((c << 6) & 0xc0); | ||
| 109 | else | ||
| 110 | break; | ||
| 111 | if (state >= 0xa0 || state == 0x99) | ||
| 112 | return i + 1; | ||
| 113 | } | ||
| 114 | } | ||
| 115 | return 0; | ||
| 116 | } | ||
| 117 | |||
| 118 | static void handle_read(const int64_t sock, struct ot_workstruct *ws) { | ||
| 119 | struct http_data *cookie = fdstate_cookie(sock); | ||
| 120 | ssize_t byte_count = read((int)sock, ws->inbuf, G_INBUF_SIZE); | ||
| 121 | |||
| 122 | if (byte_count == 0 || (byte_count == -1 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) { | ||
| 123 | http_close(sock); | ||
| 124 | return; | ||
| 125 | } | ||
| 126 | |||
| 127 | if (byte_count == -1) | ||
| 128 | return; | ||
| 129 | |||
| 130 | ws->request = NULL; | ||
| 131 | ws->request_size = 0; | ||
| 132 | ws->header_size = 0; | ||
| 133 | ws->keep_alive = 0; | ||
| 134 | |||
| 135 | /* If we get the whole request in one packet, handle it without copying */ | ||
| 136 | if (!cookie->request.data) { | ||
| 137 | if ((ws->header_size = header_complete(ws->inbuf, byte_count))) { | ||
| 138 | ws->request = ws->inbuf; | ||
| 139 | ws->request_size = byte_count; | ||
| 140 | http_handle_request(sock, ws); | ||
| 141 | } else if (buf_cat(&cookie->request, ws->inbuf, (size_t)byte_count)) | ||
| 142 | http_close(sock); | ||
| 143 | return; | ||
| 144 | } | ||
| 145 | |||
| 146 | if (buf_cat(&cookie->request, ws->inbuf, (size_t)byte_count) || cookie->request.size > G_INBUF_SIZE) { | ||
| 147 | http_issue_error(sock, ws, CODE_HTTPERROR_500); | ||
| 148 | return; | ||
| 149 | } | ||
| 150 | |||
| 151 | while ((ws->header_size = header_complete(cookie->request.data, (ssize_t)cookie->request.size))) { | ||
| 152 | ws->request = cookie->request.data; | ||
| 153 | ws->request_size = (ssize_t)cookie->request.size; | ||
| 154 | http_handle_request(sock, ws); | ||
| 155 | #ifdef WANT_KEEPALIVE | ||
| 156 | if (!ws->keep_alive) | ||
| 157 | #endif | ||
| 158 | return; | ||
| 159 | } | ||
| 160 | } | ||
| 161 | |||
| 162 | static void handle_write(const int64_t sock) { | ||
| 163 | struct http_data *cookie = fdstate_cookie(sock); | ||
| 164 | int chunked = 0; | ||
| 165 | int keep_alive = 0; | ||
| 166 | |||
| 167 | if (cookie) { | ||
| 168 | chunked = (cookie->flag & STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER) != 0; | ||
| 169 | keep_alive = (cookie->flag & STRUCT_HTTP_FLAG_KEEPALIVE) != 0; | ||
| 170 | |||
| 171 | if (cookie->batch) { | ||
| 172 | if (batch_send(sock, cookie->batch) == BATCH_FAILED) { | ||
| 173 | http_close(sock); | ||
| 174 | return; | ||
| 175 | } | ||
| 176 | if (batch_pending(cookie->batch)) | ||
| 177 | return; | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | /* In a chunked transfer, wait for the next piece once everything queued has gone out */ | ||
| 182 | if (chunked) | ||
| 183 | http_interest_set(sock, cookie, 0); | ||
| 184 | else if (keep_alive) { | ||
| 185 | /* The reply is out and the client asked to keep the connection, so go | ||
| 186 | back to waiting for the next request */ | ||
| 187 | http_interest_set(sock, cookie, EVENT_IN); | ||
| 188 | fdstate_set_expiry(sock, time_future_seconds(OT_CLIENT_TIMEOUT)); | ||
| 189 | } else | ||
| 190 | http_close(sock); | ||
| 191 | } | ||
| 192 | |||
| 193 | static void handle_accept(const int64_t serversocket, struct ot_workstruct *ws) { | ||
| 194 | struct http_data *cookie; | ||
| 195 | int64_t sock; | ||
| 196 | ot_ip6 ip; | ||
| 197 | uint16_t port; | ||
| 198 | |||
| 199 | while ((sock = socket_accept(serversocket, ip, &port)) != -1) { | ||
| 200 | |||
| 201 | if (!(cookie = (struct http_data *)calloc(1, sizeof(struct http_data)))) { | ||
| 202 | close((int)sock); | ||
| 203 | continue; | ||
| 204 | } | ||
| 205 | memcpy(cookie->ip, ip, sizeof(ot_ip6)); | ||
| 206 | cookie->interest = EVENT_IN; | ||
| 207 | |||
| 208 | if (event_add((int)sock, EVENT_IN, fdstate_open(sock, cookie, time_future_seconds(OT_CLIENT_TIMEOUT))) == -1) { | ||
| 209 | fdstate_close(sock); | ||
| 210 | free(cookie); | ||
| 211 | close((int)sock); | ||
| 212 | continue; | ||
| 213 | } | ||
| 214 | |||
| 215 | stats_issue_event(ws, EVENT_ACCEPT, FLAG_TCP, (uintptr_t)ip); | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | /* How many ready descriptors one wait hands back at a time */ | 100 | /* How many ready descriptors one wait hands back at a time */ |
| 220 | #define OT_EVENT_BATCH 512 | 101 | #define OT_EVENT_BATCH 512 |
| 221 | 102 | ||
| @@ -261,7 +142,7 @@ static void *server_mainloop(void *args) { | |||
| 261 | cookie = fdstate_cookie((int)sock); | 142 | cookie = fdstate_cookie((int)sock); |
| 262 | 143 | ||
| 263 | if ((intptr_t)cookie == FLAG_TCP) | 144 | if ((intptr_t)cookie == FLAG_TCP) |
| 264 | handle_accept(sock, &ws); | 145 | tcp_accept(sock, &ws); |
| 265 | else if ((intptr_t)cookie == FLAG_UDP) | 146 | else if ((intptr_t)cookie == FLAG_UDP) |
| 266 | handle_udp6(sock, &ws); | 147 | handle_udp6(sock, &ws); |
| 267 | else if ((intptr_t)cookie == FLAG_SELFPIPE) | 148 | else if ((intptr_t)cookie == FLAG_SELFPIPE) |
| @@ -269,10 +150,10 @@ static void *server_mainloop(void *args) { | |||
| 269 | ; | 150 | ; |
| 270 | else { | 151 | else { |
| 271 | if (ready[i].what & EVENT_IN) | 152 | if (ready[i].what & EVENT_IN) |
| 272 | handle_read(sock, &ws); | 153 | tcp_read(sock, &ws); |
| 273 | /* The read may have closed it, so look again before writing */ | 154 | /* The read may have closed it, so look again before writing */ |
| 274 | if ((ready[i].what & EVENT_OUT) && fdstate_generation((int)sock) == ready[i].generation) | 155 | if ((ready[i].what & EVENT_OUT) && fdstate_generation((int)sock) == ready[i].generation) |
| 275 | handle_write(sock); | 156 | tcp_write(sock); |
| 276 | } | 157 | } |
| 277 | } | 158 | } |
| 278 | 159 | ||
diff --git a/ot_config.c b/ot_config.c index a0dbef1..272acf1 100644 --- a/ot_config.c +++ b/ot_config.c | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | 14 | ||
| 15 | /* Opentracker */ | 15 | /* Opentracker */ |
| 16 | #include "ot_accesslist.h" | 16 | #include "ot_accesslist.h" |
| 17 | #include "ot_event.h" | ||
| 17 | #include "ot_config.h" | 18 | #include "ot_config.h" |
| 18 | #include "ot_http.h" | 19 | #include "ot_http.h" |
| 19 | #include "ot_ip.h" | 20 | #include "ot_ip.h" |
| @@ -32,7 +33,8 @@ const char *g_statefile; | |||
| 32 | static ot_listen *g_listen; | 33 | static ot_listen *g_listen; |
| 33 | static size_t g_listen_count; | 34 | static size_t g_listen_count; |
| 34 | static int g_listen_shared; | 35 | static int g_listen_shared; |
| 35 | static int g_listen_workers = 1; | 36 | static int g_listen_workers_udp = 1; |
| 37 | static int g_listen_workers_tcp = 0; | ||
| 36 | 38 | ||
| 37 | static void panic(const char *routine) __attribute__((noreturn)); | 39 | static void panic(const char *routine) __attribute__((noreturn)); |
| 38 | static void panic(const char *routine) { | 40 | static void panic(const char *routine) { |
| @@ -196,6 +198,7 @@ typedef enum { | |||
| 196 | CONFIG_LISTEN_TCP, | 198 | CONFIG_LISTEN_TCP, |
| 197 | CONFIG_LISTEN_UDP, | 199 | CONFIG_LISTEN_UDP, |
| 198 | CONFIG_LISTEN_UDP_WORKERS, | 200 | CONFIG_LISTEN_UDP_WORKERS, |
| 201 | CONFIG_LISTEN_TCP_WORKERS, | ||
| 199 | CONFIG_LISTEN_SHARED, | 202 | CONFIG_LISTEN_SHARED, |
| 200 | CONFIG_ACCESS_WHITELIST, | 203 | CONFIG_ACCESS_WHITELIST, |
| 201 | CONFIG_ACCESS_BLACKLIST, | 204 | CONFIG_ACCESS_BLACKLIST, |
| @@ -224,6 +227,7 @@ static const config_key config_keys[] = { | |||
| 224 | CONFIG_KEY("listen.tcp", CONFIG_LISTEN_TCP), | 227 | CONFIG_KEY("listen.tcp", CONFIG_LISTEN_TCP), |
| 225 | CONFIG_KEY("listen.udp", CONFIG_LISTEN_UDP), | 228 | CONFIG_KEY("listen.udp", CONFIG_LISTEN_UDP), |
| 226 | CONFIG_KEY("listen.udp.workers", CONFIG_LISTEN_UDP_WORKERS), | 229 | CONFIG_KEY("listen.udp.workers", CONFIG_LISTEN_UDP_WORKERS), |
| 230 | CONFIG_KEY("listen.tcp.workers", CONFIG_LISTEN_TCP_WORKERS), | ||
| 227 | CONFIG_KEY("listen.shared", CONFIG_LISTEN_SHARED), | 231 | CONFIG_KEY("listen.shared", CONFIG_LISTEN_SHARED), |
| 228 | #ifdef WANT_ACCESSLIST_WHITE | 232 | #ifdef WANT_ACCESSLIST_WHITE |
| 229 | CONFIG_KEY("access.whitelist", CONFIG_ACCESS_WHITELIST), | 233 | CONFIG_KEY("access.whitelist", CONFIG_ACCESS_WHITELIST), |
| @@ -318,7 +322,7 @@ static int parse_configfile(char *config_filename) { | |||
| 318 | if (!off || !listen_scan_options(p + off, &shared, &workers)) | 322 | if (!off || !listen_scan_options(p + off, &shared, &workers)) |
| 319 | goto parse_error; | 323 | goto parse_error; |
| 320 | if (kind != CONFIG_LISTEN_UDP) | 324 | if (kind != CONFIG_LISTEN_UDP) |
| 321 | listen_append(tmpip, tmpport, FLAG_TCP, shared, OT_LISTEN_INHERIT), ++bound; | 325 | listen_append(tmpip, tmpport, FLAG_TCP, shared, workers), ++bound; |
| 322 | if (kind != CONFIG_LISTEN_TCP) | 326 | if (kind != CONFIG_LISTEN_TCP) |
| 323 | listen_append(tmpip, tmpport, FLAG_UDP, shared, workers), ++bound; | 327 | listen_append(tmpip, tmpport, FLAG_UDP, shared, workers), ++bound; |
| 324 | break; | 328 | break; |
| @@ -326,7 +330,12 @@ static int parse_configfile(char *config_filename) { | |||
| 326 | case CONFIG_LISTEN_UDP_WORKERS: | 330 | case CONFIG_LISTEN_UDP_WORKERS: |
| 327 | if (!scan_uint(p, &value, UINT_MAX)) | 331 | if (!scan_uint(p, &value, UINT_MAX)) |
| 328 | goto parse_error; | 332 | goto parse_error; |
| 329 | g_listen_workers = (int)value; | 333 | g_listen_workers_udp = (int)value; |
| 334 | break; | ||
| 335 | case CONFIG_LISTEN_TCP_WORKERS: | ||
| 336 | if (!scan_uint(p, &value, UINT_MAX)) | ||
| 337 | goto parse_error; | ||
| 338 | g_listen_workers_tcp = (int)value; | ||
| 330 | break; | 339 | break; |
| 331 | case CONFIG_LISTEN_SHARED: | 340 | case CONFIG_LISTEN_SHARED: |
| 332 | if (!scan_uint(p, &value, 1)) | 341 | if (!scan_uint(p, &value, 1)) |
| @@ -446,7 +455,16 @@ static void listen_resolve_defaults(void) { | |||
| 446 | if (entry->shared == OT_LISTEN_INHERIT) | 455 | if (entry->shared == OT_LISTEN_INHERIT) |
| 447 | entry->shared = g_listen_shared; | 456 | entry->shared = g_listen_shared; |
| 448 | if (entry->workers == OT_LISTEN_INHERIT) | 457 | if (entry->workers == OT_LISTEN_INHERIT) |
| 449 | entry->workers = g_listen_workers; | 458 | entry->workers = entry->proto == FLAG_TCP ? g_listen_workers_tcp : g_listen_workers_udp; |
| 459 | |||
| 460 | /* A tcp worker hands a connection it cannot finish to the event loop, | ||
| 461 | which only one thread may touch where the set is ours rather than the | ||
| 462 | kernel's */ | ||
| 463 | if (entry->proto == FLAG_TCP && entry->workers > 1 && !event_can_share()) { | ||
| 464 | fprintf(stderr, "Warning: Multiple workers impossible on this platform with event mechanism %s chosen.\n", | ||
| 465 | event_mechanism()); | ||
| 466 | entry->workers = 1; | ||
| 467 | } | ||
| 450 | } | 468 | } |
| 451 | } | 469 | } |
| 452 | 470 | ||
| @@ -40,6 +40,9 @@ static struct epoll_event *g_ready; | |||
| 40 | static int g_ready_space; | 40 | static int g_ready_space; |
| 41 | 41 | ||
| 42 | const char *event_mechanism(void) { return "epoll"; } | 42 | const char *event_mechanism(void) { return "epoll"; } |
| 43 | /* The set lives in the kernel, and epoll_ctl during another thread's | ||
| 44 | epoll_wait is what it is for */ | ||
| 45 | int event_can_share(void) { return 1; } | ||
| 43 | 46 | ||
| 44 | int event_init(void) { | 47 | int event_init(void) { |
| 45 | g_ready = NULL; | 48 | g_ready = NULL; |
| @@ -118,6 +121,8 @@ static struct kevent *g_ready; | |||
| 118 | static int g_ready_space; | 121 | static int g_ready_space; |
| 119 | 122 | ||
| 120 | const char *event_mechanism(void) { return "kqueue"; } | 123 | const char *event_mechanism(void) { return "kqueue"; } |
| 124 | /* The set lives in the kernel, and kevent may be called from any thread */ | ||
| 125 | int event_can_share(void) { return 1; } | ||
| 121 | 126 | ||
| 122 | int event_init(void) { | 127 | int event_init(void) { |
| 123 | g_ready = NULL; | 128 | g_ready = NULL; |
| @@ -171,7 +176,7 @@ static int event_change(int fd, int what, uint32_t generation, int deleting) { | |||
| 171 | return 0; | 176 | return 0; |
| 172 | } | 177 | } |
| 173 | 178 | ||
| 174 | int event_add(int fd, int what, uint32_t generation) { return event_change(fd, what, generation, 0); } | 179 | int event_add(int fd, int what, uint32_t generation) { return what ? event_change(fd, what, generation, 0) : 0; } |
| 175 | int event_modify(int fd, int what, uint32_t generation) { return event_change(fd, what, generation, 0); } | 180 | int event_modify(int fd, int what, uint32_t generation) { return event_change(fd, what, generation, 0); } |
| 176 | int event_del(int fd) { return event_change(fd, 0, 0, 1); } | 181 | int event_del(int fd) { return event_change(fd, 0, 0, 1); } |
| 177 | 182 | ||
| @@ -229,6 +234,8 @@ static int *g_where; | |||
| 229 | static int g_count, g_space, g_where_space; | 234 | static int g_count, g_space, g_where_space; |
| 230 | 235 | ||
| 231 | const char *event_mechanism(void) { return "poll"; } | 236 | const char *event_mechanism(void) { return "poll"; } |
| 237 | /* The arrays below are ours and nothing guards them */ | ||
| 238 | int event_can_share(void) { return 0; } | ||
| 232 | 239 | ||
| 233 | int event_init(void) { | 240 | int event_init(void) { |
| 234 | g_pfds = NULL, g_generation = NULL, g_where = NULL; | 241 | g_pfds = NULL, g_generation = NULL, g_where = NULL; |
| @@ -38,6 +38,11 @@ int event_close(int fd); | |||
| 38 | out with up to max ready descriptors. Returns how many, or -1. */ | 38 | out with up to max ready descriptors. Returns how many, or -1. */ |
| 39 | int event_wait(ot_event *out, int max, int timeout_ms); | 39 | int event_wait(ot_event *out, int max, int timeout_ms); |
| 40 | 40 | ||
| 41 | /* Whether the registered set may be changed from another thread while this | ||
| 42 | one is waiting on it. True where the kernel keeps the set, false where | ||
| 43 | this module keeps it in memory of its own. */ | ||
| 44 | int event_can_share(void); | ||
| 45 | |||
| 41 | /* Which mechanism was compiled in, for the version string */ | 46 | /* Which mechanism was compiled in, for the version string */ |
| 42 | const char *event_mechanism(void); | 47 | const char *event_mechanism(void); |
| 43 | 48 | ||
diff --git a/ot_fdstate.c b/ot_fdstate.c index 537bcb0..7d742da 100644 --- a/ot_fdstate.c +++ b/ot_fdstate.c | |||
| @@ -186,6 +186,13 @@ void *fdstate_cookie(int fd) { | |||
| 186 | return slot ? atomic_load_explicit(&slot->cookie, memory_order_relaxed) : NULL; | 186 | return slot ? atomic_load_explicit(&slot->cookie, memory_order_relaxed) : NULL; |
| 187 | } | 187 | } |
| 188 | 188 | ||
| 189 | void fdstate_set_cookie(int fd, void *cookie) { | ||
| 190 | ot_fdslot *slot = fdstate_slot(fd); | ||
| 191 | |||
| 192 | if (slot) | ||
| 193 | atomic_store_explicit(&slot->cookie, cookie, memory_order_relaxed); | ||
| 194 | } | ||
| 195 | |||
| 189 | void fdstate_set_expiry(int fd, ot_time expiry) { | 196 | void fdstate_set_expiry(int fd, ot_time expiry) { |
| 190 | ot_fdslot *slot = fdstate_slot(fd); | 197 | ot_fdslot *slot = fdstate_slot(fd); |
| 191 | 198 | ||
diff --git a/ot_fdstate.h b/ot_fdstate.h index c09f018..93890d1 100644 --- a/ot_fdstate.h +++ b/ot_fdstate.h | |||
| @@ -56,6 +56,9 @@ int fdstate_next_expired(ot_time now, int *cursor); | |||
| 56 | 56 | ||
| 57 | /* Both only valid while the caller holds the descriptor. */ | 57 | /* Both only valid while the caller holds the descriptor. */ |
| 58 | void *fdstate_cookie(int fd); | 58 | void *fdstate_cookie(int fd); |
| 59 | |||
| 60 | /* Hand the slot a different cookie, for a connection whose state has moved */ | ||
| 61 | void fdstate_set_cookie(int fd, void *cookie); | ||
| 59 | void fdstate_set_expiry(int fd, ot_time expiry); | 62 | void fdstate_set_expiry(int fd, ot_time expiry); |
| 60 | 63 | ||
| 61 | #endif | 64 | #endif |
| @@ -39,9 +39,25 @@ 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 | /* Give up a connection: the one place that knows everything hanging off it. | 42 | int http_adopt(const int64_t sock, struct http_data *cookie) { |
| 43 | The slot is released before anything is freed, so that a late event or the | 43 | struct http_data *heap = malloc(sizeof(*heap)); |
| 44 | expiry sweep finds a closed descriptor rather than a freed cookie. */ | 44 | |
| 45 | if (!heap) | ||
| 46 | return -1; | ||
| 47 | |||
| 48 | *heap = *cookie; | ||
| 49 | heap->flag &= ~STRUCT_HTTP_FLAG_ONSTACK; | ||
| 50 | fdstate_set_cookie((int)sock, heap); | ||
| 51 | |||
| 52 | if (event_add((int)sock, heap->interest, fdstate_generation((int)sock)) == -1) { | ||
| 53 | fdstate_set_cookie((int)sock, cookie); | ||
| 54 | free(heap); | ||
| 55 | return -1; | ||
| 56 | } | ||
| 57 | return 0; | ||
| 58 | } | ||
| 59 | |||
| 60 | /* Give up a connection: the one place that knows everything hanging off it. */ | ||
| 45 | void http_close(const int64_t sock) { | 61 | void http_close(const int64_t sock) { |
| 46 | struct http_data *cookie = fdstate_cookie(sock); | 62 | struct http_data *cookie = fdstate_cookie(sock); |
| 47 | 63 | ||
| @@ -54,7 +70,8 @@ void http_close(const int64_t sock) { | |||
| 54 | buf_reset(&cookie->request); | 70 | buf_reset(&cookie->request); |
| 55 | if (cookie->flag & (STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER)) | 71 | if (cookie->flag & (STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER)) |
| 56 | mutex_workqueue_canceltask(sock); | 72 | mutex_workqueue_canceltask(sock); |
| 57 | free(cookie); | 73 | if (!(cookie->flag & STRUCT_HTTP_FLAG_ONSTACK)) |
| 74 | free(cookie); | ||
| 58 | } | 75 | } |
| 59 | 76 | ||
| 60 | event_close((int)sock); | 77 | event_close((int)sock); |
| @@ -140,6 +157,10 @@ void http_interest_set(const int64_t sock, struct http_data *cookie, int what) { | |||
| 140 | if (cookie->interest == what) | 157 | if (cookie->interest == what) |
| 141 | return; | 158 | return; |
| 142 | cookie->interest = what; | 159 | cookie->interest = what; |
| 160 | /* Still on a stack, so not in the event loop yet: whoever adopts this | ||
| 161 | connection registers whatever it asked for last */ | ||
| 162 | if (cookie->flag & STRUCT_HTTP_FLAG_ONSTACK) | ||
| 163 | return; | ||
| 143 | event_modify((int)sock, what, fdstate_generation((int)sock)); | 164 | event_modify((int)sock, what, fdstate_generation((int)sock)); |
| 144 | } | 165 | } |
| 145 | 166 | ||
| @@ -22,7 +22,8 @@ typedef enum { | |||
| 22 | STRUCT_HTTP_FLAG_ZSTD = 8, | 22 | STRUCT_HTTP_FLAG_ZSTD = 8, |
| 23 | STRUCT_HTTP_FLAG_CHUNKED = 16, | 23 | STRUCT_HTTP_FLAG_CHUNKED = 16, |
| 24 | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER = 32, | 24 | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER = 32, |
| 25 | STRUCT_HTTP_FLAG_KEEPALIVE = 64 | 25 | STRUCT_HTTP_FLAG_KEEPALIVE = 64, |
| 26 | STRUCT_HTTP_FLAG_ONSTACK = 128 | ||
| 26 | } STRUCT_HTTP_FLAG; | 27 | } STRUCT_HTTP_FLAG; |
| 27 | 28 | ||
| 28 | struct http_data { | 29 | struct http_data { |
| @@ -39,6 +40,10 @@ void http_interest_set(const int64_t s, struct http_data *cookie, int | |||
| 39 | /* Close a connection and release everything that belongs to it */ | 40 | /* Close a connection and release everything that belongs to it */ |
| 40 | void http_close(const int64_t s); | 41 | void http_close(const int64_t s); |
| 41 | 42 | ||
| 43 | /* Promote a stack cookie to a heap one for persistence and handing it over | ||
| 44 | to the event loop. */ | ||
| 45 | int http_adopt(const int64_t s, struct http_data *cookie); | ||
| 46 | |||
| 42 | ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); | 47 | ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); |
| 43 | ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); | 48 | ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); |
| 44 | ssize_t http_issue_error(const int64_t s, struct ot_workstruct *ws, int code); | 49 | ssize_t http_issue_error(const int64_t s, struct ot_workstruct *ws, int code); |
diff --git a/ot_tcp.c b/ot_tcp.c new file mode 100644 index 0000000..8647a47 --- /dev/null +++ b/ot_tcp.c | |||
| @@ -0,0 +1,157 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <errno.h> | ||
| 8 | #include <string.h> | ||
| 9 | #include <unistd.h> | ||
| 10 | |||
| 11 | /* Opentracker */ | ||
| 12 | #include "ot_batch.h" | ||
| 13 | #include "ot_buf.h" | ||
| 14 | #include "ot_event.h" | ||
| 15 | #include "ot_fdstate.h" | ||
| 16 | #include "ot_http.h" | ||
| 17 | #include "ot_mutex.h" | ||
| 18 | #include "ot_socket.h" | ||
| 19 | #include "ot_stats.h" | ||
| 20 | #include "ot_tcp.h" | ||
| 21 | #include "trackerlogic.h" | ||
| 22 | |||
| 23 | static ssize_t header_complete(char *request, ssize_t byte_count) { | ||
| 24 | ssize_t i = 0, state = 0; | ||
| 25 | |||
| 26 | for (i = 1; i < byte_count; i += 2) | ||
| 27 | if (request[i] <= 13) { | ||
| 28 | i--; | ||
| 29 | for (state = 0; i < byte_count; ++i) { | ||
| 30 | char c = request[i]; | ||
| 31 | if (c == '\r' || c == '\n') | ||
| 32 | state = (state >> 2) | ((c << 6) & 0xc0); | ||
| 33 | else | ||
| 34 | break; | ||
| 35 | if (state >= 0xa0 || state == 0x99) | ||
| 36 | return i + 1; | ||
| 37 | } | ||
| 38 | } | ||
| 39 | return 0; | ||
| 40 | } | ||
| 41 | |||
| 42 | void tcp_read(const int64_t sock, struct ot_workstruct *ws) { | ||
| 43 | struct http_data *cookie = fdstate_cookie(sock); | ||
| 44 | ssize_t byte_count = read((int)sock, ws->inbuf, G_INBUF_SIZE); | ||
| 45 | |||
| 46 | if (byte_count == 0 || (byte_count == -1 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) { | ||
| 47 | http_close(sock); | ||
| 48 | return; | ||
| 49 | } | ||
| 50 | |||
| 51 | if (byte_count == -1) | ||
| 52 | return; | ||
| 53 | |||
| 54 | ws->request = NULL; | ||
| 55 | ws->request_size = 0; | ||
| 56 | ws->header_size = 0; | ||
| 57 | ws->keep_alive = 0; | ||
| 58 | |||
| 59 | /* If we get the whole request in one packet, handle it without copying */ | ||
| 60 | if (!cookie->request.data) { | ||
| 61 | if ((ws->header_size = header_complete(ws->inbuf, byte_count))) { | ||
| 62 | ws->request = ws->inbuf; | ||
| 63 | ws->request_size = byte_count; | ||
| 64 | http_handle_request(sock, ws); | ||
| 65 | } else if (buf_cat(&cookie->request, ws->inbuf, (size_t)byte_count)) | ||
| 66 | http_close(sock); | ||
| 67 | return; | ||
| 68 | } | ||
| 69 | |||
| 70 | if (buf_cat(&cookie->request, ws->inbuf, (size_t)byte_count) || cookie->request.size > G_INBUF_SIZE) { | ||
| 71 | http_issue_error(sock, ws, CODE_HTTPERROR_500); | ||
| 72 | return; | ||
| 73 | } | ||
| 74 | |||
| 75 | while ((ws->header_size = header_complete(cookie->request.data, (ssize_t)cookie->request.size))) { | ||
| 76 | ws->request = cookie->request.data; | ||
| 77 | ws->request_size = (ssize_t)cookie->request.size; | ||
| 78 | http_handle_request(sock, ws); | ||
| 79 | #ifdef WANT_KEEPALIVE | ||
| 80 | if (!ws->keep_alive) | ||
| 81 | #endif | ||
| 82 | return; | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | void tcp_write(const int64_t sock) { | ||
| 87 | struct http_data *cookie = fdstate_cookie(sock); | ||
| 88 | int chunked = 0; | ||
| 89 | int keep_alive = 0; | ||
| 90 | |||
| 91 | if (cookie) { | ||
| 92 | chunked = (cookie->flag & STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER) != 0; | ||
| 93 | keep_alive = (cookie->flag & STRUCT_HTTP_FLAG_KEEPALIVE) != 0; | ||
| 94 | |||
| 95 | if (cookie->batch) { | ||
| 96 | if (batch_send(sock, cookie->batch) == BATCH_FAILED) { | ||
| 97 | http_close(sock); | ||
| 98 | return; | ||
| 99 | } | ||
| 100 | if (batch_pending(cookie->batch)) | ||
| 101 | return; | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | /* In a chunked transfer, wait for the next piece once everything queued has gone out */ | ||
| 106 | if (chunked) | ||
| 107 | http_interest_set(sock, cookie, 0); | ||
| 108 | else if (keep_alive) { | ||
| 109 | /* The reply is out and the client asked to keep the connection, so go | ||
| 110 | back to waiting for the next request */ | ||
| 111 | http_interest_set(sock, cookie, EVENT_IN); | ||
| 112 | fdstate_set_expiry(sock, time_future_seconds(OT_CLIENT_TIMEOUT)); | ||
| 113 | } else | ||
| 114 | http_close(sock); | ||
| 115 | } | ||
| 116 | |||
| 117 | /* The fast path, assuming that the complete request has usually arrived by the time a | ||
| 118 | connection is accepted, so it is answered here outside the event loop. The more load | ||
| 119 | a tracker has, the more likely it is that by the time it accepts, the roundtrip to | ||
| 120 | the peer has completed. | ||
| 121 | If the request could not be completely handled, the stack cookie is promoted to heap | ||
| 122 | and the connection handed over to the event loop to finish */ | ||
| 123 | void tcp_accept(const int64_t serversocket, struct ot_workstruct *ws) { | ||
| 124 | int64_t sock; | ||
| 125 | ot_ip6 ip; | ||
| 126 | uint16_t port; | ||
| 127 | |||
| 128 | while ((sock = socket_accept(serversocket, ip, &port)) != -1) { | ||
| 129 | struct http_data cookie; | ||
| 130 | uint32_t generation; | ||
| 131 | |||
| 132 | memset(&cookie, 0, sizeof(cookie)); | ||
| 133 | memcpy(cookie.ip, ip, sizeof(ot_ip6)); | ||
| 134 | cookie.interest = EVENT_IN; | ||
| 135 | cookie.flag = STRUCT_HTTP_FLAG_ONSTACK; | ||
| 136 | |||
| 137 | generation = fdstate_open(sock, &cookie, time_future_seconds(OT_CLIENT_TIMEOUT)); | ||
| 138 | if (!fdstate_claim((int)sock, generation)) { | ||
| 139 | fdstate_close(sock); | ||
| 140 | event_close((int)sock); | ||
| 141 | continue; | ||
| 142 | } | ||
| 143 | |||
| 144 | stats_issue_event(ws, EVENT_ACCEPT, FLAG_TCP, (uintptr_t)ip); | ||
| 145 | |||
| 146 | tcp_read(sock, ws); | ||
| 147 | |||
| 148 | /* Gone already if it was answered and closed inline */ | ||
| 149 | if (fdstate_generation((int)sock) != generation) | ||
| 150 | continue; | ||
| 151 | |||
| 152 | if (http_adopt(sock, &cookie) == -1) | ||
| 153 | http_close(sock); | ||
| 154 | else | ||
| 155 | fdstate_release((int)sock); | ||
| 156 | } | ||
| 157 | } | ||
diff --git a/ot_tcp.h b/ot_tcp.h new file mode 100644 index 0000000..b0a93f4 --- /dev/null +++ b/ot_tcp.h | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | #ifndef OT_TCP_H__ | ||
| 7 | #define OT_TCP_H__ | ||
| 8 | |||
| 9 | #include <stdint.h> | ||
| 10 | |||
| 11 | #include "trackerlogic.h" | ||
| 12 | |||
| 13 | /* Take whatever connections are waiting on a listening socket and answer | ||
| 14 | them, each on this thread's stack, handing a connection that cannot be | ||
| 15 | finished to the event loop */ | ||
| 16 | void tcp_accept(const int64_t serversocket, struct ot_workstruct *ws); | ||
| 17 | |||
| 18 | /* Handler connections in the event loop has become readable or writable */ | ||
| 19 | void tcp_read(const int64_t sock, struct ot_workstruct *ws); | ||
| 20 | void tcp_write(const int64_t sock); | ||
| 21 | |||
| 22 | #endif | ||
diff --git a/tests/integration/test_config.py b/tests/integration/test_config.py new file mode 100644 index 0000000..1a84fa7 --- /dev/null +++ b/tests/integration/test_config.py | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | """Settings the tracker has to accept, and still work with. | ||
| 2 | |||
| 3 | An unrecognised key makes opentracker refuse to start, so a test that it | ||
| 4 | comes up at all is a test that the key was understood. | ||
| 5 | """ | ||
| 6 | |||
| 7 | import unittest | ||
| 8 | |||
| 9 | from harness import Tracker, TrackerTestCase, free_port | ||
| 10 | |||
| 11 | |||
| 12 | class TcpWorkers(TrackerTestCase): | ||
| 13 | config = "listen.tcp.workers 2\n" | ||
| 14 | |||
| 15 | def test_the_tracker_serves_with_tcp_workers_asked_for(self): | ||
| 16 | self.assertEqual(self.tracker.announce(self.hash_for("tcpworkers"))[b"complete"], 1) | ||
| 17 | |||
| 18 | |||
| 19 | class WorkersOnAListenStatement(unittest.TestCase): | ||
| 20 | def test_a_listen_statement_may_carry_a_worker_count(self): | ||
| 21 | """The option is read off the statement rather than inherited.""" | ||
| 22 | second = free_port() | ||
| 23 | tracker = Tracker("listen.tcp 127.0.0.1:%d workers=2\n" % second) | ||
| 24 | try: | ||
| 25 | self.assertEqual(tracker.announce(b"listenopts\0\0\0\0\0\0\0\0\0\0")[b"complete"], 1) | ||
| 26 | complaints = tracker.stop() | ||
| 27 | self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) | ||
| 28 | finally: | ||
| 29 | tracker._cleanup() | ||
diff --git a/tests/t_event.c b/tests/t_event.c index ad3948f..1908dc9 100644 --- a/tests/t_event.c +++ b/tests/t_event.c | |||
| @@ -241,9 +241,40 @@ static void case_close_takes_it_out(void) { | |||
| 241 | pair_close(); | 241 | pair_close(); |
| 242 | } | 242 | } |
| 243 | 243 | ||
| 244 | /* A descriptor can be taken on before anybody waits for anything on it: the | ||
| 245 | full scrape parks a connection while a worker thread builds the reply. The | ||
| 246 | mechanisms disagree about what registering nothing means, so what is | ||
| 247 | checked is that asking for a filter afterwards still works. */ | ||
| 248 | static void case_add_nothing_then_ask(void) { | ||
| 249 | test_case("a descriptor added with no interest can be given some later"); | ||
| 250 | pair_open(); | ||
| 251 | CHECK(sv[0] != -1); | ||
| 252 | CHECK_INT(event_add(sv[0], 0, 7), 0); | ||
| 253 | CHECK_INT((int)write(sv[1], "x", 1), 1); | ||
| 254 | CHECK_INT(wait_for(sv[0], EVENT_IN, 200, NULL), 0); | ||
| 255 | |||
| 256 | CHECK_INT(event_modify(sv[0], EVENT_IN, 7), 0); | ||
| 257 | { | ||
| 258 | uint32_t generation = 0; | ||
| 259 | CHECK_INT(wait_for(sv[0], EVENT_IN, 500, &generation), 1); | ||
| 260 | CHECK_INT((int)generation, 7); | ||
| 261 | } | ||
| 262 | event_del(sv[0]); | ||
| 263 | pair_close(); | ||
| 264 | } | ||
| 265 | |||
| 266 | /* Whether a worker thread may register a descriptor while this one waits | ||
| 267 | depends on who keeps the set. Tying the answer to the mechanism's name | ||
| 268 | here means a backend added later cannot quietly forget to say. */ | ||
| 269 | static void case_sharing_matches_the_mechanism(void) { | ||
| 270 | test_case("only a set the kernel keeps may be changed from another thread"); | ||
| 271 | CHECK_INT(event_can_share(), strcmp(event_mechanism(), "poll") ? 1 : 0); | ||
| 272 | } | ||
| 273 | |||
| 244 | void test_event(void) { | 274 | void test_event(void) { |
| 245 | CHECK_INT(event_init(), 0); | 275 | CHECK_INT(event_init(), 0); |
| 246 | CHECK(event_mechanism() != NULL); | 276 | CHECK(event_mechanism() != NULL); |
| 277 | case_sharing_matches_the_mechanism(); | ||
| 247 | 278 | ||
| 248 | case_readable(); | 279 | case_readable(); |
| 249 | case_writable(); | 280 | case_writable(); |
| @@ -254,6 +285,7 @@ void test_event(void) { | |||
| 254 | case_timeout(); | 285 | case_timeout(); |
| 255 | case_unknown_descriptor(); | 286 | case_unknown_descriptor(); |
| 256 | case_close_takes_it_out(); | 287 | case_close_takes_it_out(); |
| 288 | case_add_nothing_then_ask(); | ||
| 257 | 289 | ||
| 258 | event_deinit(); | 290 | event_deinit(); |
| 259 | } | 291 | } |
