A snapshot's subscriber counts are bounded above (#342)

Store::snapshot clamped a count only from below, and LISTING sums each
feed's counts across instances, cast to BIGINT. Two instances sending
i64::MAX for a feed overflowed it (Postgres: bigint out of range;
SQLite: integer overflow), and list() and popular() failed for every
feed: the public pages 503, /v1/feeds and /v1/popular 500, until an
admin dropped the instance. Counts are now clamped to a million.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 15:17:43 +00:00
parent 0de0fb4332
commit 543d35f02c
2 changed files with 18 additions and 1 deletions

View File

@@ -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;