summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--DESIGN99
-rw-r--r--Makefile4
-rw-r--r--opentracker.c127
-rw-r--r--ot_config.c26
-rw-r--r--ot_event.c9
-rw-r--r--ot_event.h5
-rw-r--r--ot_fdstate.c7
-rw-r--r--ot_fdstate.h3
-rw-r--r--ot_http.c29
-rw-r--r--ot_http.h7
-rw-r--r--ot_tcp.c157
-rw-r--r--ot_tcp.h22
-rw-r--r--tests/integration/test_config.py29
-rw-r--r--tests/t_event.c32
14 files changed, 421 insertions, 135 deletions
diff --git a/DESIGN b/DESIGN
new file mode 100644
index 0000000..ff37706
--- /dev/null
+++ b/DESIGN
@@ -0,0 +1,99 @@
1opentracker design notes
2========================
3
4Decisions that cost measurement to reach, and the alternatives that were
5tried and rejected.
6
7What the tracker is optimised for
8---------------------------------
9
10UDP carries most of the traffic. An HTTP request costs about ten times a UDP
11announce, 26 microseconds of tracker CPU against 2.5. Nearly all of that is
12the kernel setting up and tearing down a connection rather than tracker
13code.
14
15A connection carries one request and closes. Keep-alive works and is rarely
16asked for.
17
18Statistics are approximate on purpose. Counters are per thread and summed
19when read, so a /stats read can under-report once. Nothing drifts.
20
21
22Decisions
23---------
24
25One UDP socket per worker, where the kernel spreads datagrams over a group
26bound to one address. Cost per datagram then stays flat as workers are added,
270.71 to 0.72 microseconds from one worker to eight. Sharing one socket grows
28it eightfold over the same range, 0.71 to 6.07.
29
30listen.udp.workers defaults to 1. One worker beats none by 19 to 37 per cent
31everywhere, because a blocking read costs one system call where the event
32loop costs two. Beyond one the gain depends on the kernel balancing, so
33raising it is the operator's decision.
34
35A thread parked in a blocking read is woken by a signal, not by closing or
36shutting 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
44Only the signal works everywhere. A receive timeout also works, but wakes
45every thread on every tick for the life of the process to catch something
46that happens once: at eight workers it doubled idle context switches, 255 to
47495 over thirty seconds.
48
49Each binary is built in one compiler invocation. Without object files a build
50cannot link objects compiled with different FEATURES, which was a real
51failure mode. It costs incremental rebuilds, 0.075 seconds becoming 1.56.
52
53A socket is read or written, never both. http_interest_set replaces an
54add-and-drop pair so that the invariant cannot be broken by accident, and the
55event layer hears about a change once instead of twice.
56
57Accept waits until the client's request has arrived: TCP_DEFER_ACCEPT on
58Linux, the accf_data filter on FreeBSD. Measured against a flood of silent
59connections, the filter serves more requests than no filter, not fewer, so it
60is not the denial of service vector that FreeBSD-SA-02:26 once described.
61
62
63Paths not taken
64---------------
65
66accf_http, the HTTP-aware accept filter. It duplicates a parser the tracker
67has anyway, puts more kernel code in the path, and holds a connection that
68sends "GET /" and stops, which accf_data hands straight over.
69
70recvmmsg and sendmmsg. No measurable gain on either FreeBSD machine, between
71-4 and +10 per cent, and they do not exist on Darwin.
72
73Link time optimisation. It cuts .text by 13 per cent, 93,497 bytes to 81,630,
74and changes throughput not at all. A tracker waits on memory and system
75calls, not on instruction fetch: the announce path is 6.6 KB of code against
76a 32 KB instruction cache at worst.
77
78A directory for object files. bmake has no pattern rules and the Makefile
79builds under FreeBSD's make today. Building each binary in one invocation
80clears the objects out of the tree without giving that up.
81
82A src/ directory. 52 files sharing an ot_ prefix are a module list rather
83than clutter, and build output was most of what made the tree look crowded.
84
85
86Platform differences that cost us code
87--------------------------------------
88
89SO_REUSEPORT means three different things. Linux spreads datagrams over the
90group; FreeBSD permits the bind and needs SO_REUSEPORT_LB to spread. Darwin
91hands every datagram to whichever socket bound last, so a group there starves
92all but one reader.
93
94Waking a blocked reader differs per kernel. see the table above.
95
96Deferring accept is TCP_DEFER_ACCEPT, or accf_data, or nothing.
97
98make is bmake on FreeBSD and GNU make elsewhere, and the Makefile has to
99build under both.
diff --git a/Makefile b/Makefile
index 2e52367..8f4dcf6 100644
--- a/Makefile
+++ b/Makefile
@@ -69,8 +69,8 @@ CFLAGS_libowfat=-I$(LIBOWFAT_HEADERS)
69LDFLAGS_libowfat=-L$(LIBOWFAT_LIBRARY) -lowfat 69LDFLAGS_libowfat=-L$(LIBOWFAT_LIBRARY) -lowfat
70 70
71BINARY =opentracker 71BINARY =opentracker
72HEADERS=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 72HEADERS=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
73SOURCES=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 73SOURCES=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
74SOURCES_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 74SOURCES_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
76all: $(BINARY) $(BINARY).debug 76all: $(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
99static 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
118static 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
162static 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
193static 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;
32static ot_listen *g_listen; 33static ot_listen *g_listen;
33static size_t g_listen_count; 34static size_t g_listen_count;
34static int g_listen_shared; 35static int g_listen_shared;
35static int g_listen_workers = 1; 36static int g_listen_workers_udp = 1;
37static int g_listen_workers_tcp = 0;
36 38
37static void panic(const char *routine) __attribute__((noreturn)); 39static void panic(const char *routine) __attribute__((noreturn));
38static void panic(const char *routine) { 40static 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
diff --git a/ot_event.c b/ot_event.c
index 8f55817..61cc07b 100644
--- a/ot_event.c
+++ b/ot_event.c
@@ -40,6 +40,9 @@ static struct epoll_event *g_ready;
40static int g_ready_space; 40static int g_ready_space;
41 41
42const char *event_mechanism(void) { return "epoll"; } 42const 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 */
45int event_can_share(void) { return 1; }
43 46
44int event_init(void) { 47int event_init(void) {
45 g_ready = NULL; 48 g_ready = NULL;
@@ -118,6 +121,8 @@ static struct kevent *g_ready;
118static int g_ready_space; 121static int g_ready_space;
119 122
120const char *event_mechanism(void) { return "kqueue"; } 123const char *event_mechanism(void) { return "kqueue"; }
124/* The set lives in the kernel, and kevent may be called from any thread */
125int event_can_share(void) { return 1; }
121 126
122int event_init(void) { 127int 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
174int event_add(int fd, int what, uint32_t generation) { return event_change(fd, what, generation, 0); } 179int event_add(int fd, int what, uint32_t generation) { return what ? event_change(fd, what, generation, 0) : 0; }
175int event_modify(int fd, int what, uint32_t generation) { return event_change(fd, what, generation, 0); } 180int event_modify(int fd, int what, uint32_t generation) { return event_change(fd, what, generation, 0); }
176int event_del(int fd) { return event_change(fd, 0, 0, 1); } 181int event_del(int fd) { return event_change(fd, 0, 0, 1); }
177 182
@@ -229,6 +234,8 @@ static int *g_where;
229static int g_count, g_space, g_where_space; 234static int g_count, g_space, g_where_space;
230 235
231const char *event_mechanism(void) { return "poll"; } 236const char *event_mechanism(void) { return "poll"; }
237/* The arrays below are ours and nothing guards them */
238int event_can_share(void) { return 0; }
232 239
233int event_init(void) { 240int event_init(void) {
234 g_pfds = NULL, g_generation = NULL, g_where = NULL; 241 g_pfds = NULL, g_generation = NULL, g_where = NULL;
diff --git a/ot_event.h b/ot_event.h
index aa007d0..efdefff 100644
--- a/ot_event.h
+++ b/ot_event.h
@@ -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. */
39int event_wait(ot_event *out, int max, int timeout_ms); 39int 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. */
44int 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 */
42const char *event_mechanism(void); 47const 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
189void 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
189void fdstate_set_expiry(int fd, ot_time expiry) { 196void 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. */
58void *fdstate_cookie(int fd); 58void *fdstate_cookie(int fd);
59
60/* Hand the slot a different cookie, for a connection whose state has moved */
61void fdstate_set_cookie(int fd, void *cookie);
59void fdstate_set_expiry(int fd, ot_time expiry); 62void fdstate_set_expiry(int fd, ot_time expiry);
60 63
61#endif 64#endif
diff --git a/ot_http.c b/ot_http.c
index 66f0102..bb6ff24 100644
--- a/ot_http.c
+++ b/ot_http.c
@@ -39,9 +39,25 @@ ssize_t g_stats_path_len;
39 39
40enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 }; 40enum { 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. 42int 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. */
45void http_close(const int64_t sock) { 61void 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
diff --git a/ot_http.h b/ot_http.h
index ba16de4..82d645b 100644
--- a/ot_http.h
+++ b/ot_http.h
@@ -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
28struct http_data { 29struct 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 */
40void http_close(const int64_t s); 41void 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. */
45int http_adopt(const int64_t s, struct http_data *cookie);
46
42ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws); 47ssize_t http_handle_request(const int64_t s, struct ot_workstruct *ws);
43ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial); 48ssize_t http_sendiovecdata(const int64_t s, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial);
44ssize_t http_issue_error(const int64_t s, struct ot_workstruct *ws, int code); 49ssize_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
23static 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
42void 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
86void 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 */
123void 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 */
16void tcp_accept(const int64_t serversocket, struct ot_workstruct *ws);
17
18/* Handler connections in the event loop has become readable or writable */
19void tcp_read(const int64_t sock, struct ot_workstruct *ws);
20void 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
3An unrecognised key makes opentracker refuse to start, so a test that it
4comes up at all is a test that the key was understood.
5"""
6
7import unittest
8
9from harness import Tracker, TrackerTestCase, free_port
10
11
12class 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
19class 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. */
248static 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. */
269static 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
244void test_event(void) { 274void 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}