Uh oh!
There was an error while loading. Please reload this page.
fix(service-datasource): one live introspection per datasource per validation sweep - #11371
Conversation
…lidation sweep validateAll/validateDatasource used to call config.introspect(datasource) once per federated OBJECT, all concurrently — M objects on one datasource dialled the same remote M times per sweep. The sweep now threads a per-call memo (sweepScopedIntrospect) through the validation body: the in-flight introspection promise is memoised by datasource for the lifetime of ONE validateEach call and discarded when the call returns, so a long-lived service never serves a stale schema to a later sweep. Direct validateObject calls keep reading live on every call. Pins: counting fakes assert one read per datasource per sweep (both sweep spellings), a second sweep reads live again and sees a remote change (per-call, not per-instance), direct calls stay live, and one unreachable remote costs one connection attempt while every object on it still gets its failure row. The IExternalDatasourceService.validateAll docstring stops promising "parallelised per datasource" for an implementation that parallelised per object. Fixes#10962 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
…on-memo Joint resolution with #11166 (unreachable diff kind, merged 14:17Z): validateEach keeps this branch's memoised reader (validateObjectUsing + sweepScopedIntrospect) AND main's 'unreachable' kind in the per-object catch; both docblock additions kept. The new counting pin for an unreachable remote now asserts kind 'unreachable' (a throw out of introspect is never an invented missing_table); the second-sweep and direct-call pins keep asserting missing_table because their schemas are successfully READ and lack the table - exactly the structural line #11166 draws. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
📓 Docs Drift Check6 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 126 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 a31d125b1b4ca78d87effeacebb755c4b163bd1c && git checkout a31d125b1b4ca78d87effeacebb755c4b163bd1c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 735f5c7099d627cb31881ba6f797953b5db74420 92809ceb23798d56628af6ab6a156b710718b51a && git checkout -B drift-repro 735f5c7099d627cb31881ba6f797953b5db74420 && git merge --no-ff 92809ceb23798d56628af6ab6a156b710718b51a
node scripts/docs-audit/affected-docs.mjs --json 735f5c7099d627cb31881ba6f797953b5db74420 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10962
What
validateAll/validateDatasourceperformed one liveintrospect(datasource)round-trip per federated object, all concurrently — M objects on one datasource dialled the same remote M times per sweep. The sweep now threads a per-call introspection memo through the validation body:sweepScopedIntrospect()returns a reader that memoises the in-flight introspection promise by datasource name for the lifetime of onevalidateEachcall. The memo is a local of that call — it dies when the call returns.validateObject's body moved to a privatevalidateObjectUsing(objectName, readSchema); the public method passesconfig.introspectdirectly (a direct call is always a live read), the sweep passes the memoised reader. Public signatures are unchanged.Promise.allsweep share one read; a rejected read is shared the same way — one connection attempt, M failure rows.Why per-call, not per-instance (the design constraint from triage): a
Mapon the service instance would also collapse M reads into 1 — and would serve last sweep's schema to every later sweep for the life of the process. The memo's lifetime is one call, so a long-lived service always answers a new sweep from the remote's schema as it is now.Proof — the counting pins
New block
per-sweep introspection memo [#10962]inexternal-datasource-service.test.ts(counting fake records everyintrospectcall):validateAll/validateDatasourcewith M=3 objects on one datasource: one read per datasource per sweep.validateAll()on the same instance reads live again (count 2), and sees a remote change made between sweeps (tables dropped →missing_table, a fact read from a returned schema). A per-instance cache passes every counting pin above and fails exactly this one.validateObjecttwice: two live reads, remote change seen — no cross-call reuse.kind: 'unreachable'per An UNREACHABLE federated remote is reported asmissing_table— a transient outage is indistinguishable from a dropped table, and aborts boot under the default onMismatch: 'fail' #11166's ruling — see merge note below).Ablation at
890f8e7cfd(mutation confirmed on disk by anchored greps; predicted signature written first): bypassing the memo turns exactly the 4 counting pins red (4 failed | 36 passed) while every correctness test — including the #11241 refusal-path pin (nothing persisted, nothing introspected) — stays green; restore proven byte-identical viagit hash-object, all green. No rebuild needed: the suite imports../external-datasource-service.jssource-relative.Merge with
mainat92809ceb23— joint resolution with #11166#11166 (unreachable diff kind, merged 14:17Z) rewrote the same
validateEachhunks. Resolution keeps both sides whole: the memoised reader (this branch) plus theunreachablekind in the per-object catch (main), both docblock additions kept. The new counting pin for an unreachable remote now assertskind: 'unreachable'(a throw out ofintrospectis never an inventedmissing_table); the second-sweep and direct-call pins keep assertingmissing_tablebecause their schemas are successfully read and lack the table — exactly the structural line #11166 draws.The one
packages/speclinecontracts/external-datasource-service.ts:157documentedvalidateAllas "parallelised per datasource" while the implementation parallelised per object. The docstring now states the actual behaviour: parallel per object, one live schema read per datasource per call. Comment prose only — no accept/reject change, no surface widened.Verification at
92809ceb23(merged head)pnpm --filter @objectstack/service-datasource test: 577 passed (26 files); target file verbose: 42/42, with the five memo pins, the fix(service-datasource): refuse an importObject name override that violates the namespace prefix #11241 refusal pin, and An UNREACHABLE federated remote is reported asmissing_table— a transient outage is indistinguishable from a dropped table, and aborts boot under the default onMismatch: 'fail' #11166's own classification pin all listed by name and green.pnpm --filter @objectstack/service-datasource typecheck: clean.pnpm --filter @objectstack/spec build && check:generated: all 14 artifacts up to date (run post-merge-commit, never in MERGE state).scripts/pm/dispatch-gates.mjsat92809ceb23, no hand-supplied paths): 26 gates re-run to real verdicts, all green. Declared narrowings, receipts in the dev report on the issue:check-dev-prereqs(its own verdict names only "workspace is not built" — 49 unbuilt packages, none in this diff) andcheck:type-check-debt(neither touched package carries a DEBT/TEST_DEBT ledger entry; service-datasource's tsc program includes its tests and ran clean).Scope notes
@objectstack/service-datasource(behavioural fix, no API change) and@objectstack/spec(docstring correction shipped in the package).Clause-②: yes — by the PATH limb (the diff touches
packages/spec/src/contracts/external-datasource-service.ts); content limb no (comment prose only). This PR stays draft and waits on the review chain.🤖 Generated with Claude Code
https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
Generated by Claude Code