feat(merge): run the merge pipeline in a worker thread off the event loop (#242 Phase 3) - #249
Merged
Merged
Conversation
…ardcoded credential The suite hit a hardcoded external Navidrome with a plaintext password committed in the file, and failed 8/8 whenever that host was down. Now gated on NAVIDROME_TEST_URL (+ _USER/_PASSWORD) via describe.skipIf so it skips cleanly in CI and on machines without a configured server. The old password remains in git history and should be rotated. (refs #242) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q3riZ78G26ohMgPDh8F7u
…loop (closes #242) runMergePipelineAsync() spawns a node:worker_threads worker with its own better-sqlite3 connection (WAL readers on the main connection stay unblocked for the whole merge); in-memory DBs fall back in-process. A promise-chain mutex enforces one in-flight merge; shutdownMergeWorker() terminates the worker on app close before db.close(). The pipeline transaction is now run.immediate() so the write lock is taken up front. Worker entry resolves .ts (tsx dev/vitest, via tsx/esm/api tsImport) or compiled .js (prod image has no tsx). Integration tests are now wired into CI (root test:integration script + hub-job step); the new merge-worker integration test proves main-connection reads and event- loop ticks stay responsive while a 15k-track merge runs on the worker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q3riZ78G26ohMgPDh8F7u
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.
Closes #242 (Phase 3 of 3 — Phases 1–2 landed in #248 and are validated on the live instance).
better-sqlite3 is synchronous, so the merge pipeline — snapshot →
mergeLibraries→ id-remap → orphan audit, one transaction — stalled the entire hub event loop for the duration of every merge (200k–600k track scale envelope). This PR moves that transaction onto anode:worker_threadsworker with its own connection to the same file: WAL readers on the main connection are never blocked by the worker's write transaction, so streams, Subsonic clients, and federation requests keep being served while a merge runs.Design
runMergePipelineAsync(db, { logger, inProcess? })is now the production entry point; all four call sites (sync.ts,auto-sync.ts×2, admin peer-data reset)awaitit. For a file-backed DB it spawnsmerge-worker.ts, which opensnew Database(db.name)+ WAL/foreign_keys pragmas (no schema/migration re-run) and runs the sameexecuteMergePipelinecore. In-memory DBs (:memory:/anonymous — the unit-test path) fall back to the in-process synchronous run.shutdownMergeWorker()is wired intoserver.tsonClosebeforedb.close()— terminating mid-merge just rolls the transaction back.run.immediate()(was deferred): the write lock is acquired up front instead of upgrading mid-transaction, which matters once two connections write to the same file..jsunder compileddist/(the production image has no tsx),.tsunder tsx dev/vitest.execArgv: ["--import", "tsx"]alone does not reliably register tsx's hooks inside a worker on Node 22; the worker instead loads its pipeline import viatsx/esm/api'stsImportwhen running from.tssource (Node 22.18+'s native type-stripping loads the entry file itself but can't remap.jsspecifiers to.tssiblings). In prod the compiled tree resolves normally and tsx is never imported.logPipelineReportruns on the main thread when the report message arrives.Also in this PR
test:integrationscript + a CI step in the hub job.hub/test/subsonic.integration.test.tscarrying a hardcoded live Navidrome credential and failing 8/8 against a 502ing host. It is now env-gated (NAVIDROME_TEST_URL/_USER/_PASSWORD,describe.skipIf) and the plaintext password is removed from the source.Verification
pnpm verifygreen: hub 694/694 (59 files — +3 new merge-pipeline unit tests), frontend 108/108, typecheck +lint:boundaryclean.pnpm test:integrationgreen: 21 passed, 8 skipped (Navidrome-gated). Newmerge-worker.integration.test.tsseeds a 15k-track file DB, starts the worker merge, and proves the main connection answers reads and the event loop ticks (<100 ms) while the merge is still running, then asserts a clean report and fully populated unified tables.pnpm lintzero findings inhub/andfrontend/.node dist/server.js, so this exercises the compiled.jsworker path (merges run during the suite), not just the tsx dev path.Acceptance criteria from #242 (Phase 3)
app.onClose.🤖 Generated with Claude Code
https://claude.ai/code/session_011Q3riZ78G26ohMgPDh8F7u