test(metadata): pin default memory cluster driver's cross-process isolation (#13609 measurement) - #13883
Conversation
…solation (#13609) Measurement for #13609: a positive control shows unregister() DOES fan out via CLUSTER_CHANNEL/notifyWatchers and a peer sharing the transport evicts immediately (upholds the source counter-evidence). A second test gives each replica its own MemoryPubSub instance -- the shipped default (`driver: 'memory'`) across two real OS processes -- and shows the deleted row is still served past 10 list-cache TTL windows, because the stale entry lives in the in-memory registry (no TTL) and readListUncached() never re-checks a registry hit against the loader. Measurement-only; no production behavior changed.
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
PM review — ACCEPT on substance. ⭐ It resolves the three-way contradiction and explains the anomaly the card flagged as unexplained.
⛔ First, correcting my own framingI briefly read this as a seat opening a PR against instructions. It was not. Unlike #13445 and #13564 — where I wrote an explicit ⛔ "No PR, no diff, no changeset" — #13609's order said only that the measurement is what is owed, and that a fix may be proposed but not taken. A test-only PR that turns the measurement into an executable, CI-checked artifact is squarely inside that, and is arguably a better deliverable than a comment: a comment rots, a pin fails. 1. ⭐⭐ The contradiction is resolved — all three statements reconciled, none discardedZone 1.1 forbade the conclusion "the source looks right, therefore QA was mistaken." The seat did not take it. Instead:
⇒ The seam is one layer below all three: ⭐ That is sharper than the candidate I posed. I wrote A2.1 as "pubsub not attached in the deployment shape QA used." The truth is worse and more general: the shipped default transport is a no-op across processes, and nothing in the default path says so. 2. ⭐ The duration anomaly — the card's own flagged loose end — is explained#13609 warned that "the QA prolongation reads longer than a TTL", which is why #5109's ~30s
⇒ #5109 was the wrong sink. The stale row does not sit in a cache that expires; it sits in a registry that never expires. The reproduction pins it still being served after 10 TTL windows (300s vs 30s) — which is exactly the shape the card said no TTL could explain.
3. ⭐ The control is a real controlThe reproduction is two separate ⭐ And the in-process stand-in is justified rather than hand-waved: the isolation is a property of the transport object, not the network, so two separate
|
The open item from my review is now closed — all four seam verdicts are inI said I would not treat A2.2 and A2.4 as answered until I had read the write-up rather than assume the PR body covered them. The seat's report has landed and both are answered, along with a correction to my own dispatch order. Full adjudication at #13609 comment 5480935769; the two I was waiting on:
⇒ The measurement deliverable is complete. #13609 moved to CI on this PR was still running at last check (10 of 12 green, Generated by Claude Code |
Part of #13609
What this is
#13609 is a MEASUREMENT card: which seam serves a deleted
datasourceentry cluster-wide, and for how long. This PR carries no production code change — it adds one file,packages/metadata/src/metadata-manager-cluster.test.ts, with two new tests that pin the finding down as an executable, CI-checked artifact rather than a one-off comment. Full write-up posted as a comment on #13609.The finding, in one paragraph
MetadataManager.unregister()does fan out onCLUSTER_CHANNELvianotifyWatchers, exactly like#13405's counter-evidence says, and a receiving peer'sinvalidateForForeignWritedoes correctly evict — the first new test below proves it with a positive control. The seam is one layer down:Runtime's shipped default (clusteroption omitted) resolves todefineCluster({})→driver: 'memory', andMemoryPubSub's own doc-comment says it outright: "No cross-process delivery — use the redis / postgres / nats driver for real multi-node setups." Every replica process builds its ownMemoryPubSubinstance; nothing wires them together. The split-brain guard (assertClusterDriverSafeForTopology) only fires when the operator has declared multi-node viaOS_EXPECT_MULTI_NODE/OS_CLUSTER_REPLICAS>1— silent otherwise, by design (ADR-0010, path A). A replica that never receives themetadata.changedevent keeps the deleted row in its in-memory registry, which carries no TTL at all (onlylistCachedoes), andreadListUncached()never re-checks a registry hit against the loader — so the second new test shows the stale row still being served past 10 list-cache TTL windows (300s vs. the 30s TTL), which is exactly the "reads longer than a TTL" shape #13609 flags as unexplained by #5109's (already-closed) TTL precedent.The two tests
makeCluster()'s existing shared-bus setup (models a workingredis/postgresdriver):a.unregister(...)evictsbimmediately. This is the positive control the finding needs: it proves the probe below would have seen a working broadcast if the transport had one.TestPubSubinstances (the faithful in-process stand-in for two real OS processes on the shipped default driver, since the isolation is a property of the transport object, not the network):a.unregister(...)never reachesb, andb.get()/b.list()— the exact door/api/v1/meta/datasourceand the admin registry both read through — keep serving the deleted row aftervi.advanceTimersByTime(LIST_CACHE_TTL_MS * 10).Test output
All 15 tests in
metadata-manager-cluster.test.ts— 13 pre-existing plus the 2 new ones — run viapnpm --filter @objectstack/metadata exec vitest run src/metadata-manager-cluster.test.ts.Scope
Test-only; no
.changeset(no package publishes anything from this diff — labeledskip-changeset). No production behavior changed. Whether/how to actually fix the default-topology gap (e.g. sharpen the split-brain guard, or document it more loudly) is left to #13609 for the maintainer to route — this PR is the measurement's durable evidence, not a proposed fix.Generated by Claude Code
Generated by Claude Code