Skip to content

Commit f0f4d5e

Browse files
authored
feat(vnext): add catalog search cache policy (#196)
## Summary - add a bounded exact-key ready-page cache with epoch-scoped invalidation - add fail-closed loading barriers and retry gates for subscribed and unsubscribed providers - add atomic refresh-observer transfer with reentrancy-safe availability dispatch - document marimo connection-incarnation and unresolved-metadata semantics - codify the accelerated candidate-boundary delivery cadence ## Why Catalog completion needs to reuse proven results without allowing stale pages, same-epoch loading transitions, remounted editors, or cache pressure to bypass epoch authority. This keeps the package-private scheduler, policy state, and observer lifecycle coherent before session composition and the public adapter are added. ## Verification - 1,895 tests passed; 1 expected failure - changed coverage: 98.02% statements, 95.86% branches, 99.47% functions, 98.49% lines - full typecheck, oxlint, test-integrity, browser, demo, and package smoke passed - two independent exact-head adversarial reviews approved - catalog scheduler benchmark passed <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a bounded, epoch-aware cache for catalog search to reuse “ready” pages without serving stale data, and introduces refresh leases to prevent duplicate provider work. - **New Features** - Added `SqlCatalogSearchPolicyStore`: exact-key cache for ready pages with epoch-scoped invalidation. - Bounded by `MAX_CATALOG_POLICY_STORE_ENTRIES` (256) and `MAX_CATALOG_POLICY_STORE_RETAINED_BYTES` (2 MiB). - Integrated the policy store into `relation-catalog-search-work` with fail-closed loading/retry gates. - Added refresh leasing (`retainForRefresh`) and single-shot availability dispatch with reentrancy safety. - New default `DEFAULT_CATALOG_REFRESH_LEASE_MS = 1000`. - Exposed `hasLiveSubscription(scope)` on the epoch coordinator. - Updated ADR and implementation docs; added comprehensive unit and type tests. - **Migration** - If you provide a custom `SqlCatalogEpochCoordinator`, implement `hasLiveSubscription(scope)`. - Availability targets must be synchronous and return `undefined`. - Optional: tune `refreshLeaseMs` via search-work options; no other changes required. <sup>Written for commit 803afd1. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/marimo-team/codemirror-sql/pull/196?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
1 parent d39d908 commit f0f4d5e

8 files changed

Lines changed: 3212 additions & 102 deletions

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

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,11 @@ search request contains only copied, recursively frozen plain data:
347347
The `AbortSignal` is passed separately. Providers never receive a session,
348348
revision, `EditorState`, DOM object, credential object, or arbitrary live host
349349
context. Connection identity belongs in the catalog scope; live resources stay
350-
inside the provider closure. The provider object is caller-owned. The service
350+
inside the provider closure. The scope is an opaque host-generated identity
351+
that includes the live connection incarnation, not merely a reusable display
352+
name. Rebinding a marimo engine variable to a different connection therefore
353+
uses a new scope even when the variable name is unchanged. The provider object
354+
is caller-owned. The service
351355
captures its own-data callbacks once and owns searches, subscriptions, and
352356
their cleanup, but does not imply provider-wide disposal. Provider callbacks
353357
are closure functions with a declared `this: void` contract and are invoked
@@ -389,15 +393,23 @@ Search responses distinguish:
389393
- loading; and
390394
- failed with a closed code and retry policy.
391395

392-
A terminal `loading` response is not cached. A provider that later becomes
393-
ready must publish a strictly higher epoch through its subscription; same-epoch
394-
duplicate invalidation is not a readiness signal. A still-pending search
395-
promise can instead resolve ready under the service-owned response/refresh
396-
rules below. Every completion result carrying `catalog-loading`, whether caused
397-
by terminal loading or soft expiry, also carries a checked remaining
398-
completion-intent lease. For terminal loading that lease is independent of
399-
in-flight work ownership and is keyed to the exact session, query, and captured
400-
observed or unobserved epoch.
396+
A terminal `loading` response is not cached. It installs a bounded barrier for
397+
the exact request key and captured epoch. When the provider has a live
398+
subscription, matching requests observe loading without invoking it again
399+
until a strictly higher epoch retires the barrier. Same-epoch duplicate
400+
invalidation is not a readiness signal. Without a subscription, a later
401+
explicit request may become the single shared probe behind that barrier. The
402+
probe uses the normal queue and execution budgets and may publish readiness
403+
only with a strictly higher epoch; a same-epoch ready response cannot cross the
404+
loading barrier. A still-pending original search promise can instead resolve
405+
ready before returning terminal loading under the service-owned
406+
response/refresh rules below.
407+
408+
Every completion result carrying `catalog-loading`, whether caused by terminal
409+
loading or soft expiry, also carries a checked remaining completion-intent
410+
lease. For terminal loading that lease is independent of in-flight work
411+
ownership and is keyed to the exact session, query, scope incarnation, and
412+
captured observed or unobserved epoch.
401413

402414
Partial positive entities may be offered. A partial or absent result never
403415
proves that a relation does not exist. A complete empty search means only that
@@ -501,9 +513,12 @@ searches remain usable; only the epoch observation is a duplicate. A terminal
501513
`loading` response requires a higher epoch before that exact search can become
502514
ready. Providers without subscriptions may return `loading`, but they cannot
503515
cause automatic refresh; a later explicit request must observe the higher
504-
epoch. Failed responses are attempt evidence: `next-request` may recover at
505-
the same epoch, while `after-invalidation` and `never` create bounded retry
506-
gates rather than cached search results.
516+
epoch through the single shared probe. Failed responses are attempt evidence,
517+
not cache entries. `next-request` installs no gate and may recover at the same
518+
epoch. `after-invalidation` blocks the exact request until a strictly higher
519+
epoch. `never` blocks it for the remaining scope incarnation and is retired
520+
only with that incarnation or the service. Retry gates are bounded internal
521+
metadata; they never acquire result authority or extend work deadlines.
507522

508523
The service reference-counts one provider subscription per provider
509524
configuration and scope, then fans invalidation out to subscribed sessions.
@@ -733,8 +748,10 @@ and CTE evidence is composed first and never waits past the remaining response
733748
budget. On soft expiry the request settles ready, possibly empty, with
734749
`isIncomplete: true`, `catalog-loading`, and bounded remaining
735750
completion-intent lease metadata; its session may atomically retag the request
736-
consumer as a service-owned refresh observer. The intent lease is no longer
737-
than the remaining work lease.
751+
consumer as a service-owned refresh observer. The transfer removes consumer
752+
authority and installs observer authority as one serialized mutation, with no
753+
zero-owner interval in which shared work can be aborted or a settlement can be
754+
lost. The intent lease is no longer than the remaining work lease.
738755
That bounded refresh lease can retain the shared operation only until its
739756
existing hard deadline and active/queued service limits. With no consumers or
740757
live observers, the service removes and aborts the work. An observer is bound
@@ -754,24 +771,44 @@ refresh notification and leaves the already-returned incomplete result valid.
754771
No optional catalog promise can keep `complete()` pending indefinitely or
755772
block the local baseline past its product response budget.
756773

757-
The first implementation increment of this section is intentionally
758-
package-private. It combines an authenticated provider with the epoch
759-
coordinator and owns the fixed 8-active/64-queued scheduler, exact-key
760-
in-flight sharing, one latest-wins consumer per owner, independent
761-
cancellation, absolute queue and execution deadlines, response decoding, and
762-
epoch publication. An owner captures its scope and dialect when prepared, so
763-
individual requests cannot substitute provider, scope, dialect, or epoch
764-
authority. The authenticated dialect runtime owns its canonical provider ID;
765-
callers cannot pair an unrelated ID and runtime. Establishing the first
766-
baseline re-keys other joinable unobserved work in that scope, allowing
767-
newly-observed consumers to join it without duplicating a provider call.
768-
769-
Cache entries, loading/retry policy, refresh observers and their leases, the
770-
40 ms completion-response budget, pagination composition, ranking, session
771-
composition, and CodeMirror integration do not belong to that increment. They
772-
remain explicit follow-up increments; the coordinator must not expose a
773-
premature public surface that makes those deferred semantics difficult to add
774-
or test.
774+
Terminal loading has no work consumer to transfer. The subsequent session
775+
composition increment owns its independent intent, retaining only the bounded
776+
exact key, scope incarnation, captured epoch, and lease. A higher accepted
777+
response or invalidation retires it and emits only the normal higher-epoch
778+
`catalog` revision. By contrast, a compatible same-epoch pending-work
779+
settlement emits exactly one `catalog-availability` notification. A higher
780+
epoch retires both kinds of observer and never emits a second availability
781+
notification.
782+
783+
The package-private coordinator is delivered in two increments. The first
784+
combines an authenticated provider with the epoch coordinator and owns the
785+
fixed 8-active/64-queued scheduler, exact-key in-flight sharing, one
786+
latest-wins consumer per owner, independent cancellation, absolute queue and
787+
execution deadlines, response decoding, and epoch publication. The second is
788+
one combined cache/loading/retry/observer increment over that scheduler. It
789+
adds the ready-page cache, terminal-loading barriers and probes, retry gates,
790+
and atomic consumer-to-observer transfer. The session increment then adds
791+
independent terminal-loading intents together with the revision events they
792+
own; splitting the store states would permit stale cache hits or missed
793+
readiness transitions, while placing a session event lease in the scheduler
794+
would invert ownership.
795+
796+
An owner captures its scope and dialect when prepared, so individual requests
797+
cannot substitute provider, scope, dialect, or epoch authority. The
798+
authenticated dialect runtime owns its canonical provider ID; callers cannot
799+
pair an unrelated ID and runtime. Establishing the first baseline re-keys other
800+
joinable unobserved work in that scope, allowing newly-observed consumers to
801+
join it without duplicating a provider call.
802+
803+
The combined increment exposes only package-private lifecycle outcomes and
804+
observer primitives. Selection of the 40 ms completion-response budget,
805+
session revision ownership, conversion of readiness into session events,
806+
pagination composition, ranking, and CodeMirror menu refresh remain subsequent
807+
increments. A marimo adapter will map connection replacement, deferred
808+
schema/table resolution, local-table changes, and DuckDB catalog DDL to higher
809+
epochs. Its unresolved `schemas_resolved`, `tables_resolved`, and
810+
`child_schemas_resolved` states map to terminal loading, not complete-empty
811+
catalog evidence.
775812

776813
The exact structural cache and shared-work key contains:
777814

@@ -792,15 +829,34 @@ to its epoch. Only decoded ready responses are cached under their response
792829
epoch. Loading, failure, malformed, timeout, cancellation, supersession, queue
793830
overload, and disposal outcomes are not cached. Complete-empty results are
794831
reused only for the exact key and never prove an unknown-object diagnostic.
795-
Partial and paginated entries retain incomplete coverage; pages combine only
796-
for the identical base key and epoch, and each continuation remains a distinct
797-
request key. Higher-epoch results may be cached only after older scope entries
798-
are cleared and never publish to the revision that observed the older epoch.
799-
800-
Decoded entity IDs are unique for `(provider configuration, scope, epoch)`
801-
across every page composed into one result. A repeated ID, even with otherwise
802-
identical data, makes the page chain malformed before ranking or caching; array
803-
and page order therefore cannot resolve conflicting identity records.
832+
Partial and paginated entries retain incomplete coverage. This increment
833+
caches individual decoded pages only; each continuation is a distinct request
834+
key, and automatic page following or composition remains deferred.
835+
Higher-epoch results may be cached only after older scope entries are cleared
836+
and never publish to the revision that observed the older epoch.
837+
838+
Cache retention is bounded simultaneously to 256 entries and 2 MiB of
839+
deterministically estimated retained bytes, including the copied structural key
840+
and frozen decoded page. A successful exact-key lookup and an insertion each
841+
receive the next service-owned access sequence. Before an insertion becomes
842+
visible, least-recently used entries are evicted in ascending access sequence
843+
until both bounds hold. A page whose own estimate exceeds the byte bound
844+
remains usable for its attached consumers but is not cached. Epoch retirement
845+
removes incompatible entries before admission, and eviction invokes no
846+
provider or host callback.
847+
848+
Loading and retry gates share the bounds but are correctness state, not cache
849+
entries. Their admission evicts ready pages first; an admitted gate is never
850+
evicted before its epoch or scope-incarnation retirement rule. If the bounds
851+
contain only live gates and cannot admit another, the producing request fails
852+
closed as overloaded and the policy store remains overloaded until its provider
853+
configuration is replaced. This configuration-level quarantine prevents a
854+
later request from publishing across the barrier that could not be retained.
855+
856+
Decoded entity IDs are unique within each cached page for `(provider
857+
configuration, scope, epoch)`. Cross-page uniqueness is checked later by the
858+
pagination-composition increment; array or page order can never resolve
859+
conflicting identity records.
804860

805861
### Initial resource budgets
806862

@@ -973,8 +1029,9 @@ Mixed cached assets fail the exact version check and retire the generation.
9731029
5. Add the service-owned scoped epoch/subscription coordinator and its hostile
9741030
lifecycle contract suite. It does not invoke provider search or retain
9751031
result pages.
976-
6. Add catalog search scheduling, cache, in-flight sharing, cancellation,
977-
retry gates, and availability observers over the proven epoch coordinator.
1032+
6. Add catalog search scheduling and in-flight sharing, then one combined
1033+
package-private cache/loading/retry/availability-observer increment over the
1034+
proven epoch coordinator.
9781035
7. Add the session completion method and deterministic composition.
9791036
8. Add the separate CodeMirror adapter and packed/browser fixtures.
9801037
9. Add the pinned packed/browser/runtime marimo fixture and 1/10/50-editor

implementation.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,29 @@ Prefer reviewers with different prompts or models; two identical agents are
570570
correlated evidence. Independent downstream work may target an accepted
571571
interface contract, but cannot merge until its dependency is accepted.
572572

573+
### Delivery cadence
574+
575+
Keep the rigor at candidate boundaries without paying the full cost after every
576+
small edit:
577+
578+
- Combine naturally coupled package-private work into one coherent vertical
579+
slice when separate PRs would only create temporary APIs and duplicate
580+
review cycles.
581+
- Run focused unit, type, lint, and benchmark checks while developing. Run the
582+
complete local gate once at a clean candidate commit and again only after a
583+
material fix.
584+
- Start the two independent reviews in parallel against that coherent
585+
candidate. A reviewer may recheck a narrow non-architectural amendment
586+
without repeating unrelated probes; public-contract, concurrency, or
587+
lifecycle changes still require two full exact-head approvals.
588+
- Treat hosted CI as the authoritative cross-platform and package-matrix rerun.
589+
Do not duplicate the same unchanged full matrix between every local commit.
590+
- Parallelize independent design, implementation, consumer research, and
591+
review work. Time-box exploratory research and record nonblocking ideas as
592+
follow-ups instead of expanding the active slice.
593+
- Request Copilot once when the PR first has a coherent reviewable head. Never
594+
delay a later exact-head fix solely to obtain another Copilot pass.
595+
573596
### Review automation
574597

575598
Generate one review packet containing:

0 commit comments

Comments
 (0)