From 25bbf45cbb95cf8a65878c747051aecd2f27e116 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Thu, 5 Feb 2026 09:19:32 -0500 Subject: [PATCH] Fix memory leak in ActiveDownloads dictionary - Changed ActiveDownloads from Dictionary to ConcurrentDictionary for thread safety - Added automatic cleanup of completed downloads after 5 minutes - Added automatic cleanup of failed downloads after 2 minutes - This fixes the 929MB -> 10MB memory issue where downloads were never removed from tracking --- .../Services/Common/BaseDownloadService.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/allstarr/Services/Common/BaseDownloadService.cs b/allstarr/Services/Common/BaseDownloadService.cs index 995ef39..ef304fd 100644 --- a/allstarr/Services/Common/BaseDownloadService.cs +++ b/allstarr/Services/Common/BaseDownloadService.cs @@ -5,6 +5,7 @@ using allstarr.Models.Search; using allstarr.Models.Subsonic; using allstarr.Services.Local; using allstarr.Services.Subsonic; +using System.Collections.Concurrent; using TagLib; using IOFile = System.IO.File; @@ -27,7 +28,7 @@ public abstract class BaseDownloadService : IDownloadService protected readonly string DownloadPath; protected readonly string CachePath; - protected readonly Dictionary ActiveDownloads = new(); + protected readonly ConcurrentDictionary ActiveDownloads = new(); protected readonly SemaphoreSlim DownloadLock = new(1, 1); /// @@ -298,6 +299,14 @@ public abstract class BaseDownloadService : IDownloadService song.LocalPath = localPath; + // Clean up completed download from tracking after a short delay + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(5)); // Keep for 5 minutes for status checks + ActiveDownloads.TryRemove(songId, out _); + Logger.LogDebug("Cleaned up completed download tracking for {SongId}", songId); + }); + // Register BEFORE releasing lock to prevent race conditions (both cache and download modes) await LocalLibraryService.RegisterDownloadedSongAsync(song, localPath); @@ -360,6 +369,14 @@ public abstract class BaseDownloadService : IDownloadService { downloadInfo.Status = DownloadStatus.Failed; downloadInfo.ErrorMessage = ex.Message; + + // Clean up failed download from tracking after a short delay + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(2)); // Keep for 2 minutes for error reporting + ActiveDownloads.TryRemove(songId, out _); + Logger.LogDebug("Cleaned up failed download tracking for {SongId}", songId); + }); } Logger.LogError(ex, "Download failed for {SongId}", songId); throw;