From feefe3e69e7eafabfdcb441dfbfff88f36933297 Mon Sep 17 00:00:00 2001 From: euzu Date: Sat, 3 Jan 2026 22:29:50 +0100 Subject: [PATCH] Resolved a critical issue where provider connection counters could leak or become stale during hot reloads. Added automatic garbage collection for unused provider records to prevent logical memory buildup. --- .../src/api/model/provider_lineup_manager.rs | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/backend/src/api/model/provider_lineup_manager.rs b/backend/src/api/model/provider_lineup_manager.rs index d11ed9776..e83ad0742 100644 --- a/backend/src/api/model/provider_lineup_manager.rs +++ b/backend/src/api/model/provider_lineup_manager.rs @@ -649,26 +649,54 @@ impl ProviderLineupManager { } // 3. Broadcast status updates to the UI/Event system. - // We collect entries first to release DashMap read locks quickly. - let snapshot: Vec<_> = self.provider_connections.iter() - .map(|e| (e.key().clone(), Arc::clone(e.value()))) - .collect(); + // We drop the snapshot before GC to ensure Arc counts are accurate. + { + let snapshot: Vec<_> = self.provider_connections.iter() + .map(|e| (e.key().clone(), Arc::clone(e.value()))) + .collect(); - for (name, conn_lock) in snapshot { - let count = conn_lock.read().await.current_connections; - self.event_manager.send_provider_event(&name, count); + for (name, conn_lock) in snapshot { + let count = conn_lock.read().await.current_connections; + self.event_manager.send_provider_event(&name, count); + } } // 4. Garbage Collection: Remove providers that are neither in the current config // nor referenced by any active stream allocations. - // Arc::strong_count == 1 means it's only held by this DashMap. - self.provider_connections.retain(|name, conn_lock| { - if Arc::strong_count(conn_lock) == 1 { - debug!("Purging stale provider connection record: {name}", ); - false - } else { - true + // + // deterministic check: Is the name still in any current lineup? + let current_names: std::collections::HashSet = { + let mut names = std::collections::HashSet::new(); + let lineups = self.providers.load(); + for lineup in lineups.iter() { + match lineup { + ProviderLineup::Single(s) => { names.insert(s.provider.name.clone()); } + ProviderLineup::Multi(m) => { + names.insert(m.name.clone()); + for g in &m.providers { + match g { + ProviderPriorityGroup::SingleProviderGroup(p) => { names.insert(p.name.clone()); } + ProviderPriorityGroup::MultiProviderGroup(_, group) => { + for p in group { names.insert(p.name.clone()); } + } + } + } + } + } } + names + }; + + // Arc::strong_count == 1 means it's only held by this DashMap registry. + self.provider_connections.retain(|name, conn_lock| { + if current_names.contains(name) { + return true; // Keep if in active config + } + if Arc::strong_count(conn_lock) > 1 { + return true; // Keep if held by an active stream + } + debug!("Purging stale provider connection record: {name}"); + false }); }