From 7ffca8ef8af0e30fd4d0a6078b10fd569f7adb47 Mon Sep 17 00:00:00 2001 From: Paul Logan Date: Fri, 19 Jun 2026 19:20:22 -0700 Subject: [PATCH] perf(relay): sweep intro_times on the background tick, off the hot intro path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit evict_stale_intro_nicks (a retain over up to MAX_INTRO_TRACKING_NICKS = 10k entries) ran on-demand inside handle_intro under the global lock whenever the map crossed the soft cap — a 10k scan on the hot, unauthenticated intro path that blocks every other relay handler while held. Add a proactive sweep to the existing 60s pair-sweeper tick so the map rarely reaches that threshold; the on-demand sweep stays as a backstop, so the memory bound is unchanged — only the common-case eviction moves off the hot path. (Bug-hunt relay-locks dimension, #14. The sibling #7 finding — explicit caps on the slot-keyed maps — is noted not coded: responder_health is already transitively bounded by MAX_SLOTS, and a slot-TTL reaper is a semantic change (slots never expire today) deferred for separate design.) Test sweep_intro_times_drops_aged_nicks_off_the_hot_path. 609 lib tests; fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 --- src/relay_server.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/relay_server.rs b/src/relay_server.rs index 5974d18..10bf56d 100644 --- a/src/relay_server.rs +++ b/src/relay_server.rs @@ -550,6 +550,17 @@ impl Relay { /// 60 seconds. Call once after `Relay::new`; the handle is leaked deliberately /// — process exit reaps it. Safe to skip in tests where you'd rather test /// eviction inline. + /// Proactively drop fully-aged nicks from `intro_times` on the background + /// tick, so the map rarely reaches the on-demand sweep threshold in + /// `handle_intro` — that keeps the 10k-entry `retain` scan off the hot, + /// globally-locked intro path (where it would block every other handler). + /// The on-demand sweep stays as a backstop, so the bound is unchanged. + async fn sweep_intro_times(&self) { + let now = unix_now(); + let mut inner = self.inner.lock().await; + evict_stale_intro_nicks(&mut inner.intro_times, now, INTRO_WINDOW_SECS); + } + pub fn spawn_pair_sweeper(&self) { let me = self.clone(); tokio::spawn(async move { @@ -557,6 +568,7 @@ impl Relay { loop { tick.tick().await; me.evict_expired_pair_slots().await; + me.sweep_intro_times().await; } }); } @@ -2655,6 +2667,29 @@ mod tests { assert!(s.chars().all(|c| c.is_ascii_hexdigit())); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn sweep_intro_times_drops_aged_nicks_off_the_hot_path() { + let dir = std::env::temp_dir().join(format!("wire-introsweep-{}", random_hex(8))); + let _ = std::fs::remove_dir_all(&dir); + let relay = Relay::new(dir.clone()).await.unwrap(); + let now = unix_now(); + { + let mut inner = relay.inner.lock().await; + inner.intro_times.insert("fresh".into(), vec![now]); // within window + inner + .intro_times + .insert("stale".into(), vec![now - INTRO_WINDOW_SECS - 1]); // aged out + assert_eq!(inner.intro_times.len(), 2); + } + relay.sweep_intro_times().await; + { + let inner = relay.inner.lock().await; + assert!(inner.intro_times.contains_key("fresh"), "fresh nick kept"); + assert!(!inner.intro_times.contains_key("stale"), "aged nick swept"); + } + let _ = std::fs::remove_dir_all(&dir); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn pair_slot_evicts_when_idle_past_ttl() { let dir = std::env::temp_dir().join(format!("wire-evict-{}", random_hex(8)));