From 45fbf91beec0d36905e0a23401ee01dbd8e4e065 Mon Sep 17 00:00:00 2001 From: euzu <33094714+euzu@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:15:31 +0100 Subject: [PATCH] Feature/provider linup manager deadlock fix (#545) Identified a critical deadlock path in reconcile_connections where a DashMap iterator held internal shard locks while awaiting an asynchronous --- CHANGELOG.md | 1 + backend/src/api/model/provider_lineup_manager.rs | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8539a3317..135c84017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ - Added **TMDB** settings (Rate Limit, Cache Duration, Language) and **Metadata Formats** (NFO support) to Library configuration UI. ## 🚀 Performance & Stability +- **Deadlock Resolution**: Fixed a potential deadlock in `ProviderLineupManager::reconcile_connections` by refactoring `DashMap` iterations to use snapshots, preventing internal shard locks from being held during async lock acquisition. - **Connection Reconciliation & GC**: 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. - **Full Async Runtime**: Transitioned to `#[tokio::main]` and async I/O throughout the entire application. - **Non-Blocking Operations**: Cache persistence, playlist exports, and config saves moved to async tasks to prevent runtime stalls. diff --git a/backend/src/api/model/provider_lineup_manager.rs b/backend/src/api/model/provider_lineup_manager.rs index a02322105..0476d039b 100644 --- a/backend/src/api/model/provider_lineup_manager.rs +++ b/backend/src/api/model/provider_lineup_manager.rs @@ -631,11 +631,15 @@ impl ProviderLineupManager { pub async fn reconcile_connections(&self, mut counts: HashMap, usize>) { // 1. Synchronize known providers from actual counts. - // This avoids a transient "zero state" by updating each provider in one step. - for entry in &self.provider_connections { - let name = entry.key(); - let count = counts.remove(name).unwrap_or(0); - let mut conn = entry.value().write().await; + // We take a snapshot of the keys and locks to avoid holding the DashMap's internal + // shard locks while awaiting the RwLock of each provider. Holding both can lead to deadlocks. + 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 = counts.remove(&name).unwrap_or(0); + let mut conn = conn_lock.write().await; conn.current_connections = count; if count == 0 { conn.granted_grace = false;