summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-11-02 20:39:26 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-11-02 20:39:26 +0100
commit8f520e52d7d4081a0835374c7a448116baac64ab (patch)
treefdb3c2e8d49aa8c94d6fff357ef2d18f3e0e553c
parent7419a9524754dfc4222de2c4e4c22808b571477d (diff)
Turn the bucket count into a knob, but keep cleaner wake-ups at a constant rate
-rw-r--r--DESIGN37
-rw-r--r--Makefile6
-rw-r--r--ot_clean.c38
-rw-r--r--ot_clean.h13
-rw-r--r--tests/t_mutex.c2
-rw-r--r--trackerlogic.h5
6 files changed, 82 insertions, 19 deletions
diff --git a/DESIGN b/DESIGN
index ff37706..5abeb4a 100644
--- a/DESIGN
+++ b/DESIGN
@@ -16,7 +16,7 @@ A connection carries one request and closes. Keep-alive works and is rarely
16asked for. 16asked for.
17 17
18Statistics are approximate on purpose. Counters are per thread and summed 18Statistics are approximate on purpose. Counters are per thread and summed
19when read, so a /stats read can under-report once. Nothing drifts. 19when read, so a /stats read can under-report once.
20 20
21 21
22Decisions 22Decisions
@@ -60,9 +60,44 @@ connections, 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. 60is not the denial of service vector that FreeBSD-SA-02:26 once described.
61 61
62 62
63The torrent table is split into 2^OT_BUCKET_COUNT_BITS buckets, 1024 by
64default, set from the Makefile. Inserting a torrent opens a slot in a sorted
65vector, so the bytes moved grow with the bucket's occupancy. Insert cost per
66torrent, Linux:
67
68 2^10 2^12 2^14 2^16
69 200k 466 ns 439 ns
70 1M 834 ns 579 ns 529 ns
71 4M 2348 ns 1163 ns 770 ns 674 ns
72
73Re-announcing an existing torrent moves by under ten per cent over the same
74range, so the cost is paid while the torrent population is built and not in
75steady state. Below a few hundred thousand torrents there is nothing to win.
762^14 captures most of the gain for two per cent more resident memory.
77
78The cleaner's wake rate does not follow the bucket count. A cap of ten wakes
79a second sets the sleep, and a wake cleans as many buckets as it must to
80finish a sweep in OT_CLEAN_INTERVAL_MINUTES. At the default bucket count that
81is one bucket every 117 ms.
82
83
63Paths not taken 84Paths not taken
64--------------- 85---------------
65 86
87A keyed hash for the bucket index. Info hashes are SHA-1 output and already
88uniform, so it changes no distribution. It would buy resistance to deliberate
89bucket flooding and cost the ordering a full scrape depends on: buckets are
90ordered by hash prefix and sorted within, so walking them emits the sorted
91dictionary bencode wants, with no sort and no copy.
92
93Cache line padding for the per-bucket locks and vector headers. The
94contention it was meant to remove is not there. On glibc, where a 48-byte
95mutex straddles a line, padding gains 3 to 8 per cent, but gains as much at
96one thread where no false sharing can exist, and scaling from one to eight
97threads is identical either way. On FreeBSD, where eight 8-byte mutexes share
98a line and the risk should be highest, padding is 15 per cent slower at one
99thread and neutral to 6 per cent slower at four and eight.
100
66accf_http, the HTTP-aware accept filter. It duplicates a parser the tracker 101accf_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 102has anyway, puts more kernel code in the path, and holds a connection that
68sends "GET /" and stops, which accf_data hands straight over. 103sends "GET /" and stops, which accf_data hands straight over.
diff --git a/Makefile b/Makefile
index f124be8..993f0ae 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,12 @@ STRIP?=strip
20#FEATURES+=-DWANT_DEV_RANDOM 20#FEATURES+=-DWANT_DEV_RANDOM
21FEATURES+=-DWANT_FULLSCRAPE 21FEATURES+=-DWANT_FULLSCRAPE
22 22
23# Buckets the torrent table is split into, as a power of two, 8 to 16. More
24# buckets make inserting a new torrent cheaper and cost memory, which is worth
25# it for a tracker carrying millions of torrents and not for one carrying
26# thousands. DESIGN has the measurements.
27FEATURES+=-DOT_BUCKET_COUNT_BITS=10
28
23#FEATURES+=-D_DEBUG_HTTPERROR 29#FEATURES+=-D_DEBUG_HTTPERROR
24#FEATURES+=-D_DEBUG_RANDOMTORRENTS 30#FEATURES+=-D_DEBUG_RANDOMTORRENTS
25 31
diff --git a/ot_clean.c b/ot_clean.c
index e1d1971..f181e83 100644
--- a/ot_clean.c
+++ b/ot_clean.c
@@ -125,26 +125,34 @@ static void clean_sleep(void) {
125 pthread_mutex_unlock(&g_clean_mutex); 125 pthread_mutex_unlock(&g_clean_mutex);
126} 126}
127 127
128/* Clean up all peers in current bucket, remove timedout pools and 128/* Clean up all peers in one bucket, remove timedout pools and torrents */
129 torrents */ 129static void clean_bucket(size_t bucket) {
130 ot_vector *torrents_list = mutex_bucket_lock((int)bucket);
131 size_t toffs;
132 int delta_torrentcount = 0;
133
134 for (toffs = 0; toffs < torrents_list->size; ++toffs) {
135 ot_torrent *torrent = ((ot_torrent *)(torrents_list->data)) + toffs;
136 if (clean_single_torrent(torrent)) {
137 vector_remove_torrent(torrents_list, torrent);
138 --delta_torrentcount;
139 --toffs;
140 }
141 }
142 mutex_bucket_unlock((int)bucket, delta_torrentcount);
143}
144
130static void *clean_worker(void *args) { 145static void *clean_worker(void *args) {
131 (void)args; 146 (void)args;
132 while (1) { 147 while (1) {
133 size_t bucket = OT_BUCKET_COUNT; 148 size_t bucket = OT_BUCKET_COUNT;
134 while (bucket--) { 149 while (bucket) {
135 ot_vector *torrents_list = mutex_bucket_lock(bucket); 150 size_t todo = bucket < OT_CLEAN_BUCKETS_PER_WAKE ? bucket : OT_CLEAN_BUCKETS_PER_WAKE;
136 size_t toffs; 151
137 int delta_torrentcount = 0; 152 /* One bucket is locked at a time, however many a wake gets through */
153 while (todo--)
154 clean_bucket(--bucket);
138 155
139 for (toffs = 0; toffs < torrents_list->size; ++toffs) {
140 ot_torrent *torrent = ((ot_torrent *)(torrents_list->data)) + toffs;
141 if (clean_single_torrent(torrent)) {
142 vector_remove_torrent(torrents_list, torrent);
143 --delta_torrentcount;
144 --toffs;
145 }
146 }
147 mutex_bucket_unlock(bucket, delta_torrentcount);
148 if (!g_opentracker_running) 156 if (!g_opentracker_running)
149 return NULL; 157 return NULL;
150 clean_sleep(); 158 clean_sleep();
diff --git a/ot_clean.h b/ot_clean.h
index e069ee0..0592791 100644
--- a/ot_clean.h
+++ b/ot_clean.h
@@ -9,8 +9,17 @@
9/* The amount of time a clean cycle should take */ 9/* The amount of time a clean cycle should take */
10#define OT_CLEAN_INTERVAL_MINUTES 2 10#define OT_CLEAN_INTERVAL_MINUTES 2
11 11
12/* So after each bucket wait 1 / OT_BUCKET_COUNT intervals */ 12/* How often the cleaner may wake. A full sweep takes OT_CLEAN_INTERVAL_MINUTES.
13#define OT_CLEAN_SLEEP (((OT_CLEAN_INTERVAL_MINUTES) * 60 * 1000000) / (OT_BUCKET_COUNT)) 13 The clean operations per wake is goverend by OT_BUCKET_COUNT_BITS,
14 set by the Makefile.
15 At the default of 1024 buckets this works out at one bucket every
16 117 ms. */
17#define OT_CLEAN_MAX_WAKES_PER_SECOND 10
18
19#define OT_CLEAN_WAKE_BUDGET ((OT_CLEAN_INTERVAL_MINUTES) * 60 * (OT_CLEAN_MAX_WAKES_PER_SECOND))
20#define OT_CLEAN_BUCKETS_PER_WAKE (((OT_BUCKET_COUNT) + (OT_CLEAN_WAKE_BUDGET) - 1) / (OT_CLEAN_WAKE_BUDGET))
21#define OT_CLEAN_WAKES ((OT_BUCKET_COUNT) / (OT_CLEAN_BUCKETS_PER_WAKE))
22#define OT_CLEAN_SLEEP (((OT_CLEAN_INTERVAL_MINUTES) * 60 * 1000000) / (OT_CLEAN_WAKES))
14 23
15void clean_init(void); 24void clean_init(void);
16void clean_deinit(void); 25void clean_deinit(void);
diff --git a/tests/t_mutex.c b/tests/t_mutex.c
index e54e2fa..c0c41cc 100644
--- a/tests/t_mutex.c
+++ b/tests/t_mutex.c
@@ -24,7 +24,7 @@ static void hash_for_bucket(ot_hash hash, int bucket) {
24} 24}
25 25
26static void case_a_hash_names_its_bucket(void) { 26static void case_a_hash_names_its_bucket(void) {
27 static const int buckets[] = {0, 1, 2, 17, 511, 512, OT_BUCKET_COUNT - 1}; 27 static const int buckets[] = {0, 1, 2, 17, OT_BUCKET_COUNT / 2 - 1, OT_BUCKET_COUNT / 2, OT_BUCKET_COUNT - 1};
28 size_t i; 28 size_t i;
29 29
30 test_case("a hash is served by the bucket its leading bits name"); 30 test_case("a hash is served by the bucket its leading bits name");
diff --git a/trackerlogic.h b/trackerlogic.h
index 338e4e6..7292d36 100644
--- a/trackerlogic.h
+++ b/trackerlogic.h
@@ -62,7 +62,12 @@ typedef struct {
62 62
63/* We maintain a list of 1024 pointers to sorted list of ot_torrent structs 63/* We maintain a list of 1024 pointers to sorted list of ot_torrent structs
64 Sort key is, of course, its hash */ 64 Sort key is, of course, its hash */
65/* Set from the Makefile, 8 to 16. More buckets means shorter vectors and a
66 cheaper insert, which only pays for a tracker carrying millions of
67 torrents; see DESIGN. */
68#ifndef OT_BUCKET_COUNT_BITS
65#define OT_BUCKET_COUNT_BITS 10 69#define OT_BUCKET_COUNT_BITS 10
70#endif
66 71
67#define OT_BUCKET_COUNT (1 << OT_BUCKET_COUNT_BITS) 72#define OT_BUCKET_COUNT (1 << OT_BUCKET_COUNT_BITS)
68#define OT_BUCKET_COUNT_SHIFT (32 - OT_BUCKET_COUNT_BITS) 73#define OT_BUCKET_COUNT_SHIFT (32 - OT_BUCKET_COUNT_BITS)