From d620e4c3ea42b0df6eb5e8dab92d7a8f031fa617 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Thu, 5 Nov 2026 13:59:14 +0100 Subject: Take the clock off the wall --- src/ot_fullscrape.c | 2 +- src/trackerlogic.c | 15 +++++++--- src/trackerlogic.h | 12 ++++---- tests/integration/harness.py | 5 ++-- tests/integration/test_fullscrape.py | 12 ++++---- tests/integration/test_statefile.py | 53 ++++++++++++++++++++++++++++++++++++ 6 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 tests/integration/test_statefile.py diff --git a/src/ot_fullscrape.c b/src/ot_fullscrape.c index 895d7d2..85be65d 100644 --- a/src/ot_fullscrape.c +++ b/src/ot_fullscrape.c @@ -131,7 +131,7 @@ static void fullscrape_write_one( ot_tasktype mode, char **cursor, ot_torrent *t case TASK_FULLSCRAPE_TRACKERSTATE: hex_from_hash(r, *hash); r += 2 * sizeof(ot_hash); - fmt_append(&r, r_end, ":%zd:%zd\n", torrent->peer_list6->base, down_count); + fmt_append(&r, r_end, ":%zd:%zd\n", (ssize_t)time_since_minutes(torrent->peer_list6->base), down_count); break; } *cursor = r; diff --git a/src/trackerlogic.c b/src/trackerlogic.c index 8e30a48..60d5b18 100644 --- a/src/trackerlogic.c +++ b/src/trackerlogic.c @@ -38,10 +38,17 @@ void free_peerlist(ot_peerlist *peer_list) { free(peer_list); } -void add_torrent_from_saved_state(ot_hash const hash, ot_time base, size_t down_count) { +/* How many minutes before this moment the torrent was last touched, which is + what a state dump records. */ +void add_torrent_from_saved_state(ot_hash const hash, ot_time age, size_t down_count) { int exactmatch; ot_torrent *torrent; - ot_vector *torrents_list = mutex_bucket_lock_by_hash(hash); + ot_vector *torrents_list; + + if (age > OT_TORRENT_TIMEOUT) + return; + + torrents_list = mutex_bucket_lock_by_hash(hash); if (!accesslist_hashisvalid(hash)) { mutex_bucket_unlock_by_hash(hash, 0); @@ -66,8 +73,8 @@ void add_torrent_from_saved_state(ot_hash const hash, ot_time base, size_t down_ memset(torrent->peer_list6, 0, sizeof(ot_peerlist)); memset(torrent->peer_list4, 0, sizeof(ot_peerlist)); - torrent->peer_list6->base = base; - torrent->peer_list4->base = base; + torrent->peer_list6->base = time_now_minutes() - age; + torrent->peer_list4->base = time_now_minutes() - age; torrent->peer_list6->down_count = down_count; torrent->peer_list4->down_count = down_count; diff --git a/src/trackerlogic.h b/src/trackerlogic.h index c8d859c..f6aa8cc 100644 --- a/src/trackerlogic.h +++ b/src/trackerlogic.h @@ -76,12 +76,12 @@ typedef struct { on startup */ #define RANDOMTORRENTS (1024 * 1024 * 1) -#if defined(CLOCK_REALTIME_COARSE) -#define OT_CLOCK_CHEAP CLOCK_REALTIME_COARSE /* Linux */ -#elif defined(CLOCK_REALTIME_FAST) -#define OT_CLOCK_CHEAP CLOCK_REALTIME_FAST /* the BSDs */ +#if defined(CLOCK_MONOTONIC_COARSE) +#define OT_CLOCK_CHEAP CLOCK_MONOTONIC_COARSE /* Linux and FreeBSD */ +#elif defined(CLOCK_MONOTONIC_FAST) +#define OT_CLOCK_CHEAP CLOCK_MONOTONIC_FAST /* the other BSDs */ #else -#define OT_CLOCK_CHEAP CLOCK_REALTIME /* macOS and whatever else */ +#define OT_CLOCK_CHEAP CLOCK_MONOTONIC /* macOS and whatever else */ #endif static inline ot_time time_now_seconds(void) { @@ -285,7 +285,7 @@ size_t add_peer_to_torrent_and_return_peers(PROTO_FLAG proto, struct ot_workstru size_t remove_peer_from_torrent(PROTO_FLAG proto, struct ot_workstruct *ws); void return_tcp_scrape_for_torrent(ot_hash const *hash_list, size_t amount, char **reply, const char *reply_end); size_t return_udp_scrape_for_torrent(ot_hash const hash, char *reply); -void add_torrent_from_saved_state(ot_hash const hash, ot_time base, size_t down_count); +void add_torrent_from_saved_state(ot_hash const hash, ot_time age, size_t down_count); #ifdef _DEBUG_RANDOMTORRENTS void trackerlogic_add_random_torrents(size_t amount); #endif diff --git a/tests/integration/harness.py b/tests/integration/harness.py index 10aa6de..e5ec621 100644 --- a/tests/integration/harness.py +++ b/tests/integration/harness.py @@ -77,7 +77,7 @@ def free_port(attempts=50): class Tracker: """One tracker process, listening on loopback.""" - def __init__(self, extra_config="", files=None): + def __init__(self, extra_config="", files=None, extra_args=()): self.port = free_port() # Only used by a config that asks for live sync, but allocated either # way so {livesync_port} always resolves. @@ -99,7 +99,8 @@ class Tracker: # where LeakSanitizer exists and off where it does not. Asking for it # by name makes the tracker refuse to start on macOS and FreeBSD. environment = dict(os.environ, ASAN_OPTIONS="abort_on_error=0") - self.process = subprocess.Popen([TRACKER, "-f", self.config], stdout=self.log, stderr=self.log, env=environment) + argv = [TRACKER, "-f", self.config] + [str(a) for a in extra_args] + self.process = subprocess.Popen(argv, stdout=self.log, stderr=self.log, env=environment) self._wait_for_port() except BaseException: self._cleanup() diff --git a/tests/integration/test_fullscrape.py b/tests/integration/test_fullscrape.py index be7b502..3150612 100644 --- a/tests/integration/test_fullscrape.py +++ b/tests/integration/test_fullscrape.py @@ -78,13 +78,15 @@ class FullScrape(TrackerTestCase): escaped = line.rsplit(b":", 2)[0] self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes") - def test_the_state_dump_carries_a_base_time_rather_than_a_peer_count(self): - """What a restart reads back, so its second column is the torrent's - own clock and not a count of anybody.""" + def test_the_state_dump_carries_an_age_rather_than_a_peer_count(self): + """What a restart reads back. The second column is how many minutes + ago the torrent was last touched, not a stamp and not a count of + anybody: the clock counts from boot, so an absolute time would mean + nothing to the process that reads it.""" entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump")) self.assertIn(self.seeding.hex().upper().encode(), entries) - base, completed = entries[self.seeding.hex().upper().encode()] - self.assertGreater(int(base), 0) + age, completed = entries[self.seeding.hex().upper().encode()] + self.assertEqual(int(age), 0, "announced a moment ago") self.assertEqual(int(completed), 0) # ------------------------------------------------------------ shape diff --git a/tests/integration/test_statefile.py b/tests/integration/test_statefile.py new file mode 100644 index 0000000..2a2d147 --- /dev/null +++ b/tests/integration/test_statefile.py @@ -0,0 +1,53 @@ +"""The state dump and the -l switch that reads it back. + +Nothing exercised the load path before, and the dump's second column is now +how long ago a torrent was last touched rather than when, because the clock +counts from boot and an absolute stamp would mean nothing to the process +that reads the file. +""" + +import os +import unittest + +from harness import Tracker, TrackerTestCase + + +class StateFileRoundTrip(TrackerTestCase): + def dump(self): + return self.tracker.get("/stats?mode=statedump") + + def test_a_dumped_torrent_comes_back_with_its_completed_count(self): + info_hash = self.hash_for("state-rt") + self.tracker.announce(info_hash, port=6001, left=0, event="completed") + self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"downloaded"], 1) + + path = os.path.join(self.tracker.dir, "state") + with open(path, "wb") as handle: + handle.write(self.dump()) + + restarted = Tracker(extra_args=["-l", path]) + try: + files = restarted.scrape(info_hash)[b"files"] + self.assertIn(info_hash, files, "the torrent did not survive the restart") + entry = files[info_hash] + self.assertEqual(entry[b"complete"], 0, "no peers are carried over, only the torrent") + self.assertEqual(entry[b"incomplete"], 0) + complaints = restarted.stop() + self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) + finally: + restarted._cleanup() + + def test_a_torrent_older_than_the_tracker_would_keep_is_not_loaded(self): + """An age past the torrent timeout would be cleaned away on the first + sweep, so it is never built in the first place.""" + info_hash = self.hash_for("state-stale") + path = os.path.join(self.tracker.dir, "stale") + with open(path, "w") as handle: + handle.write("%s:%d:7\n" % (info_hash.hex(), 24 * 60 + 1)) + + restarted = Tracker(extra_args=["-l", path]) + try: + self.assertNotIn(info_hash, restarted.scrape(info_hash)[b"files"]) + restarted.stop() + finally: + restarted._cleanup() -- cgit v1.3