Skip to content

fix(books): use the author-refresh match instead of independently re-deriving it - #181

Open
jordanfelle wants to merge 2 commits into
Chaptarr:developfrom
jordanfelle:fix-182-unify-book-refresh-matching
Open

jordanfelle wants to merge 2 commits into
Chaptarr:developfrom
jordanfelle:fix-182-unify-book-refresh-matching

Conversation

@jordanfelle

Copy link
Copy Markdown

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.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 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, 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.

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?

  • New tests in 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.
  • Full Chaptarr.Core.Test suite (3,022 tests) passes.
  • Deployed to a live instance alongside the two companion fixes and confirmed via live logging that a book's match now resolves consistently between the classification pass and the actual update pass.

Screenshots (UI changes only)

N/A - no frontend changes in this PR.

…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.
@jordanfelle

Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant