Skip to content

Commit cd386bb

Browse files
committed
feat(vnext): coordinate catalog epochs
1 parent b77db7c commit cd386bb

7 files changed

Lines changed: 3782 additions & 14 deletions

docs/adr/0005-parser-independent-relation-completion.md

Lines changed: 109 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -481,18 +481,105 @@ The service reference-counts one provider subscription per provider
481481
configuration and scope, then fans invalidation out to subscribed sessions.
482482
The installed callback closure supplies authenticated provider and scope
483483
identity; the untrusted invalidation payload therefore contains only an epoch.
484-
Subscription membership is installed atomically before a provider can call
485-
back synchronously. A newly joining session captures an already-observed epoch
486-
without a synthetic revision bump. Disposal removes membership before
487-
unsubscribing or running external cleanup. Fifty sessions sharing a scope do
488-
not create fifty provider subscriptions.
484+
Membership uses a two-phase prepare/commit protocol: the session stores its
485+
prepared lease before commit can invoke provider code. Commit installs the
486+
membership atomically before a provider can call back synchronously. Because a
487+
subscription callback is always a change event, a valid callback fired during
488+
`subscribe` advances that committed member after subscription installation
489+
succeeds; it is not reclassified as an initial snapshot. A later joining
490+
session captures an already-observed epoch without a synthetic revision bump.
491+
Disposal removes membership before unsubscribing or running external cleanup.
492+
Retirement also clears the membership's target and coordinator link before
493+
that cleanup, so a consumer retaining a disposed lease retains only inert,
494+
bounded membership metadata. The coordinator stores only the copied provider
495+
ID and captured subscription function; it does not retain the unused search
496+
function. Coordinator disposal revokes that subscription reference before
497+
external cleanup, so a consumer retaining the disposed coordinator handle does
498+
not retain the provider graph.
499+
Fifty sessions sharing a scope do not create fifty provider subscriptions.
500+
The coordinator reserves membership state and capacity before reading the
501+
revision target callback. A throwing or invalid callback getter rolls that
502+
reservation back with `invalid-target` unavailable evidence. Reentrant
503+
coordinator disposal instead reports `disposed` and leaves no reserved
504+
membership or captured callback behind.
489505

490506
Each installed subscription has a service-owned incarnation identity captured
491507
by its callback. The identity is revoked before external unsubscribe or
492508
cleanup, and every callback checks it before entering the serialized epoch
493509
gate. Cleanup from a retired incarnation is keyed to that identity and cannot
494510
remove, mutate, or notify a reentrantly installed replacement.
495511

512+
The callback closes over a revocable cell rather than the service, scope state,
513+
or sessions. Retirement clears the cell before external cleanup, so a provider
514+
that retains an old callback retains only an inert bounded object. Synchronous
515+
callbacks are decoded into frozen epochs while `subscribe` is running; raw
516+
payloads and premature audience snapshots are not buffered. After the returned
517+
disposable is validated, the buffered epochs are staged together in FIFO
518+
order. Each accepted baseline or advance snapshots active membership and
519+
installs the epoch as one serialized step with no external call between those
520+
operations. A member joined during one event's dispatch therefore
521+
participates in a later pending event, but not the event already committed.
522+
A thrown subscription or malformed returned disposable discards that buffer
523+
and disables automatic invalidation for the live scope; explicit search
524+
remains available.
525+
The returned disposable is itself untrusted: the service captures one own
526+
enumerable data `dispose` closure, calls it with `this === undefined` at most
527+
once, and isolates malformed values and thrown cleanup. Dropping the last owner
528+
removes the complete scope incarnation before cleanup. A later join creates a
529+
new unobserved incarnation and may attempt a fresh subscription.
530+
531+
Malformed, duplicate, stale, or token-conflicting invalidations do not mutate
532+
state and do not tear down an otherwise valid subscription. Every raw callback
533+
still consumes the callback reset-window allowance. Exceeding that allowance
534+
revokes the incarnation and disables automatic invalidation before decoding
535+
further payloads. The conservative window resets from a zero-delay timer
536+
scheduled by the first callback. It intentionally makes no claim about formal
537+
JavaScript task or turn boundaries, while still preventing a provider from
538+
evading the allowance with a microtask chain. If hostile payload inspection
539+
synchronously reenters the same callback, the nested callback consumes that
540+
allowance but is ignored fail-closed; the coordinator does not retain the raw
541+
payload or invert callback order.
542+
543+
An epoch capture authenticates the exact provider configuration, scope
544+
incarnation, membership, observed epoch, and notification sequence. An
545+
equal-current response is publishable only if no accepted invalidation or
546+
higher response changed that sequence after capture. Otherwise it is
547+
superseded even when its epoch equals the newly current epoch. A capture is a
548+
single-use work token: submission claims it before queue admission, and replay
549+
is discarded even when the first submission was overloaded.
550+
551+
Epoch commands use one bounded, non-recursive FIFO drain. Its hard admission
552+
limit counts all work accepted during that drain, including commands already
553+
processed, so a one-at-a-time reentrant chain cannot evade the bound. For every
554+
accepted change, active membership is snapshotted and the epoch is immediately
555+
installed before revision preparation or any external listener runs. All
556+
affected session revisions then change before dispatch. Reentrant commands run
557+
only after the current change has reached every still-active member, preserving
558+
event order. The core does not coalesce revision events. The CodeMirror adapter
559+
may coalesce the resulting UI refresh, as described below. Admission fails
560+
before epoch mutation:
561+
an overflowing provider callback revokes automatic invalidation for that
562+
incarnation, while an overflowing response observation settles with closed
563+
overload evidence.
564+
Invalid, retired, replayed, disposed, and overflowing response submissions
565+
return closed settlement evidence without invoking a callback. Only an admitted
566+
FIFO command invokes its completion callback, so rejection cannot recursively
567+
reenter the coordinator's drain.
568+
569+
A revision target prepares its state change and returns a listener-dispatch
570+
closure. Returning `null`, returning a non-function, or throwing retires that
571+
membership. Retirement detaches state immediately, but hostile provider cleanup
572+
is deferred until every remaining target has prepared. A higher response epoch
573+
installed before preparation remains observed; if its submitting membership is
574+
retired during preparation, its response settles as retired while other
575+
successfully prepared members still dispatch. Coordinator disposal takes
576+
precedence over that retired evidence. The outermost cleanup barrier admits and
577+
invokes at most 1,024 captured disposers. If reentrant work attempts to admit a
578+
1,025th cleanup, the coordinator quarantines itself immediately: all cells and
579+
memberships become inert and all not-yet-invoked disposer references are
580+
cleared without calling more provider code. Cleanup is never continued from a
581+
timer.
582+
496583
The session exposes a disposable revision-change subscription for
497584
service-originated changes:
498585

@@ -671,6 +758,12 @@ The initial checked limits are:
671758
| Components per search path | 4 |
672759
| Total catalog context | 16,384 UTF-16 units |
673760
| Configured relation catalog providers | 1 |
761+
| Live catalog scopes per service | 128 |
762+
| Catalog memberships per service | 1,024 |
763+
| Catalog memberships per scope | 256 |
764+
| Raw subscription callbacks per reset window | 256 |
765+
| Catalog epoch admissions per drain | 1,024 |
766+
| Provider cleanups per outer barrier | 1,024 |
674767
| Catalog results per search | 100 |
675768
| Completion results after composition | 100 |
676769
| Provider ID | 256 UTF-16 units |
@@ -815,19 +908,22 @@ Mixed cached assets fail the exact version check and retire the generation.
815908
2. Attach embedded regions to session open/update transactions atomically.
816909
3. Add the bounded partial-`SELECT` query-site recognizer.
817910
4. Add the bounded CTE frame and visibility recognizer.
818-
5. Add the service-owned catalog coordinator, subscriptions, cache,
819-
in-flight sharing, cancellation, and provider contract suite.
820-
6. Add the session completion method and deterministic composition.
821-
7. Add the separate CodeMirror adapter and packed/browser fixtures.
822-
8. Add the pinned packed/browser/runtime marimo fixture and 1/10/50-editor
911+
5. Add the service-owned scoped epoch/subscription coordinator and its hostile
912+
lifecycle contract suite. It does not invoke provider search or retain
913+
result pages.
914+
6. Add catalog search scheduling, cache, in-flight sharing, cancellation,
915+
retry gates, and availability observers over the proven epoch coordinator.
916+
7. Add the session completion method and deterministic composition.
917+
8. Add the separate CodeMirror adapter and packed/browser fixtures.
918+
9. Add the pinned packed/browser/runtime marimo fixture and 1/10/50-editor
823919
performance and leak evidence.
824-
9. Validate both the in-memory/notebook and hierarchical remote provider
920+
10. Validate both the in-memory/notebook and hierarchical remote provider
825921
shapes, then stabilize the declarations and public export surface.
826-
10. Design scoped parser semantics and protocol v2 against PostgreSQL and
922+
11. Design scoped parser semantics and protocol v2 against PostgreSQL and
827923
BigQuery corpora before any additional parser-derived scope feature
828924
consumes it.
829925

830-
Breaking refinement remains allowed through step 8. The provider and
926+
Breaking refinement remains allowed through step 9. The provider and
831927
completion types become stable only after the working vertical slice, reusable
832928
provider contract suite, two materially different provider shapes, marimo
833929
packed/browser integration, hostile decoding, cancellation, epoch and

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"test:worker-placement": "node ./scripts/worker-placement.mjs",
2828
"test:integrity": "node ./scripts/check-test-integrity.mjs",
2929
"bench:catalog-boundary": "vitest bench --run src/vnext/__tests__/relation-catalog-boundary.bench.ts",
30+
"bench:catalog-coordinator": "vitest bench --run src/vnext/__tests__/relation-catalog-epoch-coordinator.bench.ts",
3031
"bench:local-relation-site": "vitest bench --run src/vnext/__tests__/local-relation-site.bench.ts",
3132
"bench:parser-adapter": "vitest bench --run src/vnext/__tests__/node-sql-parser-adapter.bench.ts",
3233
"bench:query-site": "vitest bench --run src/vnext/__tests__/query-site.bench.ts",

0 commit comments

Comments
 (0)