perf(fulltext2): reuse immutable bases across cache generations - #27462
perf(fulltext2): reuse immutable bases across cache generations#27462XuPeng-SH merged 24 commits into
Conversation
XuPeng-SH
left a comment
There was a problem hiding this comment.
Deep review of exact head a4000eb.
The latest waiting-owner, published-base, and lifecycle-hook panic fixes close the previously reported races. One blocking observability/generation defect remains:
MERGE and REBUILD immediately erase the miss reason they just recorded. invalidateLoadGeneration first calls rememberLoadReason, which increments the per-index invalidation and stores the reason under that generation. It then calls clearReusableLoadGeneration for merge/rebuild; that calls clearLoadGeneration, which deletes pendingLoadReasons[index] and increments invalidation a second time. The production CREATE/REBUILD, MERGE, and plugin paths all enter through this sequence. Consequently the actual cold reload is reported as process_start instead of merge/rebuild, and one logical invalidation advances the generation twice.
I reproduced this through invalidateLoadGeneration with the observer enabled: peekLoadReason returned empty for both merge and rebuild in the normal run and in 20/20 race runs. Existing coverage invokes these branches but never asserts that the reason survives.
Please separate reasonful invalidation from reasonless DROP cleanup: clear the reusable base/tail pools for MERGE/REBUILD while preserving the newly recorded pending reason and without a second generation bump. Add regressions proving each reason survives failed/canceled attempts and is consumed only by a successful current load.
Validation after removing the temporary counterexample: full pkg/fulltext2 and pkg/vectorindex/cache tests pass normally and under race; the four latest focused regressions pass under race for 100 repetitions; go list/build/vet, gofmt, git diff --check, and index-plugin guards pass.
|
Addressed XuPeng-SH's current-head observability/generation finding in commit
Validation on the exact clean head:
Please re-review exact head |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 010f30d.
The previous MERGE/REBUILD reason bug is fixed: reasonful invalidation now advances the generation once, preserves the pending reason across failed attempts, and clears only the reusable pools. One blocking DROP generation-order race remains.
clearReusableLoadGeneration currently clears the base and tail pools first, then calls clearLoadGeneration. An already-active load is therefore still current during the interval after the final pool clear. It can publish a base or tail state in that interval; the later generation bump makes the load obsolete but does not clear what it just published. HandleDropIndex now routes production DROP through exactly this reasonless path, and the following reasonless Cache.Remove deliberately does not invoke the invalidation hook again. DROP can consequently return with old-generation mappings/state retained in the reusable pools, up to their multi-GiB bounds, until a later lifecycle sweep. This also leaves stale material eligible if the key/generation metadata is reused.
I reproduced the ordering deterministically without sleeps using the production generation and tail-pool operations: hold pendingLoadReasons so DROP completes its pool clear and pauses immediately before clearLoadGeneration; publish through installAndAcquireIfCurrent from the still-current active generation; release DROP; the newly published tail remains in loadedTailPool. The assertion failed normally and in 20/20 race runs. The same ordering exists for base commit/acquire.
Please invalidate/clear the load generation before performing the final reusable-pool clear, while preserving the existing lock order by releasing the generation locks before taking pool locks. Then old loads cannot pass their current-generation publication checks, and the final clear removes any state created before invalidation. Add the deterministic active-load-vs-reasonless-DROP regression for both publication ownership and final pool emptiness.
Validation after removing the temporary counterexample: the three new reason regressions pass independently under race for 100 repetitions; full pkg/fulltext2, pkg/fulltext2/plugin/compile, and pkg/vectorindex/cache tests pass normally and under race; all five changed owning packages pass go list/build/vet; gofmt, git diff --check, and index-plugin guards pass.
|
Addressed the current-head generation/lifecycle finding and pushed commit
Validation on the exact clean head:
Please re-review the exact pushed head. No duplicate reviewer request was added; the existing XuPeng-SH request remains active. |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 86eab9e8bfa07d679c5f0f0ef7f100c0bbaa5b4d.
The previous DROP-ordering blocker is fixed: clearReusableLoadGeneration now invalidates the load generation before clearing the reusable pools, and the new regression test closes the stale-publish window. However, two independent blockers remain:
-
[P1 correctness] Generation supersession is exposed to SQL as an internal error instead of being retried by the cache.
The production mutation sequence publishes
OnCacheInvalidated(reason)beforeCache.Remove(key). If an in-flight miss observes the new generation in that interval,Fulltext2Search.LoadreturnserrLoadGenerationSuperseded. At that momentalgo.evictingis still false, andVectorIndexCache.Search/SearchIntoonly retry an evicting entry orErrInvalidState, so the user receives:internal error: fulltext2 load superseded by a newer generationI reproduced this through the real local
VectorIndexCacheandFulltext2Search: a SQL mock publishesLoadMissCDCFlushduring the first load, before any remove claim. The expected transparent retry instead failed with the error above, both normally and 20/20 times under-race -count=20.Please make generation supersession an exact-entry cache retry: exact-delete/destroy the failed mapped entry and retry. Merely changing the sentinel to
ErrInvalidStateis not sufficient with the current branch, because it continues without removing the non-evicting failed entry. Please add production-path coverage for the publication-before-remove window for bothSearchandSearchIntounless the handling is centralized. -
[P1 performance]
HouseKeepingevicts an exact entry even if a concurrent successful search renewed its sliding TTL after the snapshot.This PR preserves exact identity in the snapshot, but drops the pre-claim
Expired()/stalerecheck that existed in the base implementation. A deterministic two-entry test demonstrates the race: housekeeping snapshots both expired entries, the first invalidation callback blocks, a successful search renews the second entry, and housekeeping still evicts that same second entry after it resumes. The test failed 20/20 times under-race -count=20.This forces an unnecessary full cold load (the expensive path this PR is meant to avoid) and publishes a phantom TTL invalidation reason. Please restore an exact pre-claim condition such as:
if !entry.algo.Expired() && !entry.algo.stale.Load() { continue }immediately before computing the reason / calling
evictEntry, and add the concurrent-renewal regression.
Validation performed on the locked head:
- full normal and race suites passed for
pkg/fulltext2,pkg/fulltext2/plugin/compile, andpkg/vectorindex/cache; - the DROP-generation regression and adjacent generation tests passed 100/100 under race;
go list -mod=readonly, build, vet, gofmt/diff checks, and index-plugin architecture guards passed;- lifecycle audit found no additional confirmed leak, hang, or unbounded-growth defect.
aptend
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 86eab9e8bfa07d679c5f0f0ef7f100c0bbaa5b4d. I read all prior reviews, inline threads, resolutions, author replies, and issue comments; compared the incremental changes since my previous reviewed head d6b7b627bd447a422e3ef34e8ae2b8730f931ed2; and audited the complete base diff, including cache/generation ownership, cleanup, failure, cancellation, reuse, and shutdown paths.
The previous reason-retention and reasonless-DROP generation-ordering issues are closed. Two blocking races remain, detailed inline: a normal mutation can expose the internal generation-supersession sentinel to SQL instead of retrying, and housekeeping can evict an exact entry after a successful search renewed its TTL.
I reproduced both with production-shaped deterministic tests on the locked head, normally and under -race; the temporary tests were removed. After removal, full normal and race suites passed for pkg/fulltext2, pkg/fulltext2/plugin/compile, and pkg/vectorindex/cache; go vet and diff checks passed. Current GitHub required checks pass.
aunjgr
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 86eab9e8bfa07d679c5f0f0ef7f100c0bbaa5b4d against merge base 6dccc954a1a32c12a2114328c752f15cf97ef2ba.
The reasonless-DROP ordering and stale reusable-pool publication paths are closed, but two independent blockers remain.
[P1 correctness] Treat generation supersession as an exact-entry cache retry, not a SQL error
FULLTEXT2 mutation paths publish OnCacheInvalidated(reason) before calling Cache.Remove (for example pkg/iscp/fulltext2_consumer.go:123-132, with the same order in compact/rebuild). In the valid interval after the generation bump but before removal claims the cache entry, an in-flight Fulltext2Search.Load observes !loadGenerationCurrent and returns errLoadGenerationSuperseded (pkg/fulltext2/search_cache.go:161-168). The containing cache entry is not yet evicting, and the sentinel is an Internal error.
VectorIndexCache.Search therefore takes its ordinary load-error branch (pkg/vectorindex/cache/cache.go:535-543): it exact-deletes/destroys that failed entry and returns the internal error to the SQL statement. SearchInto has the same behavior at lines 570-577. A normal CDC/MERGE/REBUILD race is thus visible as internal error: fulltext2 load superseded by a newer generation instead of transparently retrying the replacement generation.
Make supersession a dedicated retryable-load outcome: remove/destroy only the exact failed mapped entry, then retry. Simply mapping it to ErrInvalidState is insufficient with the current branch because lines 537-539/571-573 continue without removing the non-evicting failed entry. Add production-cache regressions for the publish-before-remove interval through both Search and SearchInto (or one centralized handler exercised by both).
[P1 performance] Recheck expiry/staleness immediately before claiming the exact entry
HouseKeeping snapshots entries that are expired/stale at lines 443-449, but lines 451-456 call evictEntry without revalidating that condition. A successful search on the snapshotted entry can run in between and renew its sliding TTL through extend(false) (cache.go:265-271,308-309). Housekeeping still sets evicting, publishes a phantom ttl_expired reason, removes the exact renewed entry, and destroys it.
Concrete deterministic ordering: snapshot expired entries A and B; A's invalidation hook blocks the sweep; a search on B succeeds and renews B; release A; the sweep still evicts B. This forces the next query to remap/reload the potentially multi-GiB base—the expensive path this PR exists to avoid—and records a false miss reason.
Restore an exact pre-claim condition recheck immediately before reason selection/evictEntry, while retaining stale entries as unconditionally reclaimable. Add a barrier-synchronized two-entry regression proving a renewed TTL entry survives and no TTL invalidation is published.
Q1 lease/mmap retirement, Q2 waiter/cancellation cleanup, Q3 pool/registry bounds, diff cleanliness, and index-plugin architecture guards otherwise look closed.
aunjgr
left a comment
There was a problem hiding this comment.
Re-review of exact head 700763d8207d17664bf5d8a5a980c3136a795871.
The superseded-load path is now internally retryable and exact-entry cleanup is correct. The new generation tests exercise the publication-before-remove race through both Search and SearchInto. However, the TTL renewal blocker is narrowed, not closed.
[P1] Make expiry validation atomic with the eviction claim
HouseKeeping now rechecks Expired() at pkg/vectorindex/cache/cache.go:458, but there is still a TOCTOU window before evictEntry calls beginEviction() at line 465. A concurrent Search can acquire the shared mutex, observe evicting == false, and execute extend(false) in that window. Housekeeping then successfully claims and removes the entry even though its TTL has just been renewed.
The new two-entry test only renews the second entry before the line-458 recheck, so it cannot expose this remaining interleaving:
- housekeeping's line-458 recheck observes the old expiry;
- search enters under
Mutex.RLockand stores a freshExpireAt; - housekeeping sets
evictingand removes the entry.
The expiry decision and eviction claim must be serialized against Search.extend, for example by taking the entry write lock, rechecking expiry/staleness there, and claiming eviction before releasing it (with destruction structured so the lock is not recursively acquired). A generation/token CAS is another option. Please add a deterministic hook/barrier test at this exact recheck-to-claim boundary and assert that a successful renewal prevents TTL eviction. Stale-marked entries should remain unconditionally reclaimable.
I found no other blocker in the incremental fix.
aptend
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 700763d8207d17664bf5d8a5a980c3136a795871. I read the complete review/thread/reply history, compared the delta from my prior reviewed head 86eab9e8bfa07d679c5f0f0ef7f100c0bbaa5b4d, and rechecked the complete PR diff. The exact-delete/retry path fixes the initiating loader, and the housekeeping recheck fixes renewal that happens before that recheck, but two blocking concurrency windows remain: waiters can still observe the superseded load as an internal error, and a renewal between the expiry recheck and eviction claim can still be discarded.
Validation: full pkg/fulltext2, pkg/fulltext2/plugin/compile, and pkg/vectorindex/cache tests passed normally and under -race; the new focused regressions passed under -race -count=100; go vet passed. Two temporary deterministic counterexamples reproduced the remaining windows and were removed. The worktree is clean.
XuPeng-SH
left a comment
There was a problem hiding this comment.
Deep re-reviewed exact head 700763d8207d17664bf5d8a5a980c3136a795871, including the incremental fix, the full cache/load lifecycle, and prior review history.
The initiating superseded load is now removed and retried, and the added focused tests pass. Two blocking concurrency windows remain:
-
[P1 correctness] Make every waiter retry a superseded shared load.
VectorIndexSearch.LoadstoresSTATUS_ERRORand releases/broadcasts before the initiating goroutine callsdiscardFailedLoad. A query that already found this map entry and is waiting on its read lock can acquire it in that interval.VectorIndexSearch.Search/SearchIntoseeSTATUS_ERRORand returnNewInternalError("Load index error"); the outer cache loop retries onlyErrInvalidState, so this normal generation race still leaks an internal error to SQL for waiters.The new retry tests exercise only the goroutine that owns
Load; the production-shaped FULLTEXT2 test is also single-caller. Please make the superseded outcome retryable for all callers sharing the failed entry, with exact-entry cleanup/destruction remaining single-owner, and add a barrier test with one loader plus at least one already-blocked waiter for both cache APIs (or their centralized path). -
[P1 performance/lifecycle] Make TTL validation atomic with the eviction claim.
HouseKeepingrechecksExpired(), then callsevictEntry;beginEviction()is still later and does not serialize with the entry mutex. Between those operations,Searchcan acquire the shared entry lock, observeevicting == false, successfully search, and refreshExpireAtviaextend(false). Housekeeping then claims and destroys that just-renewed entry, publishes a falsettl_expiredreason, and forces an avoidable cold reload.The new two-entry test renews before the added recheck, so it cannot exercise the recheck-to-claim interleaving. Serialize expiry/staleness validation and the eviction claim against search renewal, and add a deterministic barrier at that exact boundary. Stale-marked entries should remain reclaimable regardless of TTL renewal.
Validation on the locked head: the new retry and concurrent-renewal focused tests pass with the deterministic CGo wrapper; they do not cover the two windows above.
aptend
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 90219ad7ce2e03ee5c56e4cc637502810e755777 against base 6dccc954a1a32c12a2114328c752f15cf97ef2ba.
I read the complete review/thread/reply history, compared the incremental diff from my prior reviewed head 700763d8207d17664bf5d8a5a980c3136a795871, and rechecked the complete cache/load lifecycle diff.
Both prior blockers are closed. Retryable ErrInvalidState loads publish STATUS_DESTROYED before broadcasting, so already-blocked Search/SearchInto waiters return the retryable outcome instead of Load index error; exact-entry deletion/destruction remains owned by the cache retry path. Sliding TTL renewal and the final ttl_expired eviction decision/claim now share ttlMu, while stale/generation-changed eviction remains unconditional. Lock ordering keeps resource destruction outside the TTL gate and search read lock.
Validation on this exact head: full normal and race suites passed for pkg/vectorindex/cache, pkg/fulltext2, and pkg/fulltext2/plugin/compile; the two new waiter/TTL regressions passed under -race -count=100; go vet and git diff --check passed. The worktree remained clean.
XuPeng-SH
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 90219ad7ce2e03ee5c56e4cc637502810e755777 against merge base 6dccc954a1a32c12a2114328c752f15cf97ef2ba.
The two previously requested fixes are closed:
- a superseded load now publishes
STATUS_DESTROYEDbefore broadcasting, so initiating callers and already-blockedSearch/SearchIntowaiters all take the retry path; exact-entry destruction remains single-owner; - sliding-TTL renewal and the final TTL eviction check/claim are serialized by
ttlMu, while stale/generation eviction remains unconditional. The lock graph has no reverse held-lock edge because eviction releasesttlMubefore enteringDestroy.
One independent blocker remains:
[P1 correctness/liveness] Do not retry every Load error whose MO code is ErrInvalidState
VectorIndexSearch.Load maps every ErrInvalidState from every algorithm to STATUS_DESTROYED at pkg/vectorindex/cache/cache.go:246-255. Both generic cache APIs then exact-delete the entry and unconditionally retry forever at lines 580-588 and 619-625. That error code is not exclusive to FULLTEXT2 generation supersession.
There is a concrete production path in IVF-FLAT:
planReaderinstalls its real relation scanner and callscache.Cache.Search(pkg/vectorindex/ivfflat/plan_reader.go:253-255, 369-370, 419);IvfflatSearch.LoadcallsLoadCentroids;- the scanner returns
moerr.NewInvalidStateNoCtxf("ivfflat hidden relation ... has no table definition")whenGetTableDefis nil (plan_reader.go:541-552); - the cache treats that persistent catalog/storage state as “load superseded”, destroys the exact entry, and immediately repeats the same load with the same transaction/state.
The query therefore does not receive the real error. It loops doing relation scans and entry construction/destruction. LoadCentroids also logs IVFFLAT START/END: Load Centroids at INFO on every attempt (search.go:71-74), so this is also a deterministic log storm.
I confirmed the cache behavior with a temporary terminating probe: an algorithm returned a permanent ErrInvalidState; VectorIndexCache.Search exceeded 100 loads without returning, and stopped only when the probe switched to a non-retryable error for cleanup. This reproduced 20/20 normally and 20/20 under -race. The temporary test was removed.
Please use a dedicated retryable-load outcome/marker for generation supersession instead of the shared MO error code, and preserve the destroyed-before-broadcast waiter behavior only for that outcome. Add controls proving:
- the dedicated superseded outcome retries for both initiating loaders and blocked waiters through
SearchandSearchInto; - an ordinary/permanent
ErrInvalidStateload is destroyed exactly once and returned to the caller without retry.
Validation on the locked head:
- all five focused supersession/TTL regressions passed under
-race -count=100; - an additional public-cache loader+waiter probe for both APIs passed under
-race -count=100; - full normal and race suites passed for
pkg/vectorindex/cache,pkg/fulltext2, andpkg/fulltext2/plugin/compile; - list/build/vet, gofmt, diff checks, and index-plugin guards passed;
- parallel no-op cache-hit benchmark: previous head ~166 ns/op vs current ~190 ns/op, 0 allocs (about +24 ns fixed cost; bounded and not a blocker).
aunjgr
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 90219ad7ce2e03ee5c56e4cc637502810e755777 against merge base 6dccc954a1a32c12a2114328c752f15cf97ef2ba.
Both prior blockers are closed. ttlMu now makes the final expiry check and eviction claim atomic with Search/SearchInto renewal; the lock ordering releases that gate before destruction waits on the entry RWMutex, so the search, load, remove, stale, and shutdown paths do not form a cycle. Retryable superseded loads publish STATUS_DESTROYED before broadcasting, allowing existing waiters as well as the initiating caller to re-enter through a replacement entry. Exact-entry cleanup and generation ownership remain intact. The full lifecycle and plugin-dispatch guard found no blocking issue.
aptend
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 90219ad against base 6dccc95.
The head is unchanged since my previous approval. For this new request I re-read all 37 historical reviews, 32 inline comments and replies, all 16 thread states, and the issue comments; rechecked the complete diff and the zero delta from my prior reviewed head.
The prior waiter and TTL blockers remain closed. However, one independent P1 correctness/liveness blocker is confirmed: the generic cache treats every algorithm Load error with MO code ErrInvalidState as the FULLTEXT2 generation-supersession signal. A permanent IVF-FLAT catalog error is therefore exact-deleted and retried forever instead of being returned.
I independently reproduced this through both public cache APIs. The probe returned permanent ErrInvalidState four times and a distinct terminating error on the fifth call; Search and SearchInto each performed all four reload/destroy cycles and returned only the artificial fifth error. A truly permanent error never terminates. The temporary probe was removed.
Validation on the locked head after removal:
- full normal tests passed for pkg/vectorindex/cache, pkg/fulltext2, and pkg/fulltext2/plugin/compile
- focused supersession/waiter/TTL race tests passed for 20 repetitions
- go vet and git diff --check passed
- worktree is clean
|
Addressed the current-head load-error classification finding in
Validation on the repaired local head:
|
|
Additional gate note: BVT: N/A. This repair changes an internal vector-index cache load/error-classification contract with no SQL-visible syntax, result, or persisted-format change. The production |
aptend
left a comment
There was a problem hiding this comment.
Deep re-review of exact head 4da2f78 against base 6dccc95 and my prior reviewed head 90219ad. I re-read the complete review, inline-thread, resolution, reply, and issue-comment history; checked the incremental fix and re-audited the complete cache/generation/lease lifecycle diff. The previous blocker is closed: only the dedicated cache-internal retryable-load marker now publishes STATUS_DESTROYED and retries, while an ordinary ErrInvalidState remains terminal, is exact-deleted/destroyed once, and is returned to the caller. Existing loader/waiter coverage passes, and an independent public-cache loader-plus-waiter counterexample passed for both Search and SearchInto under race for 100 repetitions. Full normal and race suites passed for pkg/vectorindex/cache, pkg/fulltext2, and pkg/fulltext2/plugin/compile; go vet and diff checks passed. No blocking issues found.
XuPeng-SH
left a comment
There was a problem hiding this comment.
Deep re-review on 4da2f78. The prior correctness/liveness blocker is closed.
The generic cache now distinguishes FULLTEXT2 generation supersession with a dedicated retryable-load marker. Only that marker publishes STATUS_DESTROYED before broadcasting and enters exact-entry delete/destroy/retry; an ordinary ErrInvalidState remains terminal, is destroyed once, and is returned unchanged. I rechecked initiating-loader and blocked-waiter behavior for Search and SearchInto, TTL renewal versus eviction claim, exact-entry replacement cleanup, load-generation ownership, base/tail lease retirement, bounded registries, cancellation, and shutdown hooks. No new correctness, liveness, resource, performance, or log-storm blocker was found.
Validation on this exact head:
- full normal and race suites passed for pkg/fulltext2, pkg/vectorindex/cache, and pkg/fulltext2/plugin/compile;
- focused retry/permanent-error tests passed under race for 100 repetitions;
- focused generation/rollback/TTL/reasonless-drop tests passed under race for 20 repetitions;
- go vet, gofmt, and git diff --check passed.
Merge Queue Status
This pull request spent 17 minutes 45 seconds in the queue, with no time running CI. Waiting for
All conditions
ReasonPull request #27462 has been dequeued Pull request from fork cannot be queued. This pull request comes from a fork, and Mergify needs the author's permission to update its branch.
Failing checks:
HintYou should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it. Tick the box to put this pull request back in the merge queue (same as
|
What type of PR is this?
Which issue(s) this PR fixes:
issue #27445
What this PR does / why we need it:
Reuses immutable FULLTEXT2 base segment mappings across cache generations and loads only new CDC tail/delete frames when the base is unchanged. This PR is stacked on
codex/fulltext2-load-observability-prand contains the second semantic commit for reusable bases.The lifecycle repair:
The PR does not change SQL/DDL, ranking, index format, CDC wire protocol, or cross-CN freshness semantics.
Validation
GOWORK=off .agents/skills/mo-dev/scripts/mo-cgo-test -mod=readonly -count=1 -timeout=240s ./pkg/fulltext2— PASS.GOWORK=off .agents/skills/mo-dev/scripts/mo-cgo-test -mod=readonly -race -count=1 -timeout=240s ./pkg/fulltext2— PASS.pkg/fulltext2andpkg/vectorindex/cache— PASS.-race -count=100; it holds active base/tail leases, invalidates throughNewFulltext2Search, rejects stale publication, and verifies final pool cleanup.VectorIndexCache.SearchandSearchIntonow exact-delete/destroy a superseded failed entry and transparently retry — normal PASS and exact-race -count=100PASS.-race -count=100PASS.go list -mod=readonly— PASS.git diff --check, self-review, and semantic preflight pass on exact clean head700763d8207d17664bf5d8a5a980c3136a795871with diff hash722f5232ec11c4ae8e8f4ded94849b313bb75f13d8cc21084af14a5153a0ff07.go buildandgo vetremain blocked by the same reproducible clean-merge-basepkg/common/docfilterC declaration failure; no build/vet pass is claimed.QA required: yes — this changes CDC invalidation, mmap/temp-file ownership, cancellation, and generation lifecycle. Production terminal: FULLTEXT2 MATCH across tail refresh, TTL, MERGE, REBUILD, and restart.
Current status: Ready for review; exact-head CI is running and XuPeng-SH/aptend re-review is pending on pushed head
700763d8207d17664bf5d8a5a980c3136a795871. QA remains pending.Non-goals:
Current-head follow-up
86eab9e8bfa07d679c5f0f0ef7f100c0bbaa5b4d.upstream/main, integrated observer/cache lifecycle coverage, and preserved durable generation for empty-tail loads.700763d8207d17664bf5d8a5a980c3136a795871; both public cache entrypoints now retry exact failed entries, and housekeeping revalidates expiry/staleness before eviction.