summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile8
-rw-r--r--opentracker.c8
-rw-r--r--ot_fdstate.c164
-rw-r--r--ot_fdstate.h52
-rw-r--r--ot_http.c23
-rw-r--r--ot_timeout.h39
-rw-r--r--tests/runner.c1
-rw-r--r--tests/t_fdstate.c224
-rw-r--r--tests/test.h1
9 files changed, 493 insertions, 27 deletions
diff --git a/Makefile b/Makefile
index c7105d4..3bf254a 100644
--- a/Makefile
+++ b/Makefile
@@ -68,8 +68,8 @@ CFLAGS+=-I$(LIBOWFAT_HEADERS) -DGIT_VERSION=$(GIT_VERSION) -Wall -pipe -pthread
68LDFLAGS+=-L$(LIBOWFAT_LIBRARY) -lowfat -pthread 68LDFLAGS+=-L$(LIBOWFAT_LIBRARY) -lowfat -pthread
69 69
70BINARY =opentracker 70BINARY =opentracker
71HEADERS=trackerlogic.h scan_urlencoded_query.h ot_batch.h ot_buf.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 71HEADERS=trackerlogic.h scan_urlencoded_query.h ot_batch.h ot_buf.h ot_fdstate.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
72SOURCES=opentracker.c trackerlogic.c scan_urlencoded_query.c ot_batch.c ot_buf.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 72SOURCES=opentracker.c trackerlogic.c scan_urlencoded_query.c ot_batch.c ot_buf.c ot_fdstate.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
73SOURCES_proxy=proxy.c ot_batch.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c 73SOURCES_proxy=proxy.c ot_batch.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c
74 74
75OBJECTS = $(SOURCES:%.c=%.o) 75OBJECTS = $(SOURCES:%.c=%.o)
@@ -119,8 +119,8 @@ $(OBJECTS_proxy_debug): $(HEADERS)
119# has to. tests/stubs.c supplies the few symbols those modules reference but 119# has to. tests/stubs.c supplies the few symbols those modules reference but
120# the tests never reach. 120# the tests never reach.
121TESTBIN=tests/runner 121TESTBIN=tests/runner
122TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_scan_urlencoded_query.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_batch.c 122TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_scan_urlencoded_query.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_batch.c tests/t_fdstate.c
123TEST_UNITS=scan_urlencoded_query.c ot_batch.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c 123TEST_UNITS=scan_urlencoded_query.c ot_batch.c ot_fdstate.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c
124TEST_HEADERS=tests/test.h $(HEADERS) 124TEST_HEADERS=tests/test.h $(HEADERS)
125# Tests reach code that sits behind feature flags, so the units under test are 125# Tests reach code that sits behind feature flags, so the units under test are
126# built with those features enabled. A test binary is not a production binary. 126# built with those features enabled. A test binary is not a production binary.
diff --git a/opentracker.c b/opentracker.c
index 50fff29..60bdceb 100644
--- a/opentracker.c
+++ b/opentracker.c
@@ -35,6 +35,7 @@
35#include "ot_random.h" 35#include "ot_random.h"
36#include "ot_scan.h" 36#include "ot_scan.h"
37#include "ot_socket.h" 37#include "ot_socket.h"
38#include "ot_timeout.h"
38#include "ot_stats.h" 39#include "ot_stats.h"
39#include "ot_udp.h" 40#include "ot_udp.h"
40#include "trackerlogic.h" 41#include "trackerlogic.h"
@@ -204,7 +205,6 @@ static void handle_accept(const int64_t serversocket) {
204 int64_t sock; 205 int64_t sock;
205 ot_ip6 ip; 206 ot_ip6 ip;
206 uint16_t port; 207 uint16_t port;
207 tai6464 t;
208 208
209 while ((sock = socket_accept(serversocket, ip, &port)) != -1) { 209 while ((sock = socket_accept(serversocket, ip, &port)) != -1) {
210 210
@@ -223,11 +223,7 @@ static void handle_accept(const int64_t serversocket) {
223 223
224 stats_issue_event(EVENT_ACCEPT, FLAG_TCP, (uintptr_t)ip); 224 stats_issue_event(EVENT_ACCEPT, FLAG_TCP, (uintptr_t)ip);
225 225
226 /* That breaks taia encapsulation. But there is no way to take system 226 timeout_in(sock, OT_CLIENT_TIMEOUT);
227 time this often in FreeBSD and libowfat does not allow to set unix time */
228 taia_uint(&t, 0); /* Clear t */
229 tai_unix(&(t.sec), (g_now_seconds + OT_CLIENT_TIMEOUT));
230 io_timeout(sock, t);
231 } 227 }
232 io_eagain(serversocket); 228 io_eagain(serversocket);
233} 229}
diff --git a/ot_fdstate.c b/ot_fdstate.c
new file mode 100644
index 0000000..6ddbea3
--- /dev/null
+++ b/ot_fdstate.c
@@ -0,0 +1,164 @@
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 <stdatomic.h>
8#include <stdint.h>
9#include <stdlib.h>
10#include <sys/mman.h>
11#include <sys/resource.h>
12#include <sys/types.h>
13
14/* Opentracker */
15#include "ot_fdstate.h"
16
17enum { FD_FREE, FD_IDLE, FD_BUSY, FD_CLOSING };
18
19/* Generation and state share one word so that taking the state is also a
20 check that the generation has not moved */
21#define FD_GEN(w) ((uint32_t)((w) >> 32))
22#define FD_ST(w) ((uint32_t)(((w) >> 24) & 0xff))
23#define FD_WORD(g, s) (((uint64_t)(g) << 32) | ((uint64_t)(s) << 24))
24#define FD_SET_ST(w, s) (((w) & ~(uint64_t)0xff000000) | ((uint64_t)(s) << 24))
25
26typedef struct {
27 _Atomic uint64_t state;
28 _Atomic(void *) cookie;
29 _Atomic ot_time expiry;
30} ot_fdslot;
31
32/* A descriptor the kernel can hand out is below the limit it was handed out
33 under, but the limit can be raised after we sized the table, so an out of
34 range descriptor is rejected */
35#define FD_SLOTS_MAX (1u << 20)
36
37static ot_fdslot *g_slots;
38static size_t g_slot_count;
39
40int fdstate_init(void) {
41 struct rlimit limit;
42 size_t count = FD_SLOTS_MAX;
43
44 if (getrlimit(RLIMIT_NOFILE, &limit) == 0 && limit.rlim_cur != RLIM_INFINITY && limit.rlim_cur < FD_SLOTS_MAX)
45 count = (size_t)limit.rlim_cur;
46
47 g_slots = mmap(NULL, count * sizeof(ot_fdslot), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
48 if (g_slots == MAP_FAILED) {
49 g_slots = NULL;
50 return -1;
51 }
52
53 g_slot_count = count;
54 return 0;
55}
56
57void fdstate_deinit(void) {
58 if (g_slots)
59 munmap(g_slots, g_slot_count * sizeof(ot_fdslot));
60 g_slots = NULL;
61 g_slot_count = 0;
62}
63
64size_t fdstate_slots(void) { return g_slot_count; }
65
66static ot_fdslot *fdstate_slot(int fd) {
67 if (fd < 0 || (size_t)fd >= g_slot_count)
68 return NULL;
69 return g_slots + fd;
70}
71
72uint32_t fdstate_open(int fd, void *cookie, ot_time expiry) {
73 ot_fdslot *slot = fdstate_slot(fd);
74 uint64_t word;
75
76 if (!slot)
77 return 0;
78
79 word = atomic_load_explicit(&slot->state, memory_order_relaxed);
80 atomic_store_explicit(&slot->cookie, cookie, memory_order_relaxed);
81 atomic_store_explicit(&slot->expiry, expiry, memory_order_relaxed);
82 /* Publishes the two above to whoever claims the slot next */
83 atomic_store_explicit(&slot->state, FD_WORD(FD_GEN(word), FD_IDLE), memory_order_release);
84
85 return FD_GEN(word);
86}
87
88int fdstate_claim(int fd, uint32_t generation) {
89 ot_fdslot *slot = fdstate_slot(fd);
90 uint64_t word;
91
92 if (!slot)
93 return 0;
94
95 word = atomic_load_explicit(&slot->state, memory_order_acquire);
96 for (;;) {
97 if (FD_GEN(word) != generation || FD_ST(word) != FD_IDLE)
98 return 0;
99 if (atomic_compare_exchange_weak_explicit(&slot->state, &word, FD_SET_ST(word, FD_BUSY), memory_order_acquire,
100 memory_order_relaxed))
101 return 1;
102 /* word now holds what was really there, so the test above runs again */
103 }
104}
105
106void fdstate_release(int fd) {
107 ot_fdslot *slot = fdstate_slot(fd);
108 uint64_t word;
109
110 if (!slot)
111 return;
112
113 word = atomic_load_explicit(&slot->state, memory_order_relaxed);
114 atomic_store_explicit(&slot->state, FD_SET_ST(word, FD_IDLE), memory_order_release);
115}
116
117int fdstate_claim_expired(int fd, ot_time now) {
118 ot_fdslot *slot = fdstate_slot(fd);
119 uint64_t word;
120 ot_time expiry;
121
122 if (!slot)
123 return 0;
124
125 /* Reading before asking for the slot keeps the common case, a descriptor
126 that has not expired, off the bus entirely */
127 word = atomic_load_explicit(&slot->state, memory_order_relaxed);
128 if (FD_ST(word) != FD_IDLE)
129 return 0;
130 expiry = atomic_load_explicit(&slot->expiry, memory_order_relaxed);
131 if (!expiry || expiry > now)
132 return 0;
133
134 /* Nothing can move the expiry while the slot sits at FD_IDLE, so winning
135 the slot is the last word */
136 return atomic_compare_exchange_strong_explicit(&slot->state, &word, FD_SET_ST(word, FD_CLOSING), memory_order_acquire,
137 memory_order_relaxed);
138}
139
140void fdstate_close(int fd) {
141 ot_fdslot *slot = fdstate_slot(fd);
142 uint64_t word;
143
144 if (!slot)
145 return;
146
147 word = atomic_load_explicit(&slot->state, memory_order_relaxed);
148 atomic_store_explicit(&slot->cookie, NULL, memory_order_relaxed);
149 atomic_store_explicit(&slot->expiry, 0, memory_order_relaxed);
150 atomic_store_explicit(&slot->state, FD_WORD(FD_GEN(word) + 1, FD_FREE), memory_order_release);
151}
152
153void *fdstate_cookie(int fd) {
154 ot_fdslot *slot = fdstate_slot(fd);
155
156 return slot ? atomic_load_explicit(&slot->cookie, memory_order_relaxed) : NULL;
157}
158
159void fdstate_set_expiry(int fd, ot_time expiry) {
160 ot_fdslot *slot = fdstate_slot(fd);
161
162 if (slot)
163 atomic_store_explicit(&slot->expiry, expiry, memory_order_relaxed);
164}
diff --git a/ot_fdstate.h b/ot_fdstate.h
new file mode 100644
index 0000000..2693a3b
--- /dev/null
+++ b/ot_fdstate.h
@@ -0,0 +1,52 @@
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_FDSTATE_H__
7#define OT_FDSTATE_H__
8
9#include <stdint.h>
10
11#include "trackerlogic.h"
12
13/* What the tracker knows about a file descriptor: who owns it, when it
14 should be given up on, and whether anyone is working on it right now.
15
16 A descriptor number says nothing about identity, since the kernel hands
17 the same number out again as soon as it is closed. Every slot therefore
18 carries a generation that is bumped when the descriptor is given up, and
19 every caller that was told about a descriptor has to say which generation
20 it was told about. A caller holding a stale generation is turned away
21 instead of being handed somebody else's connection.
22
23 The table is sized once from RLIMIT_NOFILE and never moves, so looking a
24 descriptor up needs no synchronisation at all. */
25
26int fdstate_init(void);
27void fdstate_deinit(void);
28size_t fdstate_slots(void);
29
30/* Take a freshly accepted descriptor into the table and return the
31 generation to quote when asking for it back. expiry of 0 never expires. */
32uint32_t fdstate_open(int fd, void *cookie, ot_time expiry);
33
34/* Ask for exclusive use. Returns 0 if the descriptor has been recycled since
35 generation was handed out, or if somebody else is using it. */
36int fdstate_claim(int fd, uint32_t generation);
37void fdstate_release(int fd);
38
39/* The cleaner's side of the same thing: takes the descriptor only if nobody
40 is working on it and it really has expired. Never waits. */
41int fdstate_claim_expired(int fd, ot_time now);
42
43/* Give the descriptor up. The generation is bumped here, which has to happen
44 before close(2) so that the kernel cannot reissue the number while events
45 quoting the old generation are still in flight. */
46void fdstate_close(int fd);
47
48/* Both only valid while the caller holds the descriptor. */
49void *fdstate_cookie(int fd);
50void fdstate_set_expiry(int fd, ot_time expiry);
51
52#endif
diff --git a/ot_http.c b/ot_http.c
index 4675efb..331a457 100644
--- a/ot_http.c
+++ b/ot_http.c
@@ -26,6 +26,7 @@
26#include "ot_mutex.h" 26#include "ot_mutex.h"
27#include "ot_scan.h" 27#include "ot_scan.h"
28#include "ot_stats.h" 28#include "ot_stats.h"
29#include "ot_timeout.h"
29#include "scan_urlencoded_query.h" 30#include "scan_urlencoded_query.h"
30#include "trackerlogic.h" 31#include "trackerlogic.h"
31 32
@@ -72,7 +73,6 @@ static void http_senddata(const int64_t sock, struct ot_workstruct *ws) {
72 73
73 if (written_size < ws->reply_size) { 74 if (written_size < ws->reply_size) {
74 char *outbuf; 75 char *outbuf;
75 tai6464 t;
76 76
77 if (!(outbuf = malloc(ws->reply_size - written_size))) { 77 if (!(outbuf = malloc(ws->reply_size - written_size))) {
78 buf_reset(&cookie->request); 78 buf_reset(&cookie->request);
@@ -97,8 +97,7 @@ static void http_senddata(const int64_t sock, struct ot_workstruct *ws) {
97 97
98 /* writeable short data sockets just have a tcp timeout */ 98 /* writeable short data sockets just have a tcp timeout */
99 if (!ws->keep_alive) { 99 if (!ws->keep_alive) {
100 taia_uint(&t, 0); 100 timeout_never(sock);
101 io_timeout(sock, t);
102 io_dontwantread(sock); 101 io_dontwantread(sock);
103 } 102 }
104 io_wantwrite(sock); 103 io_wantwrite(sock);
@@ -139,7 +138,6 @@ ssize_t http_sendiovecdata(const int64_t sock, struct ot_workstruct *ws, size_t
139 char *header; 138 char *header;
140 const char *encoding = ""; 139 const char *encoding = "";
141 size_t header_size, size = iovec_length(&iovec_entries, (const struct iovec **)&iovector); 140 size_t header_size, size = iovec_length(&iovec_entries, (const struct iovec **)&iovector);
142 tai6464 t;
143 141
144 /* No cookie? Bad socket. Leave. */ 142 /* No cookie? Bad socket. Leave. */
145 if (!cookie) { 143 if (!cookie) {
@@ -209,10 +207,7 @@ ssize_t http_sendiovecdata(const int64_t sock, struct ot_workstruct *ws, size_t
209 cookie->flag &= ~STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER; 207 cookie->flag &= ~STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER;
210 } 208 }
211 209
212 /* writeable sockets timeout after 10 minutes */ 210 timeout_in(sock, OT_CLIENT_TIMEOUT_SEND);
213 taia_now(&t);
214 taia_addsec(&t, &t, OT_CLIENT_TIMEOUT_SEND);
215 io_timeout(sock, t);
216 io_wantwrite(sock); 211 io_wantwrite(sock);
217 return 0; 212 return 0;
218} 213}
@@ -291,7 +286,6 @@ static ssize_t http_handle_stats(const int64_t sock, struct ot_workstruct *ws, c
291 } 286 }
292 287
293 if (mode == TASK_STATS_TPB) { 288 if (mode == TASK_STATS_TPB) {
294 tai6464 t;
295#ifdef WANT_COMPRESSION_GZIP 289#ifdef WANT_COMPRESSION_GZIP
296 ws->request[ws->request_size - 1] = 0; 290 ws->request[ws->request_size - 1] = 0;
297#ifndef WANT_COMPRESSION_GZIP_ALWAYS 291#ifndef WANT_COMPRESSION_GZIP_ALWAYS
@@ -307,8 +301,7 @@ static ssize_t http_handle_stats(const int64_t sock, struct ot_workstruct *ws, c
307 cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED; 301 cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED;
308 302
309 /* Clients waiting for us should not easily timeout */ 303 /* Clients waiting for us should not easily timeout */
310 taia_uint(&t, 0); 304 timeout_never(sock);
311 io_timeout(sock, t);
312 fullscrape_deliver(sock, format); 305 fullscrape_deliver(sock, format);
313 io_dontwantread(sock); 306 io_dontwantread(sock);
314 return ws->reply_size = -2; 307 return ws->reply_size = -2;
@@ -317,13 +310,11 @@ static ssize_t http_handle_stats(const int64_t sock, struct ot_workstruct *ws, c
317 310
318 /* default format for now */ 311 /* default format for now */
319 if ((mode & TASK_CLASS_MASK) == TASK_STATS) { 312 if ((mode & TASK_CLASS_MASK) == TASK_STATS) {
320 tai6464 t;
321 313
322 cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK; 314 cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK;
323 315
324 /* Complex stats also include expensive memory debugging tools */ 316 /* Complex stats also include expensive memory debugging tools */
325 taia_uint(&t, 0); 317 timeout_never(sock);
326 io_timeout(sock, t);
327 stats_deliver(sock, mode); 318 stats_deliver(sock, mode);
328 return ws->reply_size = -2; 319 return ws->reply_size = -2;
329 } 320 }
@@ -345,7 +336,6 @@ typedef struct {
345static ssize_t http_handle_fullscrape(const int64_t sock, struct ot_workstruct *ws) { 336static ssize_t http_handle_fullscrape(const int64_t sock, struct ot_workstruct *ws) {
346 struct http_data *cookie = io_getcookie(sock); 337 struct http_data *cookie = io_getcookie(sock);
347 int format = 0; 338 int format = 0;
348 tai6464 t;
349 339
350#ifdef WANT_MODEST_FULLSCRAPES 340#ifdef WANT_MODEST_FULLSCRAPES
351 { 341 {
@@ -404,8 +394,7 @@ static ssize_t http_handle_fullscrape(const int64_t sock, struct ot_workstruct *
404 /* Pass this task to the worker thread */ 394 /* Pass this task to the worker thread */
405 cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED; 395 cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED;
406 /* Clients waiting for us should not easily timeout */ 396 /* Clients waiting for us should not easily timeout */
407 taia_uint(&t, 0); 397 timeout_never(sock);
408 io_timeout(sock, t);
409 fullscrape_deliver(sock, format); 398 fullscrape_deliver(sock, format);
410 io_dontwantread(sock); 399 io_dontwantread(sock);
411 return ws->reply_size = -2; 400 return ws->reply_size = -2;
diff --git a/ot_timeout.h b/ot_timeout.h
new file mode 100644
index 0000000..1027c2e
--- /dev/null
+++ b/ot_timeout.h
@@ -0,0 +1,39 @@
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_TIMEOUT_H__
7#define OT_TIMEOUT_H__
8
9#include <stdint.h>
10#include <string.h>
11
12/* Libowfat */
13#include "io.h"
14
15#include "trackerlogic.h"
16
17/* io_timeout wants an absolute TAI64 deadline and io_timeouted skips any
18 socket whose deadline is zero, so that is how a socket is told to wait
19 indefinitely.
20
21 The deadline is taken from the cached clock rather than from
22 gettimeofday, which is too expensive to call at this rate on FreeBSD. */
23
24static inline void timeout_never(int64_t sock) {
25 tai6464 t;
26
27 memset(&t, 0, sizeof(t));
28 io_timeout(sock, t);
29}
30
31static inline void timeout_in(int64_t sock, unsigned int seconds) {
32 tai6464 t;
33
34 memset(&t, 0, sizeof(t));
35 tai_unix(&t.sec, g_now_seconds + seconds);
36 io_timeout(sock, t);
37}
38
39#endif
diff --git a/tests/runner.c b/tests/runner.c
index 148b928..335d83d 100644
--- a/tests/runner.c
+++ b/tests/runner.c
@@ -25,6 +25,7 @@ static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urle
25 {"ot_socket", test_socket}, 25 {"ot_socket", test_socket},
26 {"ot_mutex", test_mutex}, 26 {"ot_mutex", test_mutex},
27 {"ot_batch", test_batch}, 27 {"ot_batch", test_batch},
28 {"ot_fdstate", test_fdstate},
28 {"ot_accesslist", test_accesslist}, 29 {"ot_accesslist", test_accesslist},
29 {NULL, NULL}}; 30 {NULL, NULL}};
30 31
diff --git a/tests/t_fdstate.c b/tests/t_fdstate.c
new file mode 100644
index 0000000..bf8724a
--- /dev/null
+++ b/tests/t_fdstate.c
@@ -0,0 +1,224 @@
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 <pthread.h>
8#include <stdatomic.h>
9#include <stdint.h>
10#include <stdlib.h>
11#include <string.h>
12
13/* Opentracker */
14#include "ot_fdstate.h"
15#include "test.h"
16
17/* ------------------------------------------------------- one thread first */
18
19static void case_open_claim_release(void) {
20 char cookie[4];
21 uint32_t generation;
22
23 test_case("a descriptor taken into the table can be claimed and gives back its cookie");
24 generation = fdstate_open(7, cookie, 0);
25 CHECK_INT(fdstate_claim(7, generation), 1);
26 CHECK_PTR(fdstate_cookie(7), cookie);
27 fdstate_release(7);
28
29 test_case("and can be claimed again once released");
30 CHECK_INT(fdstate_claim(7, generation), 1);
31 fdstate_release(7);
32 fdstate_close(7);
33}
34
35static void case_one_holder_at_a_time(void) {
36 uint32_t generation = fdstate_open(8, NULL, 0);
37
38 test_case("a descriptor somebody holds cannot be claimed again");
39 CHECK_INT(fdstate_claim(8, generation), 1);
40 CHECK_INT(fdstate_claim(8, generation), 0);
41 fdstate_release(8);
42 CHECK_INT(fdstate_claim(8, generation), 1);
43 fdstate_release(8);
44 fdstate_close(8);
45}
46
47static void case_stale_generation_is_refused(void) {
48 char first[4], second[4];
49 uint32_t old, new;
50
51 test_case("a caller quoting the generation of a closed descriptor is turned away");
52 old = fdstate_open(9, first, 0);
53 fdstate_close(9);
54 new = fdstate_open(9, second, 0);
55 CHECK(old != new);
56 CHECK_INT(fdstate_claim(9, old), 0);
57
58 test_case("while the one quoting the new generation gets the new cookie");
59 CHECK_INT(fdstate_claim(9, new), 1);
60 CHECK_PTR(fdstate_cookie(9), second);
61 fdstate_release(9);
62 fdstate_close(9);
63}
64
65static void case_expiry(void) {
66 uint32_t generation = fdstate_open(10, NULL, 100);
67
68 test_case("a descriptor past its expiry is collected");
69 CHECK_INT(fdstate_claim_expired(10, 99), 0);
70 CHECK_INT(fdstate_claim_expired(10, 100), 1);
71 fdstate_close(10);
72
73 test_case("one that never expires is never collected");
74 generation = fdstate_open(10, NULL, 0);
75 CHECK_INT(fdstate_claim_expired(10, 1 << 30), 0);
76
77 test_case("and one somebody is working on is left alone");
78 fdstate_set_expiry(10, 1);
79 CHECK_INT(fdstate_claim(10, generation), 1);
80 CHECK_INT(fdstate_claim_expired(10, 1 << 30), 0);
81 fdstate_release(10);
82 CHECK_INT(fdstate_claim_expired(10, 1 << 30), 1);
83 fdstate_close(10);
84}
85
86static void case_out_of_range(void) {
87 test_case("a descriptor outside the table is refused rather than trusted");
88 CHECK_INT(fdstate_claim(-1, 0), 0);
89 CHECK_INT(fdstate_claim((int)fdstate_slots(), 0), 0);
90 CHECK_INT(fdstate_claim_expired(-1, 0), 0);
91 CHECK_PTR(fdstate_cookie(-1), NULL);
92}
93
94/* --------------------------------------------------------- then in anger */
95
96/* The race this is about only exists while a descriptor is being recycled,
97 so recycling is driven by work done rather than by a clock: every slot is
98 given up after RECYCLE_AFTER claims, whatever the machine's speed. */
99
100#define STRESS_SLOTS 64
101#define RECYCLE_AFTER 32
102#define STRESS_CLAIMS 200000
103#define STRESS_GIVEUP (STRESS_CLAIMS * 100)
104
105static _Atomic long stress_claims, stress_recycles, stress_violations, stress_attempts;
106static _Atomic int stress_lives[STRESS_SLOTS];
107static _Atomic int stress_live[STRESS_SLOTS];
108
109/* Generation and cookie are published in one word so a worker can never read
110 a cookie that belongs to a different generation than the one it claimed
111 with. Each connection gets a cookie nobody else ever gets. */
112static _Atomic uint64_t stress_conn[STRESS_SLOTS];
113static _Atomic uint64_t stress_next_cookie;
114
115#define STRESS_CONN(cookie, gen) (((uint64_t)(cookie) << 32) | (uint32_t)(gen))
116#define STRESS_CONN_COOKIE(w) ((void *)(uintptr_t)((w) >> 32))
117#define STRESS_CONN_GEN(w) ((uint32_t)(w))
118
119static int stress_running(void) {
120 return atomic_load(&stress_claims) < STRESS_CLAIMS && atomic_load(&stress_attempts) < STRESS_GIVEUP;
121}
122
123static void *stress_worker(void *arg) {
124 unsigned seed = (unsigned)(uintptr_t)arg;
125
126 while (stress_running()) {
127 int fd = (int)(rand_r(&seed) % STRESS_SLOTS);
128 uint64_t conn = atomic_load(&stress_conn[fd]);
129
130 atomic_fetch_add(&stress_attempts, 1);
131 if (!fdstate_claim(fd, STRESS_CONN_GEN(conn)))
132 continue;
133
134 if (fdstate_cookie(fd) != STRESS_CONN_COOKIE(conn))
135 atomic_fetch_add(&stress_violations, 1);
136 if (atomic_fetch_sub(&stress_lives[fd], 1) <= 1)
137 fdstate_set_expiry(fd, 1); /* retired by its holder, as a handler would */
138 if (fdstate_cookie(fd) != STRESS_CONN_COOKIE(conn))
139 atomic_fetch_add(&stress_violations, 1);
140
141 fdstate_release(fd);
142 atomic_fetch_add(&stress_claims, 1);
143 }
144 return NULL;
145}
146
147static void *stress_keeper(void *arg) {
148 (void)arg;
149 while (stress_running()) {
150 int fd;
151
152 for (fd = 0; fd < STRESS_SLOTS; ++fd) {
153 if (atomic_load(&stress_live[fd])) {
154 if (!fdstate_claim_expired(fd, 1))
155 continue; /* a worker has it, come back */
156 fdstate_close(fd);
157 atomic_store(&stress_live[fd], 0);
158 atomic_fetch_add(&stress_recycles, 1);
159 }
160 {
161 void *cookie = (void *)(uintptr_t)(atomic_fetch_add(&stress_next_cookie, 1) + 1);
162 uint32_t gen = fdstate_open(fd, cookie, 0);
163
164 atomic_store(&stress_lives[fd], RECYCLE_AFTER);
165 atomic_store(&stress_conn[fd], STRESS_CONN((uintptr_t)cookie, gen));
166 atomic_store(&stress_live[fd], 1);
167 }
168 }
169 }
170 return NULL;
171}
172
173static void case_recycling_under_load(void) {
174 pthread_t worker[4], keeper;
175 size_t i;
176
177 test_case("no worker is ever handed a connection from another generation");
178 atomic_store(&stress_claims, 0);
179 atomic_store(&stress_recycles, 0);
180 atomic_store(&stress_violations, 0);
181 atomic_store(&stress_attempts, 0);
182 atomic_store(&stress_next_cookie, 0);
183 /* Every slot is opened and published before any worker runs: a worker only
184 ever learns of a descriptor from whoever registered it, and never
185 invents a generation of its own. */
186 for (i = 0; i < STRESS_SLOTS; ++i) {
187 void *cookie = (void *)(uintptr_t)(atomic_fetch_add(&stress_next_cookie, 1) + 1);
188 uint32_t gen = fdstate_open((int)i, cookie, 0);
189
190 atomic_store(&stress_lives[i], RECYCLE_AFTER);
191 atomic_store(&stress_conn[i], STRESS_CONN((uintptr_t)cookie, gen));
192 atomic_store(&stress_live[i], 1);
193 }
194
195 pthread_create(&keeper, NULL, stress_keeper, NULL);
196 for (i = 0; i < sizeof(worker) / sizeof(worker[0]); ++i)
197 pthread_create(&worker[i], NULL, stress_worker, (void *)(uintptr_t)(i + 1));
198 for (i = 0; i < sizeof(worker) / sizeof(worker[0]); ++i)
199 pthread_join(worker[i], NULL);
200 pthread_join(keeper, NULL);
201
202 CHECK_INT(atomic_load(&stress_violations), 0);
203
204 test_case("and the run actually recycled descriptors, or it proved nothing");
205 CHECK(atomic_load(&stress_claims) >= STRESS_CLAIMS);
206 CHECK(atomic_load(&stress_recycles) > STRESS_CLAIMS / (RECYCLE_AFTER * 4));
207
208 for (i = 0; i < STRESS_SLOTS; ++i)
209 fdstate_close((int)i);
210}
211
212void test_fdstate(void) {
213 CHECK_INT(fdstate_init(), 0);
214 CHECK(fdstate_slots() > STRESS_SLOTS);
215
216 case_open_claim_release();
217 case_one_holder_at_a_time();
218 case_stale_generation_is_refused();
219 case_expiry();
220 case_out_of_range();
221 case_recycling_under_load();
222
223 fdstate_deinit();
224}
diff --git a/tests/test.h b/tests/test.h
index fd574c2..bc8c3bd 100644
--- a/tests/test.h
+++ b/tests/test.h
@@ -88,5 +88,6 @@ void test_ip(void);
88void test_socket(void); 88void test_socket(void);
89void test_mutex(void); 89void test_mutex(void);
90void test_batch(void); 90void test_batch(void);
91void test_fdstate(void);
91 92
92#endif 93#endif