Repository navigation
feat(calibre): queue deliveries and retry them from a scheduler job (#2832) - #2844
Merged
Merged
Conversation
…2832) Imports no longer push to Calibre inline. Each imported ebook file is queued in calibre_deliveries and the new calibre.Deliverer is kicked; a one minute scheduler job (calibre-deliver) runs it as well. A pass probes the bridge first (5s) and leaves every row untouched when Calibre is closed or unreachable, so a book imported while Calibre was closed is delivered once it is back. Failures back off 1m, 5m, 15m, 1h, 6h, then 24h and give up after 8 attempts; bad_format and path_forbidden give up at once. Transport errors and a 503 stop the pass without charging an attempt. Passes never overlap (TryLock shared by tick and kick). books.calibre_id now means the id in the library at calibre.library_path. A delivery fills it only when it is NULL, the book is not Calibre origin and the target is that library (or no library path is set). 409 ownership for the metadata refresh comes from the ledger instead of books.calibre_id. Cover resolution moves out of the Scanner into calibre.CoverSource, shared by the worker and Push all, which now sends covers too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This was referenced Sep 28, 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.
Summary
PR 2 of Phase 2 for #2832. Imports stop pushing to Calibre inline. They queue each imported ebook file in
calibre_deliveries(the ledger from #2841) and kick a new worker, which delivers with retry and backoff. A book imported while Calibre is closed is now delivered on its own once Calibre is reachable, within about a minute.Refs #2832
Worker (
internal/calibre/delivery_worker.go).DelivererwithEnqueue,KickandRunDeliveries. It is registered byscheduler.WithCalibreDelivererascalibre-deliver, running@every 1m. The tick and a kick share onesync.Mutextaken withTryLock, so passes never overlap. Kicks and ticks both run in the jobs group, so shutdown drains a pass that is in flight. A pass works like this:DueBatch(now, 50). An empty queue returns before any HTTP call.AdderResolver. In plugin mode it runsHealthDetailwith a 5s timeout. If the bridge is unreachable or degraded, the pass returns and no row is touched.book_filesrow. It skips the row if the book is gone, the file row is gone, or the file is missing on disk. It builds metadata at delivery time withBuildMetadata, taking the edition from the row'sedition_idor matching one by extension (editionForFile, factored out of the syncer), plus the series and the cover.MarkDelivered(id, "added", library). The library is what the plugin's health reports, orlibrary_pathfor calibredb.MarkDelivered(id, "already"). Ownership for the metadata PATCH now comes fromDeliveredByCalibreID: an earlier delivered row for this book whose target library matches, or is empty on a backfilled row.book.CalibreID == idno longer decides it.bad_format/path_forbidden: terminal. Anything else backs off 1m, 5m, 15m, 1h, 6h, then 24h, and goes terminal on the 8th attempt.ErrDisabled: the pass stops and no attempt is counted.calibre delivery passline with counts when anything happened. WARN on a row's first failure (calibre delivery: add failed, will retry) and on giving up (calibre delivery: giving up on a book, with book id and code).books.calibre_id. It now means the id in the library atcalibre.library_path, and the model field documents this. A delivery writes it only through the newSetCalibreIDIfUnset(WHERE calibre_id IS NULL), only when the book is not Calibre origin (isCalibreOrigin), and only whenlibrary_pathis empty or equals the target (sameLibraryPath, factored out ofsameCalibreLibrary). Existing values are never nulled or replaced.Importer.
pushToCalibre,pushCalibreAdd,reconcileExistingCalibreBookand the Scanner's cover code are gone. The ebook loop callsenqueueCalibreDeliveryfor each imported file. It passes thebook_filesid, the download'sEditionIDwhen it resolved to a real edition of the book, and the path; the format is the lower case extension. The worker is kicked once after the loop. The audiobook path queues nothing. External import mode is unchanged (it returns earlier), and so is the CWA mirror. With the integration off nothing is queued.Covers. Cover resolution now lives in
calibre.CoverSource.PathForand is shared by the worker and Push all. Push all never setCoverPathbefore; it now sends covers (Syncer.WithCovers). Nothing else in Push all changes. That is PR 3.Plugin client. A rejection is now a typed
*PluginError{Status, Code, Detail}, read withPluginErrorCode(err), and the error string is unchanged.HealthStategainsLibrary.Concurrency with Push all. A Push all run and the worker can push the same book at the same time. That is safe: the bridge dedupes on the
binderyidentifier, so the second push comes back as a 409 and is recorded asalready. In calibredb mode,calibredb addwithout--duplicatesskips a book it already holds, but Push all is plugin only anyway.Deviations from the plan
DueBatchruns before the health probe. Without that, an idle queue would cost a request to the Calibre host every minute. The probe still runs before any row is touched.db_unavailable), not that the book is bad.context.WithoutCancel, so a delivery that completes during shutdown is still recorded.How it was verified
go build ./... && go vet ./...pass.GOOS=windows go build ./...passes.go test ./cmd/... ./internal/...exits 0, with the exit code read directly and not piped.golangci-lint runon calibre, importer, scheduler, db, models and cmd: 0 issues.BINDERY_PLUGIN_SRC=... go test ./internal/calibre/ -run Contract) and passed. The plugin's documented codes (bad_format,path_forbidden,db_unavailableon 503) match the codes the worker branches on.calibre_idunder the rule (7 cases, including Calibre origin, a different library and an existing id); 409 ownership from the ledger (4 cases); terminal codes; the backoff schedule and the 8th attempt; a transport error stops the pass; a 503 is not an attempt;ErrDisabled; a missing file is skipped; calibredb mode; overlapping kick and tick deliver once; metadata is built at delivery time; the realPluginClientagainst httptest, both down and up.SetCalibreIDIfUnset.origin/mainwith main API variants of the tests:the import waited 6s on Calibre (adder called 2 times inline)andqueued rows = 0.bulk push sent no cover.TestPushToCalibre_409UpdatesARowBinderyOwnsencodes (book.CalibreID == id). The new "books.calibre_id alone is not ownership" case asserts the opposite.go vet, and its named test failed:OtherErrorsBackOffThenGiveUp.TerminalPluginCodesFailAtOnce.UnreachableBridgeLeavesRowsUntouched.calibre_idguard with the origin check, the library check or the existing-id check removed:SuccessMarksDeliveredAndRecordsCalibreID.OverlappingPassesDoNotDoubleDeliver.books.calibre_id:409MarksAlreadyAndOwnershipComesFromTheLedger.TransportErrorStopsThePassWithoutAnAttempt.🤖 Generated with Claude Code
https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9