From 3a360d7a1e10ff44b014f9a7367784745bfe71b1 Mon Sep 17 00:00:00 2001 From: ta264 Date: Sun, 6 Feb 2022 18:58:14 +0000 Subject: [PATCH] Fixed: Contain memory usage during library import --- src/Directory.Packages.props | 2 + src/NzbDrone.Core/Books/Model/Author.cs | 2 + .../Identification/IdentificationService.cs | 4 +- .../BookImport/ImportDecisionMaker.cs | 1 + .../BookImport/Manual/ManualImportService.cs | 6 +- .../MetadataSource/BookInfo/BookInfoProxy.cs | 23 +++++- .../Parser/Model/LocalEdition.cs | 73 +++++++++++++++++-- src/NzbDrone.Core/Readarr.Core.csproj | 2 + 8 files changed, 100 insertions(+), 13 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 27934a893..fc35da267 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -13,8 +13,10 @@ + + diff --git a/src/NzbDrone.Core/Books/Model/Author.cs b/src/NzbDrone.Core/Books/Model/Author.cs index 9a5364e57..cc68cc315 100644 --- a/src/NzbDrone.Core/Books/Model/Author.cs +++ b/src/NzbDrone.Core/Books/Model/Author.cs @@ -77,7 +77,9 @@ public override void UseDbFieldsFrom(Author other) RootFolderPath = other.RootFolderPath; Added = other.Added; QualityProfileId = other.QualityProfileId; + QualityProfile = other.QualityProfile; MetadataProfileId = other.MetadataProfileId; + MetadataProfile = other.MetadataProfile; Tags = other.Tags; AddOptions = other.AddOptions; } diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Identification/IdentificationService.cs index b99926e06..0b8e24472 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Identification/IdentificationService.cs @@ -74,7 +74,7 @@ public List Identify(List localTracks, IdentificationOv // 3 find best candidate var watch = System.Diagnostics.Stopwatch.StartNew(); - _logger.Debug("Starting track identification"); + _logger.Debug("Starting book identification"); var releases = GetLocalBookReleases(localTracks, config.SingleRelease); @@ -183,7 +183,7 @@ private void IdentifyRelease(LocalEdition localBookRelease, IdentificationOverri _logger.Debug($"Best release found in {watch.ElapsedMilliseconds}ms"); - localBookRelease.PopulateMatch(); + localBookRelease.PopulateMatch(config.KeepAllEditions); _logger.Debug($"IdentifyRelease done in {watch.ElapsedMilliseconds}ms"); } diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/BookImport/ImportDecisionMaker.cs index 3a83dc367..3bc6bfe61 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/ImportDecisionMaker.cs @@ -42,6 +42,7 @@ public class ImportDecisionMakerConfig public bool SingleRelease { get; set; } public bool IncludeExisting { get; set; } public bool AddNewAuthors { get; set; } + public bool KeepAllEditions { get; set; } } public class ImportDecisionMaker : IMakeImportDecision diff --git a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs index 79b30023d..71a8f6173 100644 --- a/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/BookImport/Manual/ManualImportService.cs @@ -117,7 +117,8 @@ public List GetMediaFiles(string path, string downloadId, Auth NewDownload = true, SingleRelease = false, IncludeExisting = !replaceExistingFiles, - AddNewAuthors = false + AddNewAuthors = false, + KeepAllEditions = true }; var decision = _importDecisionMaker.GetImportDecisions(files, null, null, config); @@ -162,7 +163,8 @@ private List ProcessFolder(string folder, string downloadId, A NewDownload = true, SingleRelease = false, IncludeExisting = !replaceExistingFiles, - AddNewAuthors = false + AddNewAuthors = false, + KeepAllEditions = true }; var decisions = _importDecisionMaker.GetImportDecisions(authorFiles, idOverrides, itemInfo, config); diff --git a/src/NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs b/src/NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs index 9c72099a1..a0fecc42c 100644 --- a/src/NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/BookInfo/BookInfoProxy.cs @@ -5,6 +5,9 @@ using System.Net; using System.Text.Json; using System.Threading; +using LazyCache; +using LazyCache.Providers; +using Microsoft.Extensions.Caching.Memory; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; @@ -35,7 +38,7 @@ public class BookInfoProxy : IProvideAuthorInfo, IProvideBookInfo, ISearchForNew private readonly Logger _logger; private readonly IMetadataRequestBuilder _requestBuilder; private readonly ICached> _cache; - private readonly ICached _authorCache; + private readonly CachingService _authorCache; public BookInfoProxy(IHttpClient httpClient, ICachedHttpResponseService cachedHttpClient, @@ -55,8 +58,13 @@ public BookInfoProxy(IHttpClient httpClient, _editionService = editionService; _requestBuilder = requestBuilder; _cache = cacheManager.GetCache>(GetType()); - _authorCache = cacheManager.GetRollingCache(GetType(), "authorCache", TimeSpan.FromMinutes(5)); _logger = logger; + + _authorCache = new CachingService(new MemoryCacheProvider(new MemoryCache(new MemoryCacheOptions { SizeLimit = 10 }))); + _authorCache.DefaultCachePolicy = new CacheDefaults + { + DefaultCacheDurationSeconds = 60 + }; } public HashSet GetChangedAuthors(DateTime startTime) @@ -532,7 +540,16 @@ private void AddDbIds(string authorId, Book book, Dictionary PollAuthorUncached(foreignAuthorId)); + return _authorCache.GetOrAdd(foreignAuthorId, + () => PollAuthorUncached(foreignAuthorId), + new LazyCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10), + ImmediateAbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10), + Size = 1, + SlidingExpiration = TimeSpan.FromMinutes(1), + ExpirationMode = ExpirationMode.ImmediateEviction + }.RegisterPostEvictionCallback((key, value, reason, state) => _logger.Debug($"Clearing cache for {key} due to {reason}"))); } private Author PollAuthorUncached(string foreignAuthorId) diff --git a/src/NzbDrone.Core/Parser/Model/LocalEdition.cs b/src/NzbDrone.Core/Parser/Model/LocalEdition.cs index ca40b82ac..5d41993f1 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEdition.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEdition.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Books; using NzbDrone.Core.MediaFiles.BookImport.Identification; +using SixLabors.ImageSharp.Processing; namespace NzbDrone.Core.Parser.Model { @@ -35,17 +36,77 @@ public LocalEdition(List tracks) public List ExistingTracks { get; set; } public bool NewDownload { get; set; } - public void PopulateMatch() + public void PopulateMatch(bool keepAllEditions) { if (Edition != null) { LocalBooks = LocalBooks.Concat(ExistingTracks).DistinctBy(x => x.Path).ToList(); - foreach (var localTrack in LocalBooks) + + if (!keepAllEditions) { - localTrack.Edition = Edition; - localTrack.Book = Edition.Book.Value; - localTrack.Author = Edition.Book.Value.Author.Value; - localTrack.PartCount = LocalBooks.Count; + // Manually clone the edition / book to avoid holding references to *every* edition we have + // seen during the matching process + var edition = new Edition(); + edition.UseMetadataFrom(Edition); + edition.UseDbFieldsFrom(Edition); + + var fullBook = Edition.Book.Value; + + var book = new Book(); + book.UseMetadataFrom(fullBook); + book.UseDbFieldsFrom(fullBook); + book.Author.Value.UseMetadataFrom(fullBook.Author.Value); + book.Author.Value.UseDbFieldsFrom(fullBook.Author.Value); + book.Author.Value.Metadata = fullBook.AuthorMetadata.Value; + book.AuthorMetadata = fullBook.AuthorMetadata.Value; + book.BookFiles = fullBook.BookFiles; + book.Editions = new List { edition }; + + if (fullBook.SeriesLinks.IsLoaded) + { + book.SeriesLinks = fullBook.SeriesLinks.Value.Select(l => new SeriesBookLink + { + Book = book, + Series = new Series + { + ForeignSeriesId = l.Series.Value.ForeignSeriesId, + Title = l.Series.Value.Title, + Description = l.Series.Value.Description, + Numbered = l.Series.Value.Numbered, + WorkCount = l.Series.Value.WorkCount, + PrimaryWorkCount = l.Series.Value.PrimaryWorkCount + }, + IsPrimary = l.IsPrimary, + Position = l.Position, + SeriesPosition = l.SeriesPosition + }).ToList(); + } + else + { + book.SeriesLinks = fullBook.SeriesLinks; + } + + edition.Book = book; + + Edition = edition; + + foreach (var localTrack in LocalBooks) + { + localTrack.Edition = edition; + localTrack.Book = book; + localTrack.Author = book.Author.Value; + localTrack.PartCount = LocalBooks.Count; + } + } + else + { + foreach (var localTrack in LocalBooks) + { + localTrack.Edition = Edition; + localTrack.Book = Edition.Book.Value; + localTrack.Author = Edition.Book.Value.Author.Value; + localTrack.PartCount = LocalBooks.Count; + } } } } diff --git a/src/NzbDrone.Core/Readarr.Core.csproj b/src/NzbDrone.Core/Readarr.Core.csproj index e3820c66c..309b2d63d 100644 --- a/src/NzbDrone.Core/Readarr.Core.csproj +++ b/src/NzbDrone.Core/Readarr.Core.csproj @@ -4,9 +4,11 @@ + +