fix(peer): bound aggregate remote library state
Per-peer library limits still allowed 64 authenticated identities to retain a large aggregate map and repeatedly clone it for UI publication. Cap retained remote availability at 16,384 rows. A peer whose new projection would exceed the budget stays authenticated with an empty library slice, and its accepted revision is retained so liveness does not repeatedly pull the same over-budget snapshot. Test Plan: - `just test` -- passed outside the sandbox; 496 peer tests and all workspace targets passed. - `just fmt` -- Rust formatting completed; the recipe then hit the pre-existing generated security-report Markdown-lint failures. - `git diff --cached --check` -- passed.
This commit is contained in:
@@ -20,6 +20,7 @@ use lanspread_proto::{
|
||||
use tokio::time::Instant;
|
||||
|
||||
pub const MAX_AUTHENTICATED_PEERS: usize = 64;
|
||||
const MAX_TOTAL_REMOTE_LIBRARY_GAMES: usize = 16_384;
|
||||
|
||||
/// Monotonic identity for one successfully authenticated endpoint observation.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -380,9 +381,23 @@ impl PeerGameDB {
|
||||
})
|
||||
});
|
||||
|
||||
let incoming_library = library
|
||||
.as_ref()
|
||||
.map(|snapshot| (snapshot.revision, library_map(snapshot)));
|
||||
let incoming_library = library.as_ref().map(|snapshot| {
|
||||
let games = library_map(snapshot);
|
||||
let current_count = self
|
||||
.peers
|
||||
.values()
|
||||
.map(|peer| peer.games.len())
|
||||
.sum::<usize>();
|
||||
let replaced_count = self
|
||||
.peers
|
||||
.get(&endpoint.peer_id)
|
||||
.map_or(0, |peer| peer.games.len());
|
||||
let projected_count = current_count
|
||||
.saturating_sub(replaced_count)
|
||||
.saturating_add(games.len());
|
||||
let admitted = (projected_count <= MAX_TOTAL_REMOTE_LIBRARY_GAMES).then_some(games);
|
||||
(snapshot.revision, admitted)
|
||||
});
|
||||
if let Some(peer) = self.peers.get_mut(&peer_id) {
|
||||
let previous_endpoint =
|
||||
(peer.addr != addr).then(|| PeerEndpoint::new(peer.peer_id, peer.addr));
|
||||
@@ -401,6 +416,7 @@ impl PeerGameDB {
|
||||
.library_revision
|
||||
.is_none_or(|current| revision > current) =>
|
||||
{
|
||||
let games = games.unwrap_or_default();
|
||||
let changed = peer.library_revision != Some(revision) || peer.games != games;
|
||||
peer.library_revision = Some(revision);
|
||||
peer.games = games;
|
||||
@@ -446,7 +462,9 @@ impl PeerGameDB {
|
||||
runtime_session_id,
|
||||
library_revision: incoming_library.as_ref().map(|(revision, _)| *revision),
|
||||
call_to_play_revision: None,
|
||||
games: incoming_library.map_or_else(HashMap::new, |(_, games)| games),
|
||||
games: incoming_library
|
||||
.and_then(|(_, games)| games)
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
);
|
||||
self.addr_index.insert(addr, peer_id);
|
||||
@@ -745,6 +763,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn large_library(revision: u64, seed: u8) -> LibrarySnapshot {
|
||||
LibrarySnapshot {
|
||||
revision,
|
||||
games: (0..lanspread_proto::MAX_LIBRARY_GAMES)
|
||||
.map(|index| GameAvailability {
|
||||
game_id: format!("game-{seed}-{index:04}"),
|
||||
content_id: ContentId::from_bytes([seed; 32]),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn commit(
|
||||
db: &mut PeerGameDB,
|
||||
endpoint: PeerEndpoint,
|
||||
@@ -785,6 +815,51 @@ mod tests {
|
||||
assert_eq!(db.peer_endpoint(&endpoint.peer_id), Some(endpoint));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregate_remote_library_budget_drops_only_the_oversized_projection() {
|
||||
let mut db = PeerGameDB::new();
|
||||
for seed in 1..=4 {
|
||||
commit(
|
||||
&mut db,
|
||||
endpoint(seed, 12_000 + u16::from(seed)),
|
||||
session(seed),
|
||||
large_library(u64::from(seed), seed),
|
||||
Some(0),
|
||||
);
|
||||
}
|
||||
let fifth = endpoint(5, 12_005);
|
||||
let ticket = db
|
||||
.begin_candidate_negotiation(fifth)
|
||||
.expect("fifth candidate should reserve");
|
||||
let upsert = db
|
||||
.commit_authenticated_snapshot(fifth, ticket, session(5), Some(large_library(5, 5)))
|
||||
.expect("fifth snapshot should still authenticate")
|
||||
.expect("fifth ticket should remain current");
|
||||
|
||||
assert!(upsert.library_changed);
|
||||
assert_eq!(
|
||||
db.peer_snapshots()
|
||||
.iter()
|
||||
.find(|peer| peer.peer_id == fifth.peer_id)
|
||||
.expect("fifth peer should be retained")
|
||||
.game_count,
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
db.revision_snapshot(&fifth.peer_id)
|
||||
.expect("fifth peer should have a revision watermark")
|
||||
.library_revision,
|
||||
Some(5),
|
||||
);
|
||||
assert_eq!(
|
||||
db.peer_snapshots()
|
||||
.iter()
|
||||
.map(|peer| peer.game_count)
|
||||
.sum::<usize>(),
|
||||
MAX_TOTAL_REMOTE_LIBRARY_GAMES
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_remote_peers_returns_peer_id_sorted_retirements() {
|
||||
let mut db = PeerGameDB::new();
|
||||
|
||||
Reference in New Issue
Block a user