summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/ot_fullscrape.c2
-rw-r--r--src/trackerlogic.c15
-rw-r--r--src/trackerlogic.h12
-rw-r--r--tests/integration/harness.py5
-rw-r--r--tests/integration/test_fullscrape.py12
-rw-r--r--tests/integration/test_statefile.py53
6 files changed, 81 insertions, 18 deletions
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
131 case TASK_FULLSCRAPE_TRACKERSTATE: 131 case TASK_FULLSCRAPE_TRACKERSTATE:
132 hex_from_hash(r, *hash); 132 hex_from_hash(r, *hash);
133 r += 2 * sizeof(ot_hash); 133 r += 2 * sizeof(ot_hash);
134 fmt_append(&r, r_end, ":%zd:%zd\n", torrent->peer_list6->base, down_count); 134 fmt_append(&r, r_end, ":%zd:%zd\n", (ssize_t)time_since_minutes(torrent->peer_list6->base), down_count);
135 break; 135 break;
136 } 136 }
137 *cursor = r; 137 *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) {
38 free(peer_list); 38 free(peer_list);
39} 39}
40 40
41void add_torrent_from_saved_state(ot_hash const hash, ot_time base, size_t down_count) { 41/* How many minutes before this moment the torrent was last touched, which is
42 what a state dump records. */
43void add_torrent_from_saved_state(ot_hash const hash, ot_time age, size_t down_count) {
42 int exactmatch; 44 int exactmatch;
43 ot_torrent *torrent; 45 ot_torrent *torrent;
44 ot_vector *torrents_list = mutex_bucket_lock_by_hash(hash); 46 ot_vector *torrents_list;
47
48 if (age > OT_TORRENT_TIMEOUT)
49 return;
50
51 torrents_list = mutex_bucket_lock_by_hash(hash);
45 52
46 if (!accesslist_hashisvalid(hash)) { 53 if (!accesslist_hashisvalid(hash)) {
47 mutex_bucket_unlock_by_hash(hash, 0); 54 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_
66 73
67 memset(torrent->peer_list6, 0, sizeof(ot_peerlist)); 74 memset(torrent->peer_list6, 0, sizeof(ot_peerlist));
68 memset(torrent->peer_list4, 0, sizeof(ot_peerlist)); 75 memset(torrent->peer_list4, 0, sizeof(ot_peerlist));
69 torrent->peer_list6->base = base; 76 torrent->peer_list6->base = time_now_minutes() - age;
70 torrent->peer_list4->base = base; 77 torrent->peer_list4->base = time_now_minutes() - age;
71 torrent->peer_list6->down_count = down_count; 78 torrent->peer_list6->down_count = down_count;
72 torrent->peer_list4->down_count = down_count; 79 torrent->peer_list4->down_count = down_count;
73 80
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 {
76 on startup */ 76 on startup */
77#define RANDOMTORRENTS (1024 * 1024 * 1) 77#define RANDOMTORRENTS (1024 * 1024 * 1)
78 78
79#if defined(CLOCK_REALTIME_COARSE) 79#if defined(CLOCK_MONOTONIC_COARSE)
80#define OT_CLOCK_CHEAP CLOCK_REALTIME_COARSE /* Linux */ 80#define OT_CLOCK_CHEAP CLOCK_MONOTONIC_COARSE /* Linux and FreeBSD */
81#elif defined(CLOCK_REALTIME_FAST) 81#elif defined(CLOCK_MONOTONIC_FAST)
82#define OT_CLOCK_CHEAP CLOCK_REALTIME_FAST /* the BSDs */ 82#define OT_CLOCK_CHEAP CLOCK_MONOTONIC_FAST /* the other BSDs */
83#else 83#else
84#define OT_CLOCK_CHEAP CLOCK_REALTIME /* macOS and whatever else */ 84#define OT_CLOCK_CHEAP CLOCK_MONOTONIC /* macOS and whatever else */
85#endif 85#endif
86 86
87static inline ot_time time_now_seconds(void) { 87static 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
285size_t remove_peer_from_torrent(PROTO_FLAG proto, struct ot_workstruct *ws); 285size_t remove_peer_from_torrent(PROTO_FLAG proto, struct ot_workstruct *ws);
286void return_tcp_scrape_for_torrent(ot_hash const *hash_list, size_t amount, char **reply, const char *reply_end); 286void return_tcp_scrape_for_torrent(ot_hash const *hash_list, size_t amount, char **reply, const char *reply_end);
287size_t return_udp_scrape_for_torrent(ot_hash const hash, char *reply); 287size_t return_udp_scrape_for_torrent(ot_hash const hash, char *reply);
288void add_torrent_from_saved_state(ot_hash const hash, ot_time base, size_t down_count); 288void add_torrent_from_saved_state(ot_hash const hash, ot_time age, size_t down_count);
289#ifdef _DEBUG_RANDOMTORRENTS 289#ifdef _DEBUG_RANDOMTORRENTS
290void trackerlogic_add_random_torrents(size_t amount); 290void trackerlogic_add_random_torrents(size_t amount);
291#endif 291#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):
77class Tracker: 77class Tracker:
78 """One tracker process, listening on loopback.""" 78 """One tracker process, listening on loopback."""
79 79
80 def __init__(self, extra_config="", files=None): 80 def __init__(self, extra_config="", files=None, extra_args=()):
81 self.port = free_port() 81 self.port = free_port()
82 # Only used by a config that asks for live sync, but allocated either 82 # Only used by a config that asks for live sync, but allocated either
83 # way so {livesync_port} always resolves. 83 # way so {livesync_port} always resolves.
@@ -99,7 +99,8 @@ class Tracker:
99 # where LeakSanitizer exists and off where it does not. Asking for it 99 # where LeakSanitizer exists and off where it does not. Asking for it
100 # by name makes the tracker refuse to start on macOS and FreeBSD. 100 # by name makes the tracker refuse to start on macOS and FreeBSD.
101 environment = dict(os.environ, ASAN_OPTIONS="abort_on_error=0") 101 environment = dict(os.environ, ASAN_OPTIONS="abort_on_error=0")
102 self.process = subprocess.Popen([TRACKER, "-f", self.config], stdout=self.log, stderr=self.log, env=environment) 102 argv = [TRACKER, "-f", self.config] + [str(a) for a in extra_args]
103 self.process = subprocess.Popen(argv, stdout=self.log, stderr=self.log, env=environment)
103 self._wait_for_port() 104 self._wait_for_port()
104 except BaseException: 105 except BaseException:
105 self._cleanup() 106 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):
78 escaped = line.rsplit(b":", 2)[0] 78 escaped = line.rsplit(b":", 2)[0]
79 self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes") 79 self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes")
80 80
81 def test_the_state_dump_carries_a_base_time_rather_than_a_peer_count(self): 81 def test_the_state_dump_carries_an_age_rather_than_a_peer_count(self):
82 """What a restart reads back, so its second column is the torrent's 82 """What a restart reads back. The second column is how many minutes
83 own clock and not a count of anybody.""" 83 ago the torrent was last touched, not a stamp and not a count of
84 anybody: the clock counts from boot, so an absolute time would mean
85 nothing to the process that reads it."""
84 entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump")) 86 entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump"))
85 self.assertIn(self.seeding.hex().upper().encode(), entries) 87 self.assertIn(self.seeding.hex().upper().encode(), entries)
86 base, completed = entries[self.seeding.hex().upper().encode()] 88 age, completed = entries[self.seeding.hex().upper().encode()]
87 self.assertGreater(int(base), 0) 89 self.assertEqual(int(age), 0, "announced a moment ago")
88 self.assertEqual(int(completed), 0) 90 self.assertEqual(int(completed), 0)
89 91
90 # ------------------------------------------------------------ shape 92 # ------------------------------------------------------------ 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 @@
1"""The state dump and the -l switch that reads it back.
2
3Nothing exercised the load path before, and the dump's second column is now
4how long ago a torrent was last touched rather than when, because the clock
5counts from boot and an absolute stamp would mean nothing to the process
6that reads the file.
7"""
8
9import os
10import unittest
11
12from harness import Tracker, TrackerTestCase
13
14
15class StateFileRoundTrip(TrackerTestCase):
16 def dump(self):
17 return self.tracker.get("/stats?mode=statedump")
18
19 def test_a_dumped_torrent_comes_back_with_its_completed_count(self):
20 info_hash = self.hash_for("state-rt")
21 self.tracker.announce(info_hash, port=6001, left=0, event="completed")
22 self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"downloaded"], 1)
23
24 path = os.path.join(self.tracker.dir, "state")
25 with open(path, "wb") as handle:
26 handle.write(self.dump())
27
28 restarted = Tracker(extra_args=["-l", path])
29 try:
30 files = restarted.scrape(info_hash)[b"files"]
31 self.assertIn(info_hash, files, "the torrent did not survive the restart")
32 entry = files[info_hash]
33 self.assertEqual(entry[b"complete"], 0, "no peers are carried over, only the torrent")
34 self.assertEqual(entry[b"incomplete"], 0)
35 complaints = restarted.stop()
36 self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints))
37 finally:
38 restarted._cleanup()
39
40 def test_a_torrent_older_than_the_tracker_would_keep_is_not_loaded(self):
41 """An age past the torrent timeout would be cleaned away on the first
42 sweep, so it is never built in the first place."""
43 info_hash = self.hash_for("state-stale")
44 path = os.path.join(self.tracker.dir, "stale")
45 with open(path, "w") as handle:
46 handle.write("%s:%d:7\n" % (info_hash.hex(), 24 * 60 + 1))
47
48 restarted = Tracker(extra_args=["-l", path])
49 try:
50 self.assertNotIn(info_hash, restarted.scrape(info_hash)[b"files"])
51 restarted.stop()
52 finally:
53 restarted._cleanup()