Files
ngit-grasp/tests/relay_subscription_state.rs
DanConwayDev c5e52c5a1e fix(relay): admit repository-scale live coverage
Production gitnostr.com repeatedly received CLOSED responses from relay.ngit.dev after reconnect: its 34-filter live set for 924 full repositories, 89 state-only repositories, and 3,748 roots crossed rust-nostr 0.45's newly selected 1 MiB cumulative subscription-state limit. Fourteen representative REQs were accepted and the remainder were refused, silently leaving persistent live coverage incomplete; cooldown recovery only recreated the same impossible set.

Raise the explicitly selected per-connection retained subscription-state allowance to 5 MiB. This remains a finite boundary, matches the largest individual WebSocket message already admitted, and leaves roughly four times the observed working-set headroom without promising the theoretical 500 x 96 KiB maximum. Document the exact serving policy and its lack of NIP-11 negotiation.

A scenario opens 17 persistent sub-96 KiB REQs carrying 34 filters. It failed unchanged 2.1.1 at live-14 with the production CLOSED reason and passes with the new bound. Correctness assumes this allowance is enforced per connection by rust-nostr; client-side adaptation to unknown third-party cumulative byte limits and multi-connection sharding remain excluded because NIP-11 exposes no such capability.

Validation: focused scenario passed; 658 library tests passed; git diff --check passed; nix build .#ngit-grasp passed.
2026-08-08 10:05:49 +00:00

65 lines
2.2 KiB
Rust

//! Connection-wide active-subscription state regression scenarios.
//!
//! Repository sync can legitimately keep many byte-budgeted filters live on
//! one relay connection. The cumulative allowance must accommodate that
//! coverage even though each individual REQ remains conservatively bounded.
mod common;
use std::time::Duration;
use common::TestRelay;
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::Message;
#[tokio::test]
async fn repository_scale_live_filter_set_remains_admitted() {
let relay = TestRelay::start().await;
let (mut stream, _) = tokio_tungstenite::connect_async(relay.url())
.await
.expect("connect to relay");
// Two 550-value filters are about 74 KiB. Seventeen persistent REQs model
// the 34-filter live set that production rejected after crossing the old
// 1 MiB cumulative default, while every individual message stays below
// the sync client's 96 KiB REQ budget.
let ids = (0..550)
.map(|index| format!("{index:064x}"))
.collect::<Vec<_>>();
let filter = serde_json::json!({"#e": ids, "limit": 0});
for index in 0..17 {
let request = serde_json::json!(["REQ", format!("live-{index}"), filter, filter]);
assert!(request.to_string().len() < 96 * 1024);
stream
.send(Message::Text(request.to_string().into()))
.await
.expect("send persistent live REQ");
}
let admitted = tokio::time::timeout(Duration::from_secs(10), async {
let mut eose = 0;
while let Some(message) = stream.next().await {
let text = message.expect("valid relay response").into_text().unwrap();
assert!(
!text.contains("active subscriptions exceed max size"),
"repository-scale live coverage was rejected: {text}"
);
if text.starts_with("[\"EOSE\",\"live-") {
eose += 1;
if eose == 17 {
return;
}
}
}
panic!("relay closed before acknowledging every live REQ");
})
.await;
assert!(
admitted.is_ok(),
"relay did not acknowledge live coverage in time"
);
relay.stop().await;
}