Repository navigation
fix(metadata-protocol): a seed that stores many seedFile values no longer starves sign-ins (yield per row, 20-row batches) - #26822
Conversation
… so a large file seed does not starve requests On a synchronous driver every await of the seed's per-row file work resolves as a microtask, so the claims settled after each 200-row insert batch ran as one unbroken chain and requests (sign-ins) waited unread behind it. Each row's file work, stored and settled, now ends with one macrotask turn. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B
…atches of 20 Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B
…ing requests Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B
…eld pin Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B
…insert path; add changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 94015eabff04184baf366d001c3abd59c5d0599f && git checkout 94015eabff04184baf366d001c3abd59c5d0599f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0ea90c7a18504e5b67f137aefe7d3e7bb0b7157b 4bf76001838f9e0d4884a9903c5fba15221aa440 && git checkout -B drift-repro 0ea90c7a18504e5b67f137aefe7d3e7bb0b7157b && git merge --no-ff 4bf76001838f9e0d4884a9903c5fba15221aa440
node scripts/docs-audit/affected-docs.mjs --json 0ea90c7a18504e5b67f137aefe7d3e7bb0b7157b
|
席位核对 · ACCEPT ·
|
Fixes #26670
Clause-②: no
Metadata-protocol: none
维护者速读
packages/metadata-protocol/src/seed-files.ts(yieldToEventLoop及两处调用点,settleRow的finally);packages/metadata-protocol/src/seed-loader.ts(insertBatchSize)。pnpm --filter @objectstack/metadata-protocol exec vitest run src/seed-loader-seed-files.test.ts,预期 19 passed,其中「a seed of many files yields to waiting requests (A seed that stores manyseedFilefiles holds the sqlite pool: sign-ins during the boot seed time out (Knex: Timeout acquiring a connection) #26670)」三条全绿。What changes
The reading first: which call holds what. The card's seam ("the seed loader holds a pooled connection or a transaction") was marked unverified, so the first measurement was the pool itself. No call on the seed-file path opens a transaction: none in
seed-loader.ts,seed-files.tsorfield-file-service.ts, nor in the boot runner (app-plugin.tsrunInlineSeed). Every step is sequential, and there is noPromise.all. Measured on the real composition (os serve --dev, file-backed sqlite, 1,500 rows each holding one 20 KBseedFile, sign-in every 250 ms while the background seed runs). A preload patched tarn'sPool(acquire, release) and sampled event-loop lag with a 100 ms timer:a485b5a3)abc4ef3b;srcunchanged since)The pool was never starved. The event loop was. On a synchronous driver (better-sqlite3) every
awaitof a seed file's roughly 25 engine calls (thependingsys_filerow, the bytes, thenclaimFiles: describe,ownedBySlot, release, commit, each with its write hooks and audit rows) resolves as a microtask. The loader buffers inserts into 200-row batches, and after each flushsettleSeedFilesclaims the batch's rows back to back. Those claims involve no I/O at all, so a whole batch ran as one unbroken microtask chain, and a sign-in arriving meanwhile was not even read off the socket until it ended. The stalls landed once per batch (about 8 for 1,500 rows), and a CPU profile put the time in better-sqlite3's synchronous_queryandprepare. Control on the same boot without file values: 1,500 rows and also 30,000 rows finish inside the 8 s inline budget, with about 2,140 acquires in all, so the cost is the per-row file work and not row count.The fix, in the producer (
packages/metadata-protocol, inside the claim's file surface):seed-files.tsgetsyieldToEventLoop(), onesetImmediateturn. This is the cure@objectstack/core's import runner already applies to the same trap.prepareRowends with it when the row stored or kept files, andsettleRowends with it (in afinally) after every row's claim. A request now waits behind one row's files, never behind a batch's.seed-loader.tswrites a dataset that carries seed files (SeedDatasetFiles.carriesFiles, the bundledfilesmap is non-empty) in insert batches ofSEED_FILE_BATCH_SIZE = 20instead of 200. With the settle loop broken up, the remaining stall was the singleinsertManyof 200 file-bearing rows: about 1.5 s, since each row's file-reference hooks cost about 7 ms on sqlite. At 20 rows that is about 150 ms. Datasets without seed files keep the 200-row batch.⛔ No pool size or timeout was raised. The seed's wall time is unchanged within boot noise (about 25–30 s of seed in each 1,500-file run).
packages/metadata-protocol/src/protocol.tsandpackages/services/service-storage/src/field-file-service.tsare untouched: neither holds anything.Tests
New
describeinpackages/metadata-protocol/src/seed-loader-seed-files.test.ts. It seeds 60 files on an object with no self-reference, so the loader takes the batched-insert path the stall was measured on. The suite's fake engine and field-file service resolve purely in microtasks, which is the property of better-sqlite3 that matters, so the pin is deterministic without a driver:stores at most one row's files and settles at most one row's claim between two macrotask turns: samples progress on everysetImmediateturn while the load runs.serves an unrelated read issued during the seed within one row's file work: asys_userread issued from a macrotask, standing in for the sign-in's user lookup.writes a dataset that carries seed files in insert batches of at most 20 rows.Ablations (each mutation landed and restored by
scripts/ablation-replace.mjs, which checks the anchor and blob counts and proves the restore against the HEAD blob; the test imports source, so nodist/is involved):yieldToEventLoopresolves at once. Pins 1 and 2 red:no macrotask turn ran while the seed ran: expected 0 to be greater than 1,the read never completed: expected undefined to be defined. 2 failed, 17 passed.SEED_FILE_BATCH_SIZE = 200. Pin 3 red:expected 60 to be less than or equal to 20. 1 failed, 18 passed.expected { maxStored: 1, maxClaims: 20 } to deeply equal { maxStored: 1, maxClaims: 1 }, which is a whole batch's claims settled with no turn, the measured shape. The first attempt at C was a no-op (the tool refused: the replacement was a substring of its anchor, so the test never ran); it was re-run with a distinct replacement.clm_version, whose self-lookup sends the loader down the sequential per-record path. Leg B stayed green against it, which exposed the pins as vacuous for the batched path. They were moved toclm_docbefore the legs above.Runs on the final head
4bf7600183unless noted:Readings
pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/seed-loader-seed-files.test.tsat4bf7600183:Tests 19 passed (19), lock VERDICT command-exit 0.pnpm --filter @objectstack/metadata-protocol typecheckat4bf7600183: exit 0 (same&&chain). The test file is in the tsc program (--listFilesnames it once).pnpm --filter @objectstack/metadata-protocol testatd294c889(the merge oforigin/main; the only later commit types one test literal):Test Files 269 passed | 3 skipped (272),Tests 21619 passed | 19 skipped (21638), after rebuilding the package's dependency closure (pnpm --filter '@objectstack/metadata-protocol^...' build).pnpm exec eslinton the three changed source files: exit 0.Acceptance notes
Knex: Timeout acquiring a connectionrefusal. The measured pool waits never exceeded 21 ms, so this run shows the sign-in slowdown (5.2 s at worst on 1,500 small files) and its cause, not the refusal. hotclm's 28 s sign-in and its refusals are consistent with longer per-batch stalls (bigger files, more write hooks), but that is inference, not a reading.emitSeedSettled, thenclaimSeedOwnershipOnSettle, thenclaimSeedOwnership(@objectstack/plugin-security, the ADR-0138 hand-over onapp:seeded), with a driverupdateManyand per-row insert and audit frames. It re-owns the seeded rows in one unbroken chain, scales with rows rather than files, and runs for any seed that overruns the inline budget. Reported to the seat, not fixed here.Verification
Gate families derived for this diff by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat4bf7600183(59 commands), each run on4bf7600183with its exit code captured before any pipe.--ranreconciliation:59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN(a derived zero: every line records its exit code).59 gate commands, exit codes
node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0node scripts/check-adr-0087-registration.mjs --self-test: exit 0node scripts/check-changeset-no-major.mjs --base origin/main: exit 0node scripts/check-closing-keyword-parity.mjs: exit 0node scripts/check-closing-keyword-parity.mjs --self-test: exit 0node scripts/check-comment-mask-adoption.mjs: exit 0node scripts/check-comment-mask-adoption.mjs --self-test: exit 0node scripts/check-comment-mask-corpus.mjs: exit 0node scripts/check-dts-emitted.mjs --self-test: exit 0node scripts/check-empty-changeset.mjs --base origin/main: exit 0node scripts/check-empty-changeset.mjs --self-test: exit 0node scripts/check-issue-citations.mjs: exit 0node scripts/check-plugin-teardown-shape.mjs: exit 0node scripts/check-plugin-teardown-shape.mjs --self-test: exit 0node scripts/ci/assert-turbo-remote-cache.mjs --self-test: exit 0node scripts/ci/plan-test-core-shards.mjs --self-test: exit 0node scripts/ci/prune-turbo-cache.mjs --self-test: exit 0node scripts/docs-audit/check-affected-docs.mjs: exit 0node scripts/docs-audit/check-drift-comment.mjs: exit 0node scripts/pm/release-rehearsal-clone.mjs --self-test: exit 0node scripts/release-pending-publish.mjs --self-test: exit 0pnpm --filter @objectstack/spec run check:duration-unit-keys: exit 0pnpm check:changeset-gate-self-tests: exit 0pnpm check:ci-filter-parity: exit 0pnpm check:closed-ceremony-text: exit 0pnpm check:cross-package-test-inputs: exit 0pnpm check:doc-authoring: exit 0pnpm check:driver-memory-census: exit 0pnpm check:dts-closure: exit 0pnpm check:dual-build-cjs-loads: exit 0pnpm check:durability-log-level: exit 0pnpm check:engine-double-contract: exit 0pnpm check:error-status-conformance: exit 0pnpm check:gitlink-declared: exit 0pnpm check:issue-citations: exit 0pnpm check:keyed-text-bounds: exit 0pnpm check:lean-entry-closure: exit 0pnpm check:logger-receiver-detach: exit 0pnpm check:nul-bytes: exit 0pnpm check:objectql-double-limit: exit 0pnpm check:org-identifier: exit 0pnpm check:page-declaration-shape: exit 0pnpm check:platform-object-tenancy-census: exit 0pnpm check:pm-changeset-deadline-census: exit 0pnpm check:published-files: exit 0pnpm check:query-options-erasure: exit 0pnpm check:refd-timer-probe: exit 0pnpm check:registry-log-declared: exit 0pnpm check:rest-log-spy-declared: exit 0pnpm check:slot-lookup: exit 0pnpm check:sourcemap-no-sources-content: exit 0pnpm check:system-context-census: exit 0pnpm check:test-source-alias: exit 0pnpm check:tier-file-adoption: exit 0pnpm check:type-check-coverage: exit 0pnpm check:type-check-debt: exit 0pnpm check:undeclared-dep-imports: exit 0pnpm check:watch-hint-literal: exit 0pnpm check:where-matcher: exit 0d294c889) caught one real red:check:query-options-erasure, where the test surface grew 236 to 237 through a stand-in read'sas any. It was fixed in4bf7600183by typing the literal. That union also readcheck-plugin-teardown-shape --self-testas a prerequisite miss (shallow clone; its pinned fixture commit was fetched) andcheck:dual-build-cjs-loadsas PREREQUISITE NOT MET. Both are green above on a real measurement.test:repo(the vitestrepoproject; the diff touches none of its tests), the repo-widepnpm lint, the type-check lanes, and the artifact-roster and wide-population families the derivation lists outside its 59.!in the title,Clause-②: nowith no arm).Generated by Claude Code