fix(books): use the author-refresh match instead of independently re-deriving it - #181
Open
jordanfelle wants to merge 2 commits into
Open
jordanfelle wants to merge 2 commits into
jordanfelle wants to merge 2 commits into
Conversation
…deriving it (Chaptarr#182) RefreshAuthorService's SortChildren already decides, once, which remote item matches each local book (GetMatchingExistingChildren) - that decision is what buckets a book as Added/Updated/UpToDate/Deleted. But when the actual per-book refresh runs, RefreshBookService.GetRemoteData throws that decision away and independently re-derives its own match via BookIdentity.FindWorkFirstMatches(...).FirstOrDefault() over the same raw remote list. When the remote list has more than one plausible candidate for a book (e.g. the metadata source returning multiple entries that share a provider id), these two passes can disagree. Confirmed live: a book correctly classified as Updated (SortChildren's comparison found a real title difference) never actually got the title written, refresh after refresh, because GetRemoteData's own separate match resolved to a different candidate that already looked identical to what was stored - "N books updated" on every single refresh, nothing ever actually converging. Adds SortedChildren.MatchedRemoteByLocalId (populated where the match is actually decided, in SortChildren) and threads it through a new, non-interface RefreshBookInfo overload - same reasoning as BookService.UpdateMany's existing hint overload: adding a parameter to IRefreshBookService would break every hand-written test double, so RefreshAuthorService holds a concrete RefreshBookService reference check instead. GetRemoteData consults the hint first and only falls back to independent re-derivation when there isn't one for that book id, so every other RefreshBookInfo caller (imports, discovery, manual single-book refresh) is completely unaffected.
jordanfelle
added a commit
to jordanfelle/chaptarr
that referenced
this pull request
Sep 26, 2026
jordanfelle
added a commit
to jordanfelle/chaptarr
that referenced
this pull request
Sep 26, 2026
…gleton RefreshBookService is a singleton and RefreshAuthor commands for different authors run concurrently (RefreshAuthorCommand neither requires disk access nor is type exclusive, and there are several command threads), so a plain instance field for the per-refresh matched-remote hint was shared between refreshes: one refresh could match against another author's hint, and the finally block cleared it under a refresh still using it. Hold it in an AsyncLocal. A deterministic two-flow test fails with a shared field.
Author
|
Found in an adversarial review of #218: this PR's per-call hint was an instance field on the singleton RefreshBookService, justified by 'only one disk-access command runs at a time', but RefreshAuthorCommand neither requires disk access nor is type exclusive, so per-author refreshes run concurrently (3 by default, more with #222). One refresh could read another author's hint, and the finally block cleared it under a refresh still using it. Added a commit that holds the hint in an AsyncLocal (property backing, call sites unchanged) and a deterministic two-flow test that fails with a shared field. |
jordanfelle
added a commit
to jordanfelle/chaptarr
that referenced
this pull request
Sep 26, 2026
jordanfelle
added a commit
to jordanfelle/chaptarr
that referenced
this pull request
Sep 27, 2026
jordanfelle
added a commit
to jordanfelle/chaptarr
that referenced
this pull request
Sep 27, 2026
jordanfelle
added a commit
to jordanfelle/chaptarr
that referenced
this pull request
Sep 27, 2026
jordanfelle
added a commit
to jordanfelle/chaptarr
that referenced
this pull request
Sep 27, 2026
jordanfelle
added a commit
to jordanfelle/chaptarr
that referenced
this pull request
Sep 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
RefreshAuthorService's SortChildren already decides, once, which remote item matches each local book (
GetMatchingExistingChildren) - that decision is what buckets a book as Added/Updated/UpToDate/Deleted. But when the actual per-book refresh runs,RefreshBookService.GetRemoteDatathrows that decision away and independently re-derives its own match viaBookIdentity.FindWorkFirstMatches(...).FirstOrDefault()over the same raw remote list.When the remote list has more than one plausible candidate for a book (e.g. the metadata source returning multiple entries that share a provider id), these two passes can disagree. Confirmed live: a book correctly classified as changed (SortChildren's comparison found a real difference) never actually got the field written, refresh after refresh, because
GetRemoteData's own separate match resolved to a different candidate.Adds
SortedChildren.MatchedRemoteByLocalId(populated where the match is actually decided, inSortChildren) and threads it through a new, non-interfaceRefreshBookInfooverload - same reasoning asBookService.UpdateMany's existing hint overload: adding a parameter toIRefreshBookServicewould break every hand-written test double, soRefreshAuthorServiceholds a concreteRefreshBookServicereference check instead.GetRemoteDataconsults the hint first and only falls back to independent re-derivation when there isn't one for that book id, so every otherRefreshBookInfocaller (imports, discovery, manual single-book refresh) is completely unaffected.This is one of three related fixes (companion PRs make match-claim order deterministic, and fix a separate comparison-vs-apply mismatch); together they resolve a "books reported as changed on every single refresh, forever" symptom I found while investigating a cover-reconciliation performance issue.
Database Migration
NO
How was this tested?
RefreshBookServiceProviderMatchingFixture: with an ambiguous remote list (two candidates that both legitimately match by provider id and media type), a hint pointing at either candidate is honored regardless of list order; a hint entry for a different book id is correctly ignored and falls back to normal matching.Chaptarr.Core.Testsuite (3,022 tests) passes.Screenshots (UI changes only)
N/A - no frontend changes in this PR.