diff --git a/crates/directory/CHANGELOG.md b/crates/directory/CHANGELOG.md index d08bd96..2f9ad19 100644 --- a/crates/directory/CHANGELOG.md +++ b/crates/directory/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - HTTPS connections no longer trust certificates from emSign's distrusted intermediates, and certificate checks use constant-time comparisons: rustls-webpki, webpki-roots and ctutils are updated (#351). - The public pages read the list from the database at most once a minute, however many people ask. Each page asked for the whole catalogue, so a flood of requests with query strings of their own reached the database every time (#341). - The directory no longer fetches feeds at internal addresses (loopback, the LAN, Docker's networks), named in a snapshot or reached by a redirect, unless directory.toml's `fetch_internal` lists them. It fetched them, and stored what answered as the feed's title and description (#329). +- A server's count of a feed's subscribers is taken up to a million. Two servers sending the largest number there is made the sum overflow, and every listing and page failed until one was dropped (#342). ## [1.3.0] - 2026-10-19 diff --git a/crates/directory/src/store.rs b/crates/directory/src/store.rs index 83ee9e3..c46e160 100644 --- a/crates/directory/src/store.rs +++ b/crates/directory/src/store.rs @@ -13,6 +13,9 @@ pub const ITEMS: usize = 20; /// How long a feed's failure goes unexplained: a site down for an hour is not called gone (ipx#143). const FLAG_AFTER_SECS: i64 = 24 * 3600; +/// The most subscribers one instance may say a feed has. +pub const MAX_SUBSCRIBERS: i64 = 1_000_000; + pub struct Store { db: DatabaseConnection, } @@ -221,7 +224,9 @@ impl Store { continue; } // The same feed twice in one snapshot counts once, at its larger number. - let n = e.subscribers.max(0); + // Bounded above as well: two instances' i64::MAX summed past BIGINT, and every listing + // and page failed until the instance was dropped (#342). No server has a million. + let n = e.subscribers.clamp(0, MAX_SUBSCRIBERS); let path = e.category.as_deref().map(str::trim).filter(|c| !c.is_empty()).map(ipx_feed::category_path); let slot = keep.entry(id).or_insert((url, n, path.clone())); slot.1 = slot.1.max(n); @@ -548,6 +553,17 @@ pub(crate) mod tests { assert!(normalise("not a url").is_none()); } + #[tokio::test] + async fn no_count_an_instance_sends_breaks_the_listing() { + let s = Store::scratch().await; + s.snapshot("a", &[e("https://x.test/a.xml", i64::MAX)]).await.unwrap(); + s.snapshot("b", &[e("https://x.test/a.xml", i64::MAX)]).await.unwrap(); + fetch_all(&s).await; + assert_eq!(s.list().await.unwrap()[0].subscribers, 2 * MAX_SUBSCRIBERS, "summed, not overflowed (#342)"); + assert_eq!(s.popular(10).await.unwrap()[0].subscribers, 2 * MAX_SUBSCRIBERS); + assert!(s.instances().await.is_ok()); + } + #[tokio::test] async fn snapshots_replace_what_an_instance_sent_and_add_up_across_instances() { let s = Store::scratch().await;