Skip to content

kv, main, adapter: restore the Redis TTL-inline migration on every locally led shard - #1278

Open
bootjp wants to merge 16 commits into
mainfrom
feature/focused-pascal-dbr58v
Open

bootjp wants to merge 16 commits into
mainfrom
feature/focused-pascal-dbr58v

Conversation

@bootjp

@bootjp bootjp commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

The Redis delta compactor's TTL-inline migration (adapter/redis_ttl_inline_migrator.go) has done no work in the server since a36dac7 ("Limit TTL migration scans to local leaders", 2026-07-22). This PR makes it run on every data group a node leads, makes the original failure mode fail safe, and adds a behavioural guard against coordinator wrappers that drop an optional capability.

There were two bugs, one behind the other:

  1. The capability never reached the migrator. The adapters hold WithKeyVizLabel(startupGatedCoordinator{ShardedCoordinator}). The migrator asks that value which data groups this node leads (LocalLeaderGroupIDs) and scans only those. The label wrapper answered nil when its inner lacked the method, and the startup gate never forwarded it, so the migrator planned zero scans.
    • main has since fixed the forwarding in Unify drifted production duplicates; plan the test-suite cleanup #1277: both wrappers now embed a shared kv.CoordinatorForwarder. This branch is merged with main and takes that forwarder.
    • What this PR adds on top is the known contract (below). With it, a coordinator without the capability falls back to whole-prefix scans instead of planning none.
  2. The per-group scan refused every shard but the first. Codex review found this. ShardStore.ScanGroupAt resolves the requested range through the route catalog, and a bare anchor prefix such as !redis|str| resolves to the first route only. Every other group got ErrExplicitGroupRouteOwnerMismatch. So on a split catalog, a node migrated at most the first route's anchors and failed every tick on the rest. This PR adds ShardStore.ScanGroupOwnedAt for it.

Commits

Commit Change
main: reproduce the TTL migration losing local leader groups Failing regression test on the real chain: a ShardedCoordinator leading groups 1 and 2 and following group 3, wrapped as main.go wraps it.
kv, main, adapter: keep local leader groups through the coordinator wrappers The capability becomes kv.LocalLeaderGroupsCoordinator with LocalLeaderGroupIDs() (ids []uint64, known bool). Decorators forward through kv.LocalLeaderGroupIDsThrough, which reports a coordinator without it as unknown. Unknown falls back to the migrator's existing whole-prefix ScanAt and logs once. A known empty list still plans nothing.
main: guard every ShardedCoordinator capability through the startup wrappers Behavioural guard test (see the wrapper section).
main: forward LinearizableReadForKey through the startup gate Superseded by main's forwarder in the merge.
adapter: reproduce TTL migration refusing every shard but the first Failing regression test over a real ShardStore split at m across two groups.
kv, adapter: scan each local leader group's own share of the TTL anchors New ShardStore.ScanGroupOwnedAt, used by the migrator's per-group plans.
kv, adapter: reproduce the group-owned scan's staged, bound and legacy gaps / kv, adapter: bound the group-owned scan, defer staged routes, serve legacy rows Codex round 2: one physical page per call, staged routes deferred, legacy-route rows served.
Merge main into feature/focused-pascal-dbr58v Takes main's CoordinatorForwarder. Moves the known contract onto it. Updates main's chain and plan tests to the new signature. Reworks the behavioural guard to use main's shardedMethodsNotForwarded list.
kv, adapter: reproduce legacy-route rows served against the owner's state / kv: serve legacy-route rows only while the logical owner holds none Codex round 3: legacy stream metadata. Also fixes a key the owner also holds being served by two groups.
adapter: reproduce TTL-inline ticks starving later scan plans / adapter: rotate TTL-inline scan plans across ticks Codex round 3: plans rotate across ticks that run out of budget.
kv: reproduce the group-owned scan re-reading every wide-column row / kv: skip the point-read pass for rows the group-owned scan serves Self-review: the scan no longer re-reads each served wide-column row through the point-read path (see below).
backup: allow pebble's sstable reader through the import boundary CI fix, ported from main's breakage. TestBackupLinksNoLiveClusterCode fails on main since 3f097df (#1276) added a pebble sstable reader to internal/backup, while 5678081 (#1277) forbade all of pebble there. The commit allows exactly the sstable package.

ShardStore.ScanGroupOwnedAt

ScanGroupOwnedAt(ctx, groupID, start, end, limit, ts) (rows, resume, err):

  • One physical page per call. Reads up to limit rows from the group's own store, fenced like the other explicit-group reads. resume is the last key the page read when it was full, nil once the range is exhausted. The migrator moves its cursor to resume, so a page with no served rows still advances. Each tick stays within one page per plan.
  • Served rows only.
    • Owned rows: keys whose route the group owns, judged by the same routeAndGroupForKey that routes writes.
    • Legacy-route rows: hash, set, zset and stream rows still stored at the route of their raw key. These are served only while the logical owner holds no version of the key at ts, as checked by this node as the owner's leader. That is exactly when a point read falls back to the legacy route (routeMissStopsPointFallback), so the row is the value a reader sees. The legacy route candidate is legacyPointRouteKey, the one point reads use.
    • Skipped: copies of ranges the group no longer owns. No key is rewritten from a stale value.
  • No point-read pass. Other scans re-read every hash, set and zset row through the point-read path. That pass now runs once in scanRouteAtForwardPage, after whichever page path ran, so those scans are unchanged. ScanGroupOwnedAt skips it, because every row it serves already carries the point-read value. This removes:
    • one point read per owned wide-column row;
    • a proxied read per legacy-route row the scan then dropped;
    • a page failure when such a row's owner had no known leader.
  • Staged routes deferred. A key whose route is under staged visibility is skipped and picked up on a later pass. An older live copy must not feed a rewrite while a newer staged copy waits for promotion.
  • Meant for the group's own leader. If leadership moves between planning and the scan, the forwarded request is served as ScanGroupAt and may be refused for one tick. The next tick replans.

Known limit, handled in the separate design PR #1280. A legacy row in group L whose user key belongs to group P, where a different node leads P, is not migrated here.

  • Why: only P's leader may write the key (the HLC rule), and only P's leader can tell whether P holds it. Per-group plans never let P's leader read L.
  • Plan: docs: propose TTL-inline migration of legacy-route rows across leaders #1280 proposes measuring the backlog first. L's leader then asks P's leader whether P holds the key and forwards the rewrite, with its timestamp issued by P's leader (A0b) or the TSO (Phase D). Finally L's shadowed copies are evacuated.
  • History: main has not migrated these rows since a36dac7. Before it, every node's whole-prefix scan reached them.
  • Meanwhile: reads stay correct through the dual-route point-read fallback and the legacy TTL fallback. Phase 3 (disabling the fallback) must wait until this case is covered or shown empty.

Plan rotation

Every plan (one per handler and local leader group) shares the compactor's per-tick budget. migrateTTLInlineOnce now flattens the plans into one list per tick and starts at the plan after the last one the previous tick ran, as compactBackgroundHandlers already does for its handlers. A tick that runs out of budget hands the rest of the rotation to the next tick, so no handler or group starves.

Design choices

  • known on main's forwarder. CoordinatorForwarder.LocalLeaderGroupIDs returns LocalLeaderGroupIDsThrough(Inner). Without known, any forwarder over an inner that lacks the capability turns "my inner doesn't say" into "leads no group", the failure mode of bug (1). This matches main's own fallbacks for single-group coordinators, such as IsLeaderForGroup falling back to the inner's leadership.
  • Presence check over canonicalization for legacy rows. With the owner-holds-nothing rule, the raw row is already the canonical value. Streams therefore need no value-reading canonicalization, and a key the owner also holds is migrated once, by the owner's plan.

Behavior change

  • Production: each node migrates the TTL anchors of every data group it leads, in rotation. Each plan scans at most 512 physical rows per tick (30s) and rewrites at most 32 keys per proposal, under the existing 5s tick budget and 1m timeout backoff. Expect migration writes after rollout in proportion to the legacy backlog.
  • API:
    • ShardedCoordinator.LocalLeaderGroupIDs and CoordinatorForwarder.LocalLeaderGroupIDs now return ([]uint64, bool).
    • New ShardStore.ScanGroupOwnedAt.
    • Existing ShardStore behaviour is unchanged.
  • Fail-safe: if a coordinator does not report its groups, the migrator logs ttl inline migrator: coordinator does not report local leader groups; falling back to whole-prefix scans once, then runs one ScanAt per prefix. That path is not new and is not reached in the server.

Wrapper capability guard

Since #1277, both wrappers embed kv.CoordinatorForwarder, which closed every gap in this PR's original audit:

  • LinearizableReadForKey on the gate;
  • EnsureMutationsWriteAllowed, ObserveForwardedRequests, RaftMembers*, PhaseD* and ValidateDurableTimestamp on the label.

The two guard tests check different things:

  • main's TestAdapterCoordinatorWrappersForwardShardedCapabilities checks that every ShardedCoordinator method not listed in shardedMethodsNotForwarded exists on both wrappers with the same signature.
  • This PR's TestStartupWrappersForwardShardedCoordinatorCapabilities uses the same list. It also calls each forwarded method through startupGatedCoordinator and through WithKeyVizLabel(startupGatedCoordinator) against a spy, and requires the call to reach the spy and return the spy's result and error: 30 methods × 2 wrappers.

That second check catches the bug-(1) class, a method that exists but probes its inner through an interface that no longer matches. In a mutation test, I put the forwarder's LocalLeaderGroupIDs probe back on the old []uint64 signature. This test, the chain test and the wiring test all failed; the signature test passed.

Do nodes need a one-off migration catch-up?

No. The regular loop is the catch-up.

  • Cursors reset on restart. Migration cursors live only in memory, so a restart with this fix starts every per-group cursor at the top of each prefix. The rotation then cycles through every handler and locally led group.
  • Reads stay correct meanwhile. The stock server never disables the legacy !redis|ttl| read fallback; WithRedisLegacyTTLReadFallback is not wired to any flag or env var.
  • What stalled and now resumes:
    • rewriting legacy anchors into the inline format;
    • repairing the !redis|ttl| index;
    • physically cleaning up expired TTL-indexed legacy collections. This one affects storage, not correctness.
  • Caveats:
    • Time elapsed since 2026-07-22 says nothing about migration progress. Count from this rollout before considering Phase 3 (disabling the fallback).
    • On split catalogs, the whole-prefix ScanAt reaches only the first route for string, HLL, TTL-index and list anchors. So anchors outside the first route may never have been migrated at all, not just since 2026-07-22. This PR covers them.
    • The different-leader legacy case above stays unmigrated, and Phase 3 must wait for docs: propose TTL-inline migration of legacy-route rows across leaders #1280.
  • Nothing signals that a full pass has finished; I suggest adding that as a follow-up.

Test evidence

  • Each fix was preceded by a failing test:

    • The wrapper chain loses the groups: expected []uint64{0x1, 0x2}, actual []uint64(nil).
    • The split store refuses group 2: explicit group read does not own the requested key range, for all eight prefixes.
    • The staged-route key was returned.
    • A scan read 7 rows for limit=2.
    • A legacy-route hash row was dropped.
    • A legacy stream meta row was dropped, and the compactor left its TTL out (ExpireAt:0x0).
    • A hash key the owner also held was served by both groups.
    • 24 budget-limited ticks scanned the first of 24 plans 24 times.
    • Three owned hash rows cost three point reads.
    • A legacy hash row whose owner had no leader failed the page with leader not found.
  • Mutation checks: each of these fails its targeted tests:

    • dropping the owner presence check, or narrowing the legacy candidate back to wide-column keys;
    • ignoring "cannot answer";
    • always starting the rotation at plan 0;
    • putting the forwarder's LocalLeaderGroupIDs probe back on the old signature.
  • Race suites: go test -race -count=1 ./adapter/... ./kv/... . on a73a7c0a: adapter ok (233s), kv ok (14s), root ok (46s).

  • Lint: golangci-lint v2.9.0, CI's version, with main's config (now including cyclop): 0 issues.

  • End to end: one process hosting groups 1 and 2 split at m, with a legacy string plus a legacy !redis|ttl| anchor seeded on each side.

    Binary Group 1 key Group 2 key "does not own" errors compactor pass failures
    PR at 35f698e (before the split-store fix) migrated still legacy after 100s 4 3
    PR at a73a7c0a (merged with main) migrated migrated (both within 30s) 0 0

    TTL and GET were correct throughout. An earlier single-group run (3 nodes) showed base 184351ae still legacy after 100s, while this PR migrated within 30s.

  • Jepsen: on a 3-node cluster running a73a7c0a, the same workloads as CI with --time-limit 60 --rate 10 --concurrency 10:

    • elastickv.redis-workload: {:valid? true}, 603 ok / 0 fail / 2 info.
    • elastickv.redis-zset-safety-workload: {:valid? true}, 586 ok / 0 fail / 2 info.
    • Zero migrator, ownership or panic lines in the node logs.
    • These workloads write only inline-format data, so the migrator scans each group concurrently with them but has nothing to rewrite. The end-to-end runs cover the rewrite.

Self-review (five lenses)

  1. Data loss:
    • Migration writes are OCC transactions at the scan's readTS, with a commit timestamp from kv.NextTimestampAfterThrough on the dispatching group leader (dispatchCompaction). A user write after readTS conflicts rather than being overwritten.
    • The scan hands the migrator only rows whose value is the one a reader sees: owned rows, and legacy-route rows the owner holds no version of. Never stale copies of moved ranges, never live copies of a route under staged visibility, never a legacy copy the owner has superseded or deleted.
    • Rotation changes only the order of plans.
    • No change to Raft apply, snapshots, Pebble sync or retention.
  2. Concurrency / distributed:
    • LocalLeaderGroupIDs is a point-in-time snapshot. Each key is still checked with IsLeaderForKey before dispatch.
    • The owner presence check runs only on the owner's leader, behind the same lease fence point reads use. Elsewhere the row is skipped.
    • A leadership change between the check and the dispatch is caught by IsLeaderForKey and OCC.
    • The rotation cursor is guarded by stateMu. Concurrent SyncOnce calls only shift where a tick starts.
    • The one-time warning uses sync.Once. Race tests pass.
  3. Performance:
    • One physical page per scan call, plus one in-memory route lookup per row. A legacy-route row adds one local version probe.
    • No per-row point reads; the shared page path keeps its point-read pass for every other caller.
    • Rotation adds no reads; it builds handlers × local-groups plan structs per tick.
    • Followers scan nothing. The Redis request path is unchanged.
  4. Data consistency:
    • The page and the presence check both read at readTS. The presence rule mirrors the point-read fallback exactly, including tombstones.
    • Ownership uses the same resolution as writes.
    • The new code issues no timestamps and does not touch the route catalog.
    • Keeping known-empty distinct from unknown keeps followers from scanning.
    • TLA+ anchors (kv/coordinator.go, kv/sharded_coordinator.go): the change adds a capability interface and helper and changes LocalLeaderGroupIDs' return. Nothing in the HLC-4, renewal-cadence, group-selection, fail-closed or OCC checklist changed, so no spec update is needed.
  5. Test coverage:
    • Regression tests for every bug and review finding: wrapper chain, real split ShardStore, staged routes, physical bound, legacy-route rows (hash and stream, three owner states, owner led elsewhere), legacy stream migration end to end in the compactor, plan rotation, no point reads for served rows.
    • The behavioural guard test and mutation checks.
    • End-to-end runs on real binaries and the Redis Jepsen workloads.

Risk

Low to moderate. This turns back on a background writer that has been dormant in production for about three months, and on split catalogs it reaches parts of the keyspace it never reached before. Its writes are OCC-guarded and rate-bounded. Watch Raft proposal load on group leaders during the first pass after rollout.

🤖 Generated with Claude Code

https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC

The Redis delta compactor's TTL-inline migrator does no work in the
production server. main.go hands it a startupGatedCoordinator, and
NewDeltaCompactor wraps that in WithKeyVizLabel. The label wrapper always
exposes LocalLeaderGroupIDs and answers nil when its inner value lacks
the method, and the startup gate never forwarded it. The migrator then
takes its per-group branch with zero groups, plans no scans, and never
reaches the full-keyspace fallback.

Pin the real chain: a ShardedCoordinator leading groups 1 and 2 and
following group 3, wrapped exactly as main.go wraps it. The test fails
here because LocalLeaderGroupIDs comes back nil through the chain and
SyncOnce issues no ScanGroupAt at all. The fix follows in the next
commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
…rappers

The TTL-inline migrator asks its coordinator which data groups this node
leads so it scans only those. In the server that coordinator is
WithKeyVizLabel(startupGatedCoordinator{ShardedCoordinator}). The label
wrapper always exposed LocalLeaderGroupIDs and answered nil when its
inner value lacked the method, and the startup gate never forwarded it.
The migrator read nil as "this node leads no group", planned no scans,
and never reached its full-keyspace fallback. The migration has done
nothing in production since a36dac7.

Forward LocalLeaderGroupIDs through the startup gate, and make the answer
carry whether it is known. kv.LocalLeaderGroupsCoordinator now returns
(ids, known); ShardedCoordinator always knows, and decorators forward
through kv.LocalLeaderGroupIDsThrough, which reports a coordinator
without the capability as unknown. A decorator that drops the forward
can no longer turn "unknown" into "no groups": the migrator falls back
to the full-keyspace scan it already had, filtered per key by
IsLeaderForKey, and logs once that it did. A known empty list still
plans nothing, since this node then leads no group.

The regression test from the previous commit now passes; only its two
probe lines change for the new signature. Adapter and kv tests pin the
unknown and known-empty cases; both fail against the old "nil means no
groups" behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
…rappers

Capabilities are optional interfaces found by type assertion, so a
wrapper that does not forward one fails silently: the probe misses and
the consumer takes a fallback, or the outer wrapper answers on the
inner's behalf. That has now cost the Redis read fence its group
routing, Internal.Forward its route-floor gate, and the Redis TTL-inline
migration all of its work, each time with green unit tests that used an
unwrapped coordinator.

Classify every exported ShardedCoordinator method by the surface its
consumers probe: the adapters' WithKeyVizLabel(startupGatedCoordinator),
the startupGatedCoordinator main probes itself (internal gRPC server
options, S3 blob peer discovery), or the raw coordinator main drives
directly (lifecycle, HLC lease renewal, autosplit, the legacy TSO
bridge). A new method fails the test until it is classified. Each
forwarded capability is called through its surface against a spy and
must reach the spy and return its result and error.

The test fails on one gap: the startup gate does not forward
LinearizableReadForKey, so WithKeyVizLabel serves it from the default
group's LinearizableRead and a key in another group would be fenced
against the wrong group. Nothing in the server calls it through the
adapters today, so the gap is latent. The next commit closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
WithKeyVizLabel exposes LinearizableReadForKey to the adapters and serves
it from the plain LinearizableRead when its inner coordinator lacks it.
The startup gate in between did not forward it, so through the adapters'
coordinator a key owned by a non-default group would get a read fence on
the default group. Nothing in the server calls it that way today, so the
gap was latent; the capability guard from the previous commit flagged it.

Forward it, falling back to LinearizableRead for an inner coordinator
without a key-routed fence -- one that owns a single group -- as
WithKeyVizLabel already does. The guard now passes, and a unit test pins
the fallback branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9c92e025-2aa4-4805-a23b-b035e68a3c0b
📥 Commits

Reviewing files that changed from the base of the PR and between a73a7c0 and 5ac9319.

📒 Files selected for processing: 1
  • internal/backup/import_boundary_test.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

TTLインライン移行を、ローカルリーダーグループごとの走査計画で実行するよう変更します。グループ所有行のページ走査と再開位置を追加します。グループ情報が不明な場合は全プレフィックス走査に切り替えます。バックアップ依存関係の許可リストも更新します。

Changes: TTLインライン移行

Layer / File(s) Summary
リーダーグループ情報の転送
kv/coordinator.go, kv/sharded_coordinator.go, kv/coordinator_forwarder.go, main.go, kv/keyviz_label_test.go, main_coordinator_capabilities_test.go, main_coordinator_chain_test.go
ローカルリーダーグループ一覧に既知状態を追加します。コーディネーターのラッパーが一覧と状態を転送します。テストではラッパー経由の能力転送を検証します。
グループ所有キーの走査
kv/shard_store.go, kv/shard_store_test.go
ScanGroupOwnedAtが指定グループの所有行と再開位置を返します。旧ルート行、ステージング可視性、所有権確認のケースをテストします。通常の前方走査ではstored-page取得とwide-column行の正規化を分離します。
TTL移行の走査計画と接続
adapter/redis_ttl_inline_migrator.go, adapter/redis_delta_compactor.go, adapter/redis_delta_compactor_test.go, adapter/redis_ttl_inline_migrator_plan_test.go, main_redis_ttl_migration_wiring_test.go
移行処理を計画単位でローテーションし、スキャン予算を共有します。グループ情報が既知の場合はローカルリーダーグループを走査し、不明な場合は全プレフィックス走査に切り替えます。テストでは分割シャード、ページ再開、旧ルートのメタデータ移行を検証します。

バックアップ依存関係境界

Layer / File(s) Summary
SSTable依存関係の許可
internal/backup/import_boundary_test.go
pebble/v2/sstableを許可依存関係に追加します。依存関係が許可リストに一致する場合、禁止依存関係として扱いません。

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant RedisTTLInlineMigrator
  participant CoordinatorForwarder
  participant ShardedCoordinator
  participant ShardStore
  RedisTTLInlineMigrator->>CoordinatorForwarder: ローカルリーダーグループ一覧と既知性を取得
  CoordinatorForwarder->>ShardedCoordinator: LocalLeaderGroupIDsを呼び出す
  ShardedCoordinator-->>CoordinatorForwarder: グループID一覧と既知状態を返す
  CoordinatorForwarder-->>RedisTTLInlineMigrator: グループID一覧と既知状態を返す
  RedisTTLInlineMigrator->>ShardStore: 各グループのScanGroupOwnedAtを呼び出す
  ShardStore-->>RedisTTLInlineMigrator: 所有行と再開位置を返す
Loading

Suggested reviewers: claude

Merge Risk: ⚪ Minimal · up to 5ac93

The change narrowly permits the backup SSTable reader while retaining the stated restriction on other Pebble packages. No actionable merge risk is evident in the supplied scope.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 34 functions across 16 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed タイトルは、ローカルリーダーの各シャードでRedis TTL-inline移行を復旧するという主要変更を正確かつ具体的に示しています。
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

TLA+ spec divergence review (auto-triggered)

This PR touches files that the TLA+ safety spec has an anchor on (per
docs/design/2026_05_28_implemented_tla_safety_spec.md §3),
so an AI review is requested below to verify the implementation has not drifted
from the model.

Anchored files changed in this PR head (35f698e):

  • kv/coordinator.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.go — Next() must respect the HLC-4 preconditions (i)/(ii)/(iii) from the design doc: bounded skew, logical-counter handoff on leader change (strategy (c) Observe(MaxAppliedHLC)), and the commit-time ceiling fence (fail-closed when wall_now >= physicalCeiling). Any change to the bit layout (48/16), the CAS loop, or the ceiling getter/setter is in scope.
  • kv/coordinator.go, kv/sharded_coordinator.go — RunHLCLeaseRenewal, hlcRenewalInterval, hlcPhysicalWindowMs constants, and the new-term detection that calls Observe(fsm.MaxAppliedHLC()) (strategy (c)). Any change to renewal cadence, group selection, or fail-closed behaviour is in scope.
  • kv/transaction.go, kv/lock_resolver.go — OCC commit-ts assignment, lock-map encoding (key, lock_ts) -> start_ts, and the LockResolver action OCC-3 depends on. (M2 spec will land OCC-1..OCC-5; until then the spec doc §5.2 is the contract.)
  • kv/fsm.go — FSM apply of HLC lease entries (SetPhysicalCeiling), and any future MaxAppliedHLC() accessor that strategy (c) needs.
  • store/mvcc_store.go — version visibility, snapshot install, and the MVCC-1..MVCC-4 invariants (M3 scope).
  • distribution/** — route catalog versioning, SplitRange atomicity, and CatalogWatcher async fan-out (M4 scope).

If the change is correct but requires a spec update, edit tla/hlc/HLC.tla (or the corresponding M2..M5 module once landed) and the design doc in the same PR. The tla-check workflow runs the TLC model check on the same paths.


@claude review please verify TLA+ spec divergence per the checklist above.

@codex review please verify TLA+ spec divergence per the checklist above.

@bootjp
bootjp marked this pull request as ready for review October 16, 2026 16:28
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T11:06:14.920897Z 5ac9319 Manual request
🔒 Security Review ✅ Completed 2026-10-17T11:06:58.872449Z 5ac9319 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35f698ee9d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_ttl_inline_migrator.go
Review on #1278 found that the per-group TTL-inline scans only work on a
single-route store. The migrator scans each locally led group with
ShardStore.ScanGroupAt over a bare anchor prefix such as "!redis|str|".
ScanGroupAt resolves the requested range through the route catalog, and
a bare prefix resolves to the first route only, so every other group is
refused with ErrExplicitGroupRouteOwnerMismatch. A node leading several
shards migrates at most the first route's anchors and fails every tick
on the rest. The existing tests hand the compactor a store whose
ScanGroupAt ignores routing, so they could not see it.

Run the migrator over a real ShardStore split at "m" across two groups
and require legacy string and hash anchors on both sides to migrate.
It fails here on group 2's scans for all eight anchor prefixes. The fix
follows in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
The TTL-inline migrator scanned each locally led group with
ShardStore.ScanGroupAt over a bare anchor prefix. ScanGroupAt resolves
the requested range through the route catalog first, and a bare prefix
such as "!redis|str|" resolves to the first route only, so every other
group was refused with ErrExplicitGroupRouteOwnerMismatch. On a store
with more than one route, a node migrated at most the first route's
anchors and failed every tick on the rest.

Add ShardStore.ScanGroupOwnedAt. It scans the raw range on the group's
own store, fenced like the other explicit-group reads, and keeps only
keys whose route the group currently owns, judged by the same
routeAndGroupForKey that routes writes. It keeps paging past skipped
keys, so fewer than limit results still means the range is exhausted.
A group's store can still hold copies of ranges it no longer owns, and
ScanGroupAt returned those to the first route's group; acting on one
could rewrite a key from a stale value, so skipping them matters. Each
call reads about one page, the same per-tick cost a36dac7 intended,
instead of one walk per owned route.

The migrator uses it for its per-group plans. The split-store
regression test from the previous commit now passes. kv tests cover both
anchor encodings (user key directly after the prefix, and length
prefixed), a stale copy, and paging past a page of skipped keys.

Also correct the fallback wording: when the groups are unknown, the
migrator runs one ScanAt over the whole prefix, which on a split catalog
reaches only the first route for the string, HLL, TTL and list anchors.
That path is not new and is not reached in the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
@bootjp bootjp changed the title kv, main: restore the Redis TTL-inline migration behind the startup gate kv, main, adapter: restore the Redis TTL-inline migration on every locally led shard Oct 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 301aaa060c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kv/shard_store.go Outdated
Comment thread kv/shard_store.go Outdated
Comment thread kv/shard_store.go Outdated
…y gaps

Review of 301aaa0 found three gaps in ShardStore.ScanGroupOwnedAt:

- It scans with a bare group route, which carries no staged-visibility
  flag, so a route being imported is read from its live rows only. An
  older live copy could feed a rewrite while a newer staged copy waits
  for promotion.
- It reads physical pages until it has limit owned rows. A group
  holding a large backlog of rows of ranges it no longer owns turns one
  call into a walk over the whole backlog, which overruns the tick
  budget and never advances the cursor.
- It keeps a key only when its logical owner is the scanned group.
  Hash, set and zset rows written before wide-column routing still sit
  at the route of their raw key, and reads fall back to them there, so
  dropping them leaves those anchors unmigrated.

Pin each one: the staged route's key must be deferred, one call must
read at most one page, a legacy-route row must be served with the value
a point read sees, a tick must stop after one page of stale rows and
resume on the next, and the migrator must rewrite a legacy-route hash
meta into its user key's group. All five fail here; the fix follows in
the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
…egacy rows

Close the three gaps the previous commit reproduced in
ShardStore.ScanGroupOwnedAt:

- One physical page per call. It now reads a single page and returns
  the last key that page read when it was full, nil once the range is
  exhausted. The migrator moves its cursor to that key instead of to the
  last row it kept, so a page holding no row the group serves still
  advances the cursor and a tick stays within one page per plan.
- Staged routes are deferred. A key whose route is under staged
  visibility is skipped: the page reads live rows only, and an older
  live copy must not feed a rewrite while a newer staged copy waits for
  promotion. Such keys are picked up on a later pass.
- Legacy-route rows are served. Hash, set and zset rows still stored at
  the route of their raw key are kept when that route is the scanned
  group. The page has already re-read them through the point-read path,
  so they carry the value a reader sees, not a shadowed copy. The
  migrator still writes one only when this node leads the user key's
  group, as the HLC rule requires.

The earlier stale-copy test used a hash key, which this commit now
correctly serves at its legacy route; it uses string keys, which have no
legacy route, instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50494e865a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kv/shard_store.go Outdated
Comment thread adapter/redis_ttl_inline_migrator.go
main (#1277) added kv.CoordinatorForwarder, which both coordinator
wrappers now embed, and fixed the same wrapper bug this branch fixed by
hand-forwarding. Resolution:

- Take main's startupGatedCoordinator and keyVizLabeledCoordinator. The
  forwards this branch added to them (LocalLeaderGroupIDs on both,
  LinearizableReadForKey on the gate) are served by the forwarder.
- Keep this branch's known contract on the forwarder itself:
  CoordinatorForwarder.LocalLeaderGroupIDs returns ([]uint64, bool)
  through kv.LocalLeaderGroupIDsThrough, so an inner coordinator without
  the capability stays unknown and the TTL-inline migration falls back to
  whole-prefix scans instead of planning none.
- Update main's chain and plan tests to the ([]uint64, bool) signature
  and to ScanGroupOwnedAt.
- The behavioural capability guard now classifies methods with main's
  shardedMethodsNotForwarded list and calls every forwarded method
  through both wrappers. Drop the gate's LinearizableReadForKey fallback
  test: the gate no longer has its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
…tate

ScanGroupOwnedAt serves a hash, set or zset row stored at the route of
its raw key whenever the scan canonicalized it, and never a stream row:

- A legacy stream meta row whose user key belongs to another group is
  dropped, so the stream TTL-inline handler never migrates it, even when
  this node leads both groups (Codex review).
- A wide-column key the logical owner also holds is served by both
  groups. The migrator then rewrites it from both plans in one tick, and
  the second dispatch fails its OCC check.

Point reads fall back to the legacy route only when the logical owner
holds no version of the key, so that is when the legacy group should
serve it. These tests fail until the scan follows that rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
ScanGroupOwnedAt now serves a row stored at the route of its raw key
only when this node leads the key's logical owner and that group holds
no version of the key at the read timestamp: exactly when a point read
falls back to the legacy route, so the row is the value a reader sees.

- Stream metadata is covered: the legacy route candidate is now
  legacyPointRouteKey, the one point reads use, which includes streams.
  Streams are not canonicalized by the scan, and they need not be: when
  the owner holds nothing, the raw row is the canonical value.
- A wide-column key the owner also holds is served only by the owner,
  so the migrator rewrites it once per tick instead of from two plans
  whose second dispatch failed its OCC check.
- When another node leads the owner, or this node does not host it, the
  row is left out; only the owner's leader may rewrite the key.

Cost: one local version probe per legacy-route row, through the same
lease-fenced check point reads already use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
Every TTL-inline scan plan (one per handler and locally led group)
shares the compactor's per-tick budget, and every tick starts from the
first handler's first group. When the earlier plans use up the budget,
the later handlers and groups are never reached (Codex review). With
three local leader groups, 24 budget-limited ticks scan the first plan
24 times and no other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
Each tick now flattens every handler's scan plans (one per handler and
locally led group) into one list and starts at the plan after the last
one the previous tick ran. A tick that runs out of budget hands the rest
of the rotation to the next tick instead of starting over at the first
handler's first group, so no handler or group is starved while the
earlier plans keep using up the budget.

The background delta handlers already rotate this way. Their two cursor
helpers now take the cursor they rotate, and both loops share them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
ScanGroupOwnedAt reads its page through scanRouteAtForwardPage, which
re-reads every hash, set and zset row through the point-read path. Under
the owner-holds-nothing rule every served row already carries the value
a point read returns, so those reads are redundant, and some do harm:

- one extra point read per owned wide-column row;
- a proxied read to the owner's leader for every legacy-route row the
  scan then drops;
- a page that fails outright with "leader not found" when a legacy
  row's owner has no known leader, so the plan's cursor cannot advance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
The three forward page paths (local, leader, proxy) each re-read the
page's hash, set and zset rows through the point-read path. That pass
now runs once, in scanRouteAtForwardPage, after whichever path read the
page, so its callers behave as before. ScanGroupOwnedAt reads the page
through the new scanRouteAtForwardStoredPage, without the pass: every row
it serves is owned or held by no one at the owner, so it already carries
the value a point read returns.

This drops one point read per owned wide-column row, a proxied read per
legacy-route row the scan then drops, and the page failure when such a
row's owner has no known leader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
@github-actions

Copy link
Copy Markdown
Contributor

TLA+ spec divergence review (auto-triggered)

This PR touches files that the TLA+ safety spec has an anchor on (per
docs/design/2026_05_28_implemented_tla_safety_spec.md §3),
so an AI review is requested below to verify the implementation has not drifted
from the model.

Anchored files changed in this PR head (a73a7c0):

  • kv/coordinator.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.go — Next() must respect the HLC-4 preconditions (i)/(ii)/(iii) from the design doc: bounded skew, logical-counter handoff on leader change (strategy (c) Observe(MaxAppliedHLC)), and the commit-time ceiling fence (fail-closed when wall_now >= physicalCeiling). Any change to the bit layout (48/16), the CAS loop, or the ceiling getter/setter is in scope.
  • kv/coordinator.go, kv/sharded_coordinator.go — RunHLCLeaseRenewal, hlcRenewalInterval, hlcPhysicalWindowMs constants, and the new-term detection that calls Observe(fsm.MaxAppliedHLC()) (strategy (c)). Any change to renewal cadence, group selection, or fail-closed behaviour is in scope.
  • kv/transaction.go, kv/lock_resolver.go — OCC commit-ts assignment, lock-map encoding (key, lock_ts) -> start_ts, and the LockResolver action OCC-3 depends on. (M2 spec will land OCC-1..OCC-5; until then the spec doc §5.2 is the contract.)
  • kv/fsm.go — FSM apply of HLC lease entries (SetPhysicalCeiling), and any future MaxAppliedHLC() accessor that strategy (c) needs.
  • store/mvcc_store.go — version visibility, snapshot install, and the MVCC-1..MVCC-4 invariants (M3 scope).
  • distribution/** — route catalog versioning, SplitRange atomicity, and CatalogWatcher async fan-out (M4 scope).

If the change is correct but requires a spec update, edit tla/hlc/HLC.tla (or the corresponding M2..M5 module once landed) and the design doc in the same PR. The tla-check workflow runs the TLC model check on the same paths.


@claude review please verify TLA+ spec divergence per the checklist above.

@codex review please verify TLA+ spec divergence per the checklist above.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a73a7c0a59

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kv/shard_store.go

bootjp commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

test (ubuntu-latest) is red, but the failure is not from this PR. It is red on main too: the "Test" workflow failed on main at 927e4bf3, and the test fails identically on a local checkout of that commit.

The failing test is TestBackupLinksNoLiveClusterCode in internal/backup:

import_boundary_test.go:51: internal/backup links github.com/cockroachdb/pebble: github.com/bootjp/elastickv/internal/backup -> github.com/cockroachdb/pebble/v2/sstable

Two commits that landed on main independently collide:

This PR doesn't touch internal/backup. The other jobs (test (encryption=on/off), golangci-lint, build, tla-check) are green on a73a7c0a.

There is no fix PR yet. Choosing between the two designs is a maintainer call. The narrowest change I can see allows the sstable reader explicitly, since it is a file-format reader rather than a live store. I checked this patch on a scratch checkout of main, where go test ./internal/backup/ then passes:

--- a/internal/backup/import_boundary_test.go
+++ b/internal/backup/import_boundary_test.go
@@ -24,6 +24,13 @@ var backupForbiddenDeps = []string{
 	"github.com/cockroachdb/pebble",
 }
 
+// backupAllowedDeps are exceptions inside a forbidden prefix. Snapshot SST
+// ingest bundles (snapshot_sst.go) are files in pebble's sstable format, and
+// reading them takes pebble's sstable reader, not a live store.
+var backupAllowedDeps = []string{
+	"github.com/cockroachdb/pebble/v2/sstable",
+}
+
@@ -59,6 +66,11 @@ func TestBackupLinksNoLiveClusterCode(t *testing.T) {
 func forbiddenBackupDep(pkgPath string) string {
+	for _, allowed := range backupAllowedDeps {
+		if pkgPath == allowed {
+			return ""
+		}
+	}
 	for _, forbidden := range backupForbiddenDeps {

The other option is to move the SST reading out of internal/backup. I'm keeping both out of this PR. Once main carries a fix, I'll merge it in so this check re-runs.


Generated by Claude Code

@bootjp

bootjp commented Oct 17, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: a73a7c0a59

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: a73a7c0a59

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

TestBackupLinksNoLiveClusterCode fails on main since two merges met:
3f097df (#1276) taught internal/backup to read snapshot SST ingest
bundles with github.com/cockroachdb/pebble/v2/sstable, and 5678081
(#1277) forbade every github.com/cockroachdb/pebble package there.

Allow exactly the sstable package. The bundles are files in pebble's
sstable format, and the reader is a file-format dependency, not the
live store the boundary keeps out; the rest of pebble stays forbidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P5kzueGgfe3F5iSe8TMtC
@github-actions

Copy link
Copy Markdown
Contributor

TLA+ spec divergence review (auto-triggered)

This PR touches files that the TLA+ safety spec has an anchor on (per
docs/design/2026_05_28_implemented_tla_safety_spec.md §3),
so an AI review is requested below to verify the implementation has not drifted
from the model.

Anchored files changed in this PR head (5ac9319):

  • kv/coordinator.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.go — Next() must respect the HLC-4 preconditions (i)/(ii)/(iii) from the design doc: bounded skew, logical-counter handoff on leader change (strategy (c) Observe(MaxAppliedHLC)), and the commit-time ceiling fence (fail-closed when wall_now >= physicalCeiling). Any change to the bit layout (48/16), the CAS loop, or the ceiling getter/setter is in scope.
  • kv/coordinator.go, kv/sharded_coordinator.go — RunHLCLeaseRenewal, hlcRenewalInterval, hlcPhysicalWindowMs constants, and the new-term detection that calls Observe(fsm.MaxAppliedHLC()) (strategy (c)). Any change to renewal cadence, group selection, or fail-closed behaviour is in scope.
  • kv/transaction.go, kv/lock_resolver.go — OCC commit-ts assignment, lock-map encoding (key, lock_ts) -> start_ts, and the LockResolver action OCC-3 depends on. (M2 spec will land OCC-1..OCC-5; until then the spec doc §5.2 is the contract.)
  • kv/fsm.go — FSM apply of HLC lease entries (SetPhysicalCeiling), and any future MaxAppliedHLC() accessor that strategy (c) needs.
  • store/mvcc_store.go — version visibility, snapshot install, and the MVCC-1..MVCC-4 invariants (M3 scope).
  • distribution/** — route catalog versioning, SplitRange atomicity, and CatalogWatcher async fan-out (M4 scope).

If the change is correct but requires a spec update, edit tla/hlc/HLC.tla (or the corresponding M2..M5 module once landed) and the design doc in the same PR. The tla-check workflow runs the TLC model check on the same paths.


@claude review please verify TLA+ spec divergence per the checklist above.

@codex review please verify TLA+ spec divergence per the checklist above.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ac93191a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return plans
c.warnTTLInlineLocalLeaderGroupsUnknown()
}
return []ttlInlineMigrationScanPlan{{cursorName: h.typeName}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve all shards in the unknown-capability fallback

When c.st is a split *kv.ShardStore but a coordinator decorator does not forward LocalLeaderGroupIDs, this fallback creates a plain ScanAt plan. For bare string, HLL, TTL-index, and list prefixes, routesForInternalScanWithVersion resolves only the prefix's catalog route, so IsLeaderForKey can filter returned rows but cannot discover anchors on the other shards. Thus the advertised fail-safe still silently leaves most locally led shards unmigrated in exactly the missing-capability scenario; the fallback needs a way to enumerate/scan every relevant group rather than treating ScanAt as a whole-prefix physical scan.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The limit is real: on a split catalog, the unknown-groups fallback's ScanAt reaches only the first route for the bare string, HLL, TTL-index and list prefixes. This was already the behaviour before a36dac7. I'm not adding code for it here, because no caller can reach that combination:

  • main.go:413 passes startupGatedCoordinator{ShardedCoordinator}, wrapped later in WithKeyVizLabel. Both wrappers embed kv.CoordinatorForwarder, so the groups are always known.
  • cmd/server/demo.go:589 passes a single-group Coordinate with a plain store. That store is not a group scanner, so the fallback runs one ScanAt over a single route, which covers the whole prefix.
  • A future decorator that drops the capability fails TestStartupWrappersForwardShardedCoordinatorCapabilities and the wiring test before it ships. Even if it did ship, the fallback logs once instead of silently planning nothing.

Making the fallback cover every shard would mean having each node scan every group, proxied to remote leaders. Those are the whole-keyspace scans a36dac7 removed, built only for a configuration the tests already reject. I've put this call to the maintainer and am leaving the thread open in case they want it anyway.


Generated by Claude Code

@bootjp

bootjp commented Oct 17, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ac93191a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kv/shard_store.go
if g, ok := s.groupForID(owner.GroupID); !ok || g == nil || g.Store == nil {
return false, nil
}
exists, answered, err := s.routeHasVersionAtOrBefore(ctx, owner, key, ts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fence the owner group before declaring its key absent

When the scanned legacy group and logical-owner group are co-led but the owner has a committed-yet-unapplied metadata write, this direct presence probe can report the owner key absent because routeHasVersionAtOrBefore only verifies leadership and does not perform the owner group's lease/read-index fence. If another local group has already advanced readTS beyond that write's commit timestamp, the migrator then derives a rewrite from the stale legacy row; after the pending owner write applies, the migration's OCC check sees its timestamp as <= readTS and overwrites it rather than conflicting. Fence the owner group before probing it, and cover this committed-but-unapplied cross-group sequence.

AGENTS.md reference: AGENTS.md:L22-L25

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced this. The interleaving is real, but it is not specific to this probe, and a probe-local fence would not close it.

The probe is already fenced on the owner group. routeHasVersionAtOrBefore returns "cannot answer" unless isLinearizableRaftLeader(ctx, ownerEngine) passes. That check runs leaseReadEngineCtx on the owner group's engine: it uses the lease when the lease is fresh, and otherwise a LinearizableRead that waits for apply. This is the same fence, and the same call, that GetAtWithReadFence uses through routeMissStopsPointFallback to decide whether a point read falls back to the legacy route.

What remains is audit gap G0 across groups. That gap is open in docs/design/2026_09_26_proposed_serializable_isolation_audit.md §2 ("Multi-group deployments"), reproduced by TestApplyOrder_GlobalSnapshotMissesLaggingGroupWrite. The compactor's readTS comes from snapshotTS, the maximum applied watermark over all groups. A write to owner group P whose commit timestamp is below that maximum, but which has not yet applied on P, is missed by any read at readTS, and OCC ignores it because c1 ≤ startTS. A barrier on P closes only the committed-but-unapplied half: a write still in flight when the barrier runs lands later and is missed the same way.

The same exposure exists today, independent of this PR:

  • the point-read fallback, which returns the legacy value under that same interleaving;
  • this migrator's owned-row path, and buildElems' reads of P;
  • every adapter transaction started at the global snapshot.

The fix is the audit's A0: advance a lagging group's watermark to the snapshot before reading it, plus the apply-order fence. That is a per-group Raft entry on the read path, outside this PR's scope. Adding a LinearizableRead here would cost a read-index round trip per legacy row and still leave the in-flight half open.

I'm leaving this thread open for the maintainer to decide.


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 5ac93191a9

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants