Skip to content

Commit 32a37d1

Browse files
committed
fix: [#1502] bug in total number of downloads for all torrents metric
Relates to: 34c159a A couple of days ago, I made a change in [this commit](34c159a). I changed the `Swarm::meets_retaining_policy` method from: ``` /// Returns true if the torrents meets the retention policy, meaning that /// it should be kept in the tracker. pub fn meets_retaining_policy(&self, policy: &TrackerPolicy) -> bool { if policy.persistent_torrent_completed_stat && self.metadata().downloaded > 0 { return true; } if policy.remove_peerless_torrents && self.is_empty() { return false; } true } ``` To: ``` pub fn meets_retaining_policy(&self, policy: &TrackerPolicy) -> bool { !(policy.remove_peerless_torrents && self.is_empty()) } ``` I thought this code was not needed: ```rust if policy.persistent_torrent_completed_stat && self.metadata().downloaded > 0 { return true; } ``` However, it's needed. One of the metrics returned by the tracker API is the **total number of downloads for all torrents**. ```json { "torrents": 320961, "seeders": 189885, "completed": 975119, <- this "leechers": 231044, ... } ``` That metric is always stored in memory but can optionally persist into the database. It's important to highlight that the metric represents: - The total number of downloads for **ALL** torrents ever, when the metric is persisted. - The total number of downloads for **ALL** torrents since the tracker started, when the metric is not persisted. It could be mixed up with another internal metric (not exposed via the API), which is the same counter but only for ONE swarm (one torrent). - The total number of downloads for **ONE** concrete torrent ever, when the metric is persisted. - The total number of downloads for **ONE** concrete torrent since the tracker started, when the metric is not persisted. The bug affects the first metric. The exposed via the API. The problem is that this feature conflicts with removing the peerless torrents. When removing the peerless torrents config option is enabled, the counter is lost unless it is persisted. Becuase the counter values are stored in the "Swarm" together with the list of peers. If statistics persistence is enabled, that's not a problem. When the torrent is removed from the tracker (from the swarms or swarm collection), the counter is initialised again if the torrent is added. In other words, if a new peer starts the swarm again, the number of downloads is loaded from the database. However, that works for the counter of each torrent (swarm) but not for the overall counter (the sum of downloads for all torrents). That metric is not stored anywhere. It's calculated on demand by iterating all the swarms and summing up the total for each torrent, giving the total amount of downloads for **ALL** torrents. When the torrent is removed, the downloads for that torrent don't count in the total. That is the reason we have to keep the torrent (swarm) in memory, even if it does not have any peer (and it should be removed according to the other config flag). The removed line: ```rust if policy.persistent_torrent_completed_stat && self.metadata().downloaded > 0 { return true; } ``` does that. **When the stats persistence is disabled**, that's one way to store the value. Alternatively, we could add another cache for the data and never remove that value. The current solution has a problem: It can make the tracker consume a lot of memory because peerless torrents are not removed in practice (even if it's configured to be). **When the stats persistence is enabled,** we can simply return the value from the database. **NOTICE:** that the value is used in the scrape response, so it might be convenient to have a cache in memory anyway. - [x] Revert the change to fix the bug asap. - [x] Write a unit test. This behaviour was not covered by any test (or documented). - [ ] Add an in-memory cache value in `Swarms` type to store the total for all torrents, regardless of which are the current active swarms.
1 parent 3adbb89 commit 32a37d1

File tree

1 file changed

+102
-20
lines changed

1 file changed

+102
-20
lines changed

packages/torrent-repository/src/swarm.rs

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,20 @@ impl Swarm {
191191
}
192192

193193
/// Returns true if the swarm meets the retention policy, meaning that
194-
/// it should be kept in the tracker.
194+
/// it should be kept in the list of swarms.
195195
#[must_use]
196196
pub fn meets_retaining_policy(&self, policy: &TrackerPolicy) -> bool {
197-
!(policy.remove_peerless_torrents && self.is_empty())
197+
!self.should_be_removed(policy)
198+
}
199+
200+
fn should_be_removed(&self, policy: &TrackerPolicy) -> bool {
201+
// If the policy is to remove peerless torrents and the swarm is empty (no peers),
202+
(policy.remove_peerless_torrents && self.is_empty())
203+
// but not when the policy is to persist torrent stats and the
204+
// torrent has been downloaded at least once.
205+
// (because the only way to store the counter is to keep the swarm in memory.
206+
// See https://github.com/torrust/torrust-tracker/issues/1502)
207+
&& !(policy.persistent_torrent_completed_stat && self.metadata().downloaded > 0)
198208
}
199209
}
200210

@@ -205,7 +215,6 @@ mod tests {
205215
use std::sync::Arc;
206216

207217
use aquatic_udp_protocol::PeerId;
208-
use torrust_tracker_configuration::TrackerPolicy;
209218
use torrust_tracker_primitives::peer::fixture::PeerBuilder;
210219
use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
211220
use torrust_tracker_primitives::DurationSinceUnixEpoch;
@@ -376,28 +385,101 @@ mod tests {
376385
assert_eq!(swarm.len(), 1);
377386
}
378387

379-
#[test]
380-
fn it_should_be_kept_when_empty_if_the_tracker_policy_is_not_to_remove_peerless_torrents() {
381-
let empty_swarm = Swarm::default();
388+
mod for_retaining_policy {
382389

383-
let policy = TrackerPolicy {
384-
remove_peerless_torrents: false,
385-
..Default::default()
386-
};
390+
use torrust_tracker_configuration::TrackerPolicy;
391+
use torrust_tracker_primitives::peer::fixture::PeerBuilder;
387392

388-
assert!(empty_swarm.meets_retaining_policy(&policy));
389-
}
393+
use crate::Swarm;
390394

391-
#[test]
392-
fn it_should_be_removed_when_empty_if_the_tracker_policy_is_to_remove_peerless_torrents() {
393-
let empty_swarm = Swarm::default();
395+
fn empty_swarm() -> Swarm {
396+
Swarm::default()
397+
}
394398

395-
let policy = TrackerPolicy {
396-
remove_peerless_torrents: true,
397-
..Default::default()
398-
};
399+
fn not_empty_swarm() -> Swarm {
400+
let mut swarm = Swarm::default();
401+
swarm.upsert_peer(PeerBuilder::default().build().into(), &mut false);
402+
swarm
403+
}
404+
405+
fn not_empty_swarm_with_downloads() -> Swarm {
406+
let mut swarm = Swarm::default();
407+
408+
let mut peer = PeerBuilder::leecher().build();
409+
let mut downloads_increased = false;
410+
411+
swarm.upsert_peer(peer.into(), &mut downloads_increased);
412+
413+
peer.event = aquatic_udp_protocol::AnnounceEvent::Completed;
414+
415+
swarm.upsert_peer(peer.into(), &mut downloads_increased);
416+
417+
assert!(swarm.metadata().downloads() > 0);
418+
419+
swarm
420+
}
421+
422+
fn remove_peerless_torrents_policy() -> TrackerPolicy {
423+
TrackerPolicy {
424+
remove_peerless_torrents: true,
425+
..Default::default()
426+
}
427+
}
428+
429+
fn don_not_remove_peerless_torrents_policy() -> TrackerPolicy {
430+
TrackerPolicy {
431+
remove_peerless_torrents: false,
432+
..Default::default()
433+
}
434+
}
399435

400-
assert!(!empty_swarm.meets_retaining_policy(&policy));
436+
mod when_removing_peerless_torrents_is_enabled {
437+
438+
use torrust_tracker_configuration::TrackerPolicy;
439+
440+
use crate::swarm::tests::for_retaining_policy::{
441+
empty_swarm, not_empty_swarm, not_empty_swarm_with_downloads, remove_peerless_torrents_policy,
442+
};
443+
444+
#[test]
445+
fn it_should_be_removed_if_the_swarm_is_empty() {
446+
assert!(empty_swarm().should_be_removed(&remove_peerless_torrents_policy()));
447+
}
448+
449+
#[test]
450+
fn it_should_not_be_removed_is_the_swarm_is_not_empty() {
451+
assert!(!not_empty_swarm().should_be_removed(&remove_peerless_torrents_policy()));
452+
}
453+
454+
#[test]
455+
fn it_should_not_be_removed_even_if_the_swarm_is_empty_if_we_need_to_track_stats_for_downloads_and_there_has_been_downloads(
456+
) {
457+
let policy = TrackerPolicy {
458+
remove_peerless_torrents: true,
459+
persistent_torrent_completed_stat: true,
460+
..Default::default()
461+
};
462+
463+
assert!(!not_empty_swarm_with_downloads().should_be_removed(&policy));
464+
}
465+
}
466+
467+
mod when_removing_peerless_torrents_is_disabled {
468+
469+
use crate::swarm::tests::for_retaining_policy::{
470+
don_not_remove_peerless_torrents_policy, empty_swarm, not_empty_swarm,
471+
};
472+
473+
#[test]
474+
fn it_should_not_be_removed_even_if_the_swarm_is_empty() {
475+
assert!(!empty_swarm().should_be_removed(&don_not_remove_peerless_torrents_policy()));
476+
}
477+
478+
#[test]
479+
fn it_should_not_be_removed_is_the_swarm_is_not_empty() {
480+
assert!(!not_empty_swarm().should_be_removed(&don_not_remove_peerless_torrents_policy()));
481+
}
482+
}
401483
}
402484

403485
#[test]

0 commit comments

Comments
 (0)