Skip to content

adapter: Fence Redis reads before snapshots - #1167

Open
bootjp wants to merge 15 commits into
mainfrom
feature/fix-redis-jepsen-anomaly
Open

adapter: Fence Redis reads before snapshots#1167
bootjp wants to merge 15 commits into
mainfrom
feature/fix-redis-jepsen-anomaly

Conversation

@bootjp

@bootjp bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fence Redis read snapshots behind per-key leader verification for standalone LRANGE.
  • Apply the same leader fence before snapshot selection for MULTI/EXEC in both direct and one-phase dedup paths.
  • Add regression coverage for stale snapshot reads when leader verification catches the local store up.

Root cause

The scheduled Redis Jepsen append workload exposed a strict-serializability anomaly during leader churn. LRANGE and MULTI/EXEC could choose an MVCC readTS before the quorum-backed VerifyLeaderForKey path had caught the local replica up, so a successful read could observe an older local snapshot.

Validation

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(RangeListVerifiesLeaderBeforeSnapshot|ExecVerifiesLeaderBeforeSnapshot)' -count=1
  • Commit hook: golangci-lint --config=.golangci.yaml run --fix (0 issues)
  • Local Redis Jepsen run with CI-shaped parameters: {:valid? true} / Everything looks good!

Note: go test ./adapter as a full package run timed out after 10 minutes. The timeout dump included TestMilestone1SplitRange_RestartReloadsCatalog; that test passed when run in isolation with a 180s timeout.

Summary by CodeRabbit

  • 改善
    • リスト取得およびRedisトランザクション実行を、読取フェンスとシャードリーダー整合性に基づくルーティングへ更新しました。
    • スキャン中のルート変化を検知し、必要に応じて再試行・フェンス付きスナップショットで整合性を確保します。
    • composed-route系エラーの扱いと、再試行/再利用の条件を調整しました。
  • テスト
    • 読取フェンスキー/ルートバージョン変化時のリトライ挙動、プロキシ経路、失敗閉鎖、ObservedRouteVersionの検証を追加しました。

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Redis のリスト読み取りと MULTI/EXEC に read fence、シャードリーダールーティング、ObservedRouteVersion の伝搬・検証を導入します。ルート変更時の再試行、dedup/reuse、ワイヤエラー復元、リストキー正規化も更新します。

Changes

Redis read-fence routing

Layer / File(s) Summary
Read-fence and observed-version contracts
kv/shard_store.go, kv/shard_key.go, store/list_helpers.go, kv/transcoder.go, kv/fsm.go, kv/sharded_coordinator.go, *_test.go
フェンス対象グループとルートバージョンを提供し、リスト内部キーをユーザーキーへ正規化します。観測バージョンのゼロ値は未ピン状態と明示的なピン留め状態に区別されます。
List read fencing and leader routing
adapter/redis_lists.go, adapter/redis_proxy_leader.go, adapter/redis_txn_test.go
rangeList が読み取りグループをフェンスし、必要に応じてキー別リーダーへプロキシするか、安定性を検証しながらスナップショットから読み取ります。
Transaction fence routing and execution
adapter/redis_txn.go, adapter/redis_txn_test.go
MULTI/EXEC がフェンスグループを集約し、リーダー整合性とルート安定性を検証して、プロキシまたはローカルのスナップショット実行へ進みます。
Retry and reuse error classification
adapter/redis_retry.go, adapter/redis_proxy_leader.go, adapter/redis_error_prefix_test.go, adapter/redis_lists.go, adapter/redis_stream_cmds.go, adapter/redis_retry_test.go
Composed-route のワイヤエラーを復元して再試行対象にし、EXEC の終端エラーと list/stream の再利用可能エラーを分類します。

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RedisClient
  participant RedisServer
  participant ReadFenceCoordinator
  participant ShardLeader
  RedisClient->>RedisServer: EXEC queued commands
  RedisServer->>ReadFenceCoordinator: transactionProxyRoute
  ReadFenceCoordinator->>ReadFenceCoordinator: redisReadFencedTimestamp
  ReadFenceCoordinator->>ShardLeader: proxyTransactionToLeaderForKey
  RedisServer->>RedisServer: applyExecQueueAtSnapshot
Loading

Possibly related issues

  • bootjp/elastickv issue 557 — Redis の lrange を lease read で処理する目的と、今回の list read fence 導入が重なります。

Possibly related PRs

  • bootjp/elastickv#752rangeList 周辺のリーダー検証呼び出しと今回の read-fence 経路が重なります。
  • bootjp/elastickv#796 — list-push の dedup/reuse と今回の再利用判定変更が直接関連します。
  • bootjp/elastickv#1121 — Redis wire エラー復元と再試行分類を同じ経路で扱います。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Redisのスナップショット前フェンス導入という主要変更を簡潔に表しており、内容とも整合しています。
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

@bootjp

bootjp commented Jul 29, 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: cbc99dd14a

ℹ️ 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_txn.go Outdated
Comment on lines +60 to +61
if err := r.coordinator.VerifyLeaderForKey(ctx, key); err != nil {
return errors.WithStack(err)

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 Use leader-routed fences for queued shard keys

In a multi-group deployment with leadership balanced across nodes, exec proxies only to the default-group leader (IsLeader()), but this call verifies each key against that node's local shard engine. ShardedCoordinator.VerifyLeaderForKey does not forward to the key's leader, so even a single-key EXEC targeting a non-default shard led by another node returns a leader error (and cross-shard EXEC cannot succeed unless one node leads every involved group). Use a leader-routed per-shard fence or otherwise route execution without requiring the default leader to lead each key's group.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_lists.go Outdated
// VerifyLeaderForKey honours the caller's deadline rather than the
// long-lived handlerContext + verifyLeaderEngineCtx fallback. Same
// shape as keys() / FLUSHDB.
if err := r.coordinator.VerifyLeaderForKey(ctx, key); err != nil {

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 Wait for the read index to apply before snapshotting

When the local leader has committed but not yet applied entries, VerifyLeaderForKey is not an applied-state fence: it reaches Engine.VerifyLeader, which calls submitRead(ctx, false), and handleReadStates completes such requests immediately without waiting for e.applied. Consequently this call can succeed while the MVCC store and its LastCommitTS remain stale, so the immediately selected LRANGE snapshot can still omit a write that completed before the read; the new EXEC pre-pass has the same problem. Use the linearizable/lease-read path that waits for the read index to be applied before selecting the snapshot.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go Outdated
if !ok {
continue
}
for _, key := range redisCommandGetKeys(meta, cmd.Args) {

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 Fence each Raft group once instead of every Redis key

When a valid transaction contains many distinct keys, such as one variadic DEL or EXISTS, this loop performs a sequential VerifyLeaderForKey for every key even when all keys resolve to the same Raft group. Each verification submits a quorum-backed Raft read request, so a large command creates hundreds or thousands of serialized ReadIndex rounds under the single 30-second EXEC context, causing otherwise valid transactions to time out and unnecessarily loading the Raft group. Resolve and deduplicate the affected groups, then fence each group once.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go Outdated
continue
}
seen[keyID] = struct{}{}
if err := r.coordinator.VerifyLeaderForKey(ctx, key); err != nil {

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 Route fences through Redis-safe storage keys

For a legal Redis user key that resembles another adapter's namespace, such as !sqs|foo, passing the raw command argument to VerifyLeaderForKey changes its routing identity: routeKey treats the raw !sqs|... value as SQS-internal and routes it to the global SQS group, whereas the actual Redis rows (for example !redis|str|!sqs|foo or list-family rows) normalize back to the literal Redis user key and may belong to a different shard. The pre-pass can therefore fence an unrelated group, fail because this node does not lead that group, and leave the actual Redis data shard unfenced. Derive the fence footprint from canonical Redis storage keys, retaining a separate legacy-bare-key fence only where fallback reads require it.

Useful? React with 👍 / 👎.

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Updated the Redis read fence implementation:

  • Replaced the queued EXEC pre-pass with lease-read fences so the local FSM has applied the read index before snapshot selection.
  • Deduplicated the pre-pass by Raft group through LeaseReadGroupKeys.
  • Built the fence footprint from Redis storage/read-fence keys and avoided raw !sqs|... user-key routing for legacy bare-key fences.
  • Switched standalone LRANGE to the same lease-read fence before taking readTS.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(RangeListVerifiesLeaderBeforeSnapshot|ExecVerifiesLeaderBeforeSnapshot|ExecReadFenceUsesRedisStorageKeys)' -count=1
  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(ExecDedup|SetDedup|List|Txn|RangeListVerifiesLeaderBeforeSnapshot|ExecVerifiesLeaderBeforeSnapshot|ExecReadFenceUsesRedisStorageKeys)' -count=1
  • commit hook golangci-lint --config=.golangci.yaml run --fix (0 issues)

@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: 0ae2c34ac7

ℹ️ 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_txn.go Outdated
Comment on lines +114 to +115
for _, key := range kv.LeaseReadGroupKeys(r.coordinator, redisQueuedCommandReadFenceKeys(queue)) {
if _, err := kv.LeaseReadForKeyThrough(r.coordinator, ctx, key); err != nil {

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 Preserve EXEC routing when shard leaders differ

When a sharded cluster elects the default group and a queued key's group on different nodes, exec remains on the default-group leader because it checks only coordinator.IsLeader(), but this loop then calls LeaseReadForKey against the local follower engine for the key's group and returns a leadership error. Previously the underlying ShardStore reads could proxy to each key's leader, so even a single-key EXEC targeting a non-default shard now fails unless its leader happens to be colocated with the default leader; the fence must be established through the owning leader or the transaction must be routed accordingly.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_lists.go Outdated
return r.proxyLRange(key, routeKey, startRaw, endRaw)
}

if _, err := kv.LeaseReadForKeyThrough(r.coordinator, ctx, routeKey); err != nil {

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 every storage group read by LRANGE

In a sharded layout where a list's storage representations resolve to more than one Raft group, this fences only the listMetaKey group before choosing readTS. The subsequent keyTypeAt and resolveListMeta calls also read string/type keys and store.ListMetaDeltaScanPrefix(key); a completed write whose local apply is pending on one of those other groups can therefore be omitted from the snapshot, returning a stale type or range. Standalone LRANGE should group-deduplicate and fence the same storage footprint used by the EXEC path before selecting the timestamp.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest Redis read-fence review findings:

  • Route keyed EXEC by the queued commands' Redis storage-fence groups instead of the default Raft leader.
  • Proxy single/common-leader read-fence groups to the owning shard leader and fail closed when an EXEC spans split shard leaders.
  • Fence LRANGE using the same Redis storage/read-fence footprint as EXEC before selecting the snapshot timestamp.
  • Added regression coverage for shard-local EXEC routing, remote shard proxy routing, split-leader fail-closed behavior, and multi-group LRANGE fencing.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(RangeListVerifiesLeaderBeforeSnapshot|RangeListFencesEveryStorageReadGroup|ExecProxyRouteUsesShardLeaderInsteadOfDefaultLeader|ExecProxyRouteTargetsRemoteShardLeader|ExecProxyRouteFailsClosedOnSplitShardLeaders|ExecVerifiesLeaderBeforeSnapshot|ExecReadFenceUsesRedisStorageKeys)' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(ExecDedup|SetDedup|List|Txn|MultiExec|RangeList|ExecProxyRoute|ExecReadFence)' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./adapter/... --timeout=5m\n- commit hook golangci-lint --config=.golangci.yaml run --fix (0 issues)\n\n@codex review

@bootjp
bootjp marked this pull request as ready for review July 29, 2026 15:11
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
adapter/redis_txn.go (2)

84-110: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

フェンスキー集合の再計算をリトライループ外へ巻き上げる余地があります。

redisQueuedCommandReadFenceKeys はキュー内コマンド数 N に対し redisTxnReadFenceKeys の 25 個の派生キーを毎回構築するため、O(25 × N × キー数) のアロケーションが発生します。これが runTransactionDirectretryRedisWrite ループ内(Line 2566)から試行ごとに呼ばれます。

lease 自体は試行ごとに必要ですが、queuedCommandReadFenceGroupKeys(queue) の結果(グループキー集合)はキューが不変である限り試行間で再利用できます。ループ外で一度算出し、ループ内では leaseRedisReadFenceGroups にその結果を渡す形が素直です。

♻️ 提案する変更(Line 2564 付近)
 	var results []redisResult
+	fenceGroupKeys := r.queuedCommandReadFenceGroupKeys(queue)
 	err := r.retryRedisWrite(dispatchCtx, func() error {
-		if err := r.leaseQueuedCommandReadGroups(dispatchCtx, queue); err != nil {
+		if err := r.leaseRedisReadFenceGroups(dispatchCtx, fenceGroupKeys); err != nil {
 			return err
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adapter/redis_txn.go` around lines 84 - 110, In runTransactionDirect, compute
the queued command fence group keys once before the retryRedisWrite loop, then
pass that cached collection to leaseRedisReadFenceGroups on every attempt. Keep
lease acquisition inside the loop, and reuse redisQueuedCommandReadFenceKeys
only while the queue remains unchanged.

200-210: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

フェンスグループの lease を並列化してください。

leaseRedisReadFenceGroups は各 Raft グループの LeaseReadForKey 読みを順次実行します。DynamoDB の実装と同様に、groupKeys > 1 の際は sync.WaitGroup + cancel 子context で並列 fanning し、最初のエラーで残りの lease を取消す形にすると、マルチキー EXEC のレイテンシがグループ数に比例して増えるのを防げます。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adapter/redis_txn.go` around lines 200 - 210, Update
RedisServer.leaseRedisReadFenceGroups to lease multiple groupKeys concurrently
when more than one key is provided, using a sync.WaitGroup and cancellable child
context. Have workers stop or cancel remaining leases after the first error,
safely retain and return the first error, while preserving the existing
nil-server/coordinator and single-key behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@adapter/redis_txn.go`:
- Around line 128-161: Update readFenceLeader and the readFenceProxyKey flow so
groups led by the local node contribute a non-empty self-node address or
sentinel to targetLeader before recordReadFenceTargetLeader is called. Preserve
remote leader recording and ensure EXECs mixing local and remote leader groups
are recognized as split rather than proxied as a single remote target.

---

Nitpick comments:
In `@adapter/redis_txn.go`:
- Around line 84-110: In runTransactionDirect, compute the queued command fence
group keys once before the retryRedisWrite loop, then pass that cached
collection to leaseRedisReadFenceGroups on every attempt. Keep lease acquisition
inside the loop, and reuse redisQueuedCommandReadFenceKeys only while the queue
remains unchanged.
- Around line 200-210: Update RedisServer.leaseRedisReadFenceGroups to lease
multiple groupKeys concurrently when more than one key is provided, using a
sync.WaitGroup and cancellable child context. Have workers stop or cancel
remaining leases after the first error, safely retain and return the first
error, while preserving the existing nil-server/coordinator and single-key
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4fe10946-57b0-4d00-a5b2-0e22f0a7f11d

📥 Commits

Reviewing files that changed from the base of the PR and between 8df1eba and a171f46.

📒 Files selected for processing (4)
  • adapter/redis_lists.go
  • adapter/redis_proxy_leader.go
  • adapter/redis_txn.go
  • adapter/redis_txn_test.go

Comment thread adapter/redis_txn.go
@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest read-fence routing feedback:

  • Treat locally-led read-fence groups as an explicit target so EXEC fails closed when a transaction spans local and remote shard leaders.
  • Cache direct EXEC read-fence group keys outside the retry loop while keeping lease acquisition per attempt.
  • Lease multiple Redis read-fence groups concurrently and cancel sibling leases on the first error.
  • Added regression coverage for mixed local/remote shard leaders.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(RangeListVerifiesLeaderBeforeSnapshot|RangeListFencesEveryStorageReadGroup|ExecProxyRouteUsesShardLeaderInsteadOfDefaultLeader|ExecProxyRouteTargetsRemoteShardLeader|ExecProxyRouteFailsClosedOnSplitShardLeaders|ExecProxyRouteFailsClosedOnMixedLocalAndRemoteShardLeaders|ExecVerifiesLeaderBeforeSnapshot|ExecReadFenceUsesRedisStorageKeys)' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(ExecDedup|SetDedup|List|Txn|MultiExec|RangeList|ExecProxyRoute|ExecReadFence)' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./adapter/... --timeout=5m\n- commit hook golangci-lint --config=.golangci.yaml run --fix (0 issues)\n\n@codex review

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

CI is green on the latest head.

@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: a9b26b9251

ℹ️ 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_txn.go Outdated
Comment on lines 2599 to 2603
if err := r.leaseRedisReadFenceGroups(dispatchCtx, fenceGroupKeys); err != nil {
return err
}

startTS := r.txnStartTS()

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 every shard through the selected snapshot timestamp

When an EXEC spans at least two shard groups led by the same node, this fence-before-snapshot ordering can still produce stale OCC reads: after group A's lease completes, an A write can obtain T1 and remain unapplied while group B applies T2 > T1; txnStartTS() then uses ShardStore.LastCommitTS()—the maximum across groups—as T2, so the transaction misses A's T1 value and later treats it as non-conflicting because T1 <= startTS. This preserves the lost-write/strict-serializability anomaly the fence is intended to close. Ensure every involved group is caught up through the selected startTS, such as with a timestamp-aware or second barrier, and cover this two-group interleaving.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go
Comment on lines +177 to +178
if *target != leader {
return errRedisExecSplitShardLeaders

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 Compare Redis targets rather than per-group Raft endpoints

When a request reaches a node that is not the leader of either involved group, two groups led by the same remote process still normally report different Raft endpoints because each group has its own listener, while buildLeaderRedis maps both endpoints to that process's single Redis address (main.go:1027-1029,1154-1167). Comparing the raw Raft endpoint strings here therefore returns ERR EXEC read fence spans multiple shard leaders even though one Redis target can execute and fence the transaction; the same transaction only succeeds when sent directly to the co-located leader node. Resolve and compare the Redis target or stable node identity before deciding the leaders are split, and cover this with real per-group addresses.

AGENTS.md reference: AGENTS.md:L25-L27

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go Outdated
redisTxnWideListFenceKey(userKey),
redisHashKey(userKey),
store.HashMetaKey(userKey),
store.HashFieldScanPrefix(userKey),

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 all routes intersected by collection reads

When a hash, set, zset, or stream wide-column range is split after its scan-prefix key, this representative fences only the group owning the prefix start, although ShardStore.ScanAt visits every intersecting route (kv/shard_store.go:302-308). Point reads are also missed: for example, loadHashFieldsIntoState reads HashFieldKey(key, field), which can fall in a different route than HashFieldScanPrefix(key). The proxy prepass can therefore conclude that all required groups are local and then build its transaction snapshot from an unfenced group, allowing stale reads during leadership churn. Enumerate all routes intersecting each scanned range and include command-dependent exact storage keys before selecting the proxy target or leasing groups.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest Redis read-fence P1 findings:

  • Compare read-fence targets by resolved Redis address, so co-located shard leaders with distinct Raft endpoints are not split-failed.
  • Fence involved Redis read groups before and after selecting the snapshot timestamp for EXEC and standalone LRANGE.
  • Expand collection read-fence coverage to every route intersected by wide-column prefix scans and add command-known exact field/member keys.
  • Audited Redis read-fence callers after the fail-closed/routing semantic changes.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(RangeListVerifiesLeaderBeforeSnapshot|RangeListFencesEveryStorageReadGroup|ExecProxyRouteUsesShardLeaderInsteadOfDefaultLeader|ExecProxyRouteTargetsRemoteShardLeader|ExecProxyRouteFailsClosedOnSplitShardLeaders|ExecProxyRouteFailsClosedOnMixedLocalAndRemoteShardLeaders|ExecProxyRouteAllowsDistinctRaftEndpointsWithSameRedisTarget|ExecReadFenceUsesRangeRoutesAndExactHashFields|ReadFencedTimestampLeasesBeforeAndAfterSelectingTimestamp|ExecVerifiesLeaderBeforeSnapshot|ExecReadFenceUsesRedisStorageKeys)' -count=1
  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(ExecDedup|SetDedup|List|Txn|MultiExec|RangeList|ExecProxyRoute|ExecReadFence)' -count=1
  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./kv -run 'TestShardStore(ScanAt|ReadFenceGroupKeys)|TestLeaseReadGroupKeys|TestShardedCoordinator_LeaseReadForKey|TestShardedCoordinatorLeaseReadAllGroups' -count=1
  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./adapter/... ./kv/... --timeout=5m
  • git diff --check

@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: 86cd4f0933

ℹ️ 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 s == nil || s.engine == nil {
return nil
}
routes, clampToRoutes := s.routesForForwardScan(start, end)

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 Keep range routes stable through the read fence

When a shard cutover updates the distribution engine after this call but before the Redis read, these routes can differ from the routes that the later ScanAt independently obtains via routesForForwardScan. Both fence passes reuse the already-materialized groupKeys, so a newly selected group is never leased and the snapshot can read stale state from that group. Pin the route snapshot/version through the read, or re-resolve and retry until the route set remains unchanged across the second fence and scan.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest read-fence route-stability P1:

  • Added a ShardStore read-fence route version so Redis range fences can detect distribution route changes after representative group keys are resolved.
  • Recompute Redis EXEC range-fence group keys inside retry attempts and discard the attempt if the route version changes after the second fence or before dispatch.
  • Retry standalone LRANGE when the route version changes during proxy/fence/scan, so stale scan results are not returned.
  • Added regression coverage for EXEC route-version retry and LRANGE route-version retry during scan.
  • Audited the Redis read-fence route-version callers after the retry/fail-closed semantic change.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(RangeListRetriesWhenReadFenceRouteVersionChangesDuringScan|ExecRetriesWhenReadFenceRouteVersionChanges|RangeListVerifiesLeaderBeforeSnapshot|RangeListFencesEveryStorageReadGroup|ExecReadFenceUsesRangeRoutesAndExactHashFields|ReadFencedTimestampLeasesBeforeAndAfterSelectingTimestamp|ExecDedup|Exec|MultiExec|RangeList|List|Txn)' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./kv -run 'TestShardStoreReadFenceGroupKeysForRangeIncludesIntersectingRoutes|TestShardStore(ScanAt|ReadFenceGroupKeys)' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./adapter/... ./kv/... --timeout=5m\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter ./kv -count=1\n- git diff --check\n- commit hook golangci-lint --config=.golangci.yaml run --fix (0 issues)\n\n@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: 257d4b5835

ℹ️ 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_txn.go
Comment on lines +2810 to +2811
if err := r.ensureRedisReadFenceRouteStable(routeVersion); err != nil {
return err

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 Carry the fenced route version into Dispatch

When the catalog changes after this check, txn.commit() can still spend time building deletion/stream elements and allocating a commit timestamp before dispatching. The resulting OperationGroup leaves ObservedRouteVersion at zero, and ShardedCoordinator deliberately does not auto-pin transactions with a supplied StartTS or read keys, so the FSM cannot reject a transaction that read from the old owner and is dispatched to the new owner. Carry the observed version through prepareDispatch into every OperationGroup, or otherwise make the final stability check atomic with dispatch; the dedup first-attempt path has the same gap between its check and prepareDispatch.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go
func (r *RedisServer) firstExecAttempt(dispatchCtx context.Context, queue []redcon.Command) ([]redisResult, *reusableExecTxn, error) {
startTS := r.txnStartTS()
readPin := r.pinReadTS(startTS)
routeVersion := r.redisReadFenceRouteVersion()

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 Revalidate routes before reusing a dedup attempt

With onePhaseTxnDedup enabled, this version is captured only while constructing the first attempt. If that dispatch returns a retryable error and the route catalog changes during the retry backoff, the pending != nil branch calls dispatchExecReuse directly without recomputing group keys, taking new read fences, or comparing the route version. It can therefore dispatch the old write/read set and return cached results against the new ownership; retain the version in reusableExecTxn and drop/rebuild the pending attempt when it changes.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go Outdated
Comment on lines +442 to +452
route, err := r.transactionProxyRoute(queue)
if err != nil {
writeRedisError(conn, err)
return
}
if route.defaultLeader {
r.proxyTransactionToLeader(conn, queue)
return
}
if len(route.key) > 0 {
r.proxyTransactionToLeaderForKey(conn, route.key, queue)

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 Stabilize route resolution before proxying EXEC

When transactionProxyRoute selects a remote target, the function can resolve its many range representatives across different catalog versions, but this proxy branch bypasses runTransaction, so none of the new route-version checks run. If a split or move is applied while those representatives are being collected, the mixed set can select the old Redis target and omit a newly intersecting group; that target may then execute using its still-stale catalog and return stale results or commit against the old ownership. Capture and verify the route version around proxy-route resolution before forwarding the EXEC.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest route-version P1 findings:

  • Carry the observed Redis read-fence route version into OperationGroup.ObservedRouteVersion for direct EXEC dispatch and dedup first attempts.
  • Preserve the observed route version in reusable EXEC attempts, and drop/rebuild the pending dedup attempt if the route catalog changes before reuse.
  • Retry EXEC proxy-route resolution when route representatives are resolved across a route-version change before forwarding.
  • Added regression coverage for dispatch ObservedRouteVersion, dedup pending rebuild on route change, and proxy-route retry on route change.
  • Audited the changed callers: exec/transactionProxyRoute, direct txn.commit, and dispatchExecReuse.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedisExec(DispatchCarriesReadFenceRouteVersion|DedupDropsPendingWhenReadFenceRouteVersionChanges|ProxyRouteRetriesWhenReadFenceRouteVersionChanges|RetriesWhenReadFenceRouteVersionChanges)|TestRedisRangeListRetriesWhenReadFenceRouteVersionChangesDuringScan' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedis(ExecProxyRoute|ExecReadFence|ReadFencedTimestamp|ExecDedup|Exec|MultiExec|RangeList)' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./kv -run 'TestShardStoreReadFenceGroupKeysForRangeIncludesIntersectingRoutes|TestShardStore(ScanAt|ReadFenceGroupKeys)' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./adapter/... ./kv/... --timeout=5m\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter ./kv -count=1\n- git diff --check\n- commit hook golangci-lint --config=.golangci.yaml run --fix (0 issues)\n\n@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: 09a0e4959a

ℹ️ 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_txn.go Outdated
if !v.tracked {
return 0
}
return v.version

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 Preserve tracking for catalog version zero

When the store is using the valid version-0 catalog (the state created by NewEngineWithDefaultRoute), this returns 0, which downstream code interprets as unpinned: verifyComposed1 skips its ownership checks and ensureReusableExecRouteStable skips rebuilding a pending attempt. If the first catalog transition occurs after the Redis read fence, EXEC can therefore dispatch or reuse writes derived from the version-0 routing without detecting that ownership changed. Preserve the tracked/untracked distinction through dispatch, or prevent Redis requests from being served while zero is a valid catalog version.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go Outdated
return nil, nil, err
}

prepared, err := txn.prepareDispatch()

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 Recheck the route version after preparing the write set

When the catalog changes during prepareDispatch, its delete/replacement builders can run range scans such as deleteLogicalKeyElems and deleteStreamWideColumnElems against groups that were not covered by the earlier fence. There is no stability check after these scans, and the FSM's composed-route check examines only emitted mutations, so a stale scan that omits moved rows can still commit a partial collection deletion. Recheck routeVersion after preparation and before dispatch; the direct txn.commit path has the same ordering and needs the same protection.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go Outdated
Comment on lines +2897 to +2898
if err := r.ensureReusableExecRouteStable(pending); err != nil {
return nil, true, err

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 Do not discard an ambiguous EXEC attempt on route changes

When the first dispatch returns a retryable ambiguous error, pending exists specifically because that attempt may already have committed and must be probed through PrevCommitTS. If the route catalog changes before reuse, this early return sets drop=true, causing the retry loop to clear pending and rebuild the transaction without first determining whether the original commit landed. A committed non-idempotent command such as INCR can then execute a second time; if the old attempt cannot safely be probed after the route change, return the ambiguity instead of replaying it.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go Outdated
Comment on lines 2843 to 2844
if err := txn.commit(routeVersion.observedRouteVersion()); err != nil {
return err

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 Retry composed-route rejections by rebuilding EXEC

If the catalog changes after the last adapter-side stability check but before the Raft apply, the newly supplied ObservedRouteVersion makes the FSM return ErrComposed1Violation or ErrComposed1VersionGCd. ShardedCoordinator deliberately surfaces those errors for read-write transactions so the adapter can rerun the reads, but retryRedisWrite only recognizes write conflicts and transaction locks, so this path returns the routing error to the Redis client instead of rebuilding EXEC. Classify the composed-route sentinels as retryable at the outer EXEC level while ensuring any pending ambiguous attempt retains its dedup protection.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest HEAD review findings in cf73c96.

Changes:

  • Preserve tracked catalog version 0 by encoding it separately from the unpinned ObservedRouteVersion=0 sentinel.
  • Recheck Redis EXEC read-fence route versions after prepare scans and before dispatch.
  • Fail closed when an ambiguous EXEC pending attempt cannot be safely probed after a route-version change.
  • Retry Composed-1 route rejections at the outer Redis EXEC loop while keeping dedup pending reuse limited to reusable ambiguous errors.
  • Audited retry/dedup callers after the retry predicate semantic change; pending creation/update now uses isReusableRedisTxnErr.

Validation:

  • go test ./adapter ./kv -run 'TestRedisExec(DispatchCarries(ReadFenceRouteVersion|EncodedReadFenceRouteVersionZero)|DedupFailsClosedWhenReadFenceRouteVersionChangesAfterAmbiguousAttempt|DedupRetriesWhenReadFenceRouteVersionChangesDuringPrepare|RetriesComposedRouteRejectionByRebuilding)|TestRedisTxnCommitRechecksReadFenceRouteVersionAfterPrepare|TestRetryRedisWriteRetriesWireComposedRouteErrors|TestVerifyComposed1_EncodedObservedVersionZeroRunsGate' -count=1\n- golangci-lint --config=.golangci.yaml run ./adapter/... ./kv/... --timeout=5m\n- go test ./adapter ./kv -count=1 -timeout=20m\n- git diff --check\n\n@codex review

@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 (cf73c96):

  • kv/fsm.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.goNext() 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.goRunHLCLeaseRenewal, 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

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@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 (b36da7a):

  • kv/fsm.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.goNext() 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.goRunHLCLeaseRenewal, 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 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest read-fence review findings:

  • Added GET/EXISTS type-detection range fences for the list/hash/set/zset scans used by staged key type checks, while leaving list claim, zset score, and stream-entry scan routes out of that command-specific footprint.
  • Promoted decoded RESP terminal EXEC route-change errors by exact server error text, not only local error identity.
  • Routed list delta/claim scan representatives through their storage prefixes and normalized list delta/claim keys back to the literal Redis user key, including user keys that look like internal namespaces.
  • Parsed forwarded key: ...: write conflict / txn locked envelopes before composed-route sentinel matching, and tightened composed-route wire matching to the known envelope/suffix.
  • Gated version-zero observed-route wire encoding off until all Raft members advertise support, while preserving decode compatibility for the sentinel.

Caller audit after semantic changes:

  • parseWireRedisTxnErr: used by retry classification, wire error kind, and normalization; new tests cover wire conflict/lock keys containing composed-route sentinel text and composed-route retry preservation.
  • routeKey / list routing: used by ShardStore, ShardedCoordinator, backup scan, router, and FSM ownership checks; new tests cover list delta/claim full keys and scan prefixes with !sqs|... and !redis|... Redis user keys.
  • EncodeObservedRouteVersion: used by Redis read-fence route versions and ShardedCoordinator auto-pin/retry; new tests verify version 0 remains unpinned on the wire and non-zero versions still pin.

Validation:

  • go test ./adapter ./kv ./store -run 'TestRedisExecReadFenceUsesOnlyCommandRelevantRanges|TestRedisExecDispatchLeavesReadFenceRouteVersionZeroUnpinnedUntilCapabilityGate|TestHandleProxyTxnTerminalExecError|TestHandleProxyTxnCommandError|TestRetryRedisWriteRetriesWireComposedRouteErrors|TestRetryRedisWriteDoesNotTreatComposedSentinelInWireConflictKeyAsRouteError|TestNormalizeRetryableRedisTxnErrPrefersWireTxnKeyEnvelope|TestRouteKey_NormalizesRedisListDeltaAndClaimKeys|TestShardStoreReadFenceGroupKeysForListRangeUsesStorageRepresentative|TestEncodeObservedRouteVersionZeroGatedForRollingUpgrade|TestEncodeObservedRouteVersionNonZeroPassesThrough|TestVerifyComposed1_EncodedObservedVersionZeroRunsGate' -count=1
  • golangci-lint --config=.golangci.yaml run ./adapter/... ./kv/... ./store/... --timeout=5m
  • go test ./adapter ./kv ./store -count=1
  • git diff --check HEAD~1..HEAD

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (3)
store/list_helpers.go (1)

178-180: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

長さ比較をuint64へ広げ、//nolintを削除してください。

uint32(len(trimmed))は4GiB超で切り詰められ、厳密な長さ検証が成立しなくなります。

修正案
-	if uint32(len(trimmed)) != uint32(wideColKeyLenSize)+ukLen { //nolint:gosec // len is bounded by max slice size
+	if uint64(len(trimmed)) != uint64(wideColKeyLenSize)+uint64(ukLen) {

As per coding guidelines, “Avoid using //nolint directives; refactor instead in Go code.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/list_helpers.go` around lines 178 - 180, In the length validation
around ukLen, widen both sides of the comparison to uint64 so lengths above 4
GiB are checked without truncation, and remove the //nolint:gosec directive.
Preserve the existing nil return for mismatched lengths.

Sources: Coding guidelines, Linters/SAST tools

adapter/redis_txn_test.go (1)

694-733: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

選択されたfence/route keyを完全一致で検証してください。

現在の緩いassertionでは、不要なgroupの追加やroute変更後も古いrouteを返す退行を検出できません。

  • adapter/redis_txn_test.go#L694-L733: 各コマンドについて、必須の基底キーを含む完全な期待集合をrequire.ElementsMatchで比較してください。
  • adapter/redis_txn_test.go#L1075-L1080: route.keyが非空だけでなく、更新後のrouteBと一致することを検証してください。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@adapter/redis_txn_test.go` around lines 694 - 733, Strengthen the assertions
in adapter/redis_txn_test.go:694-733 by replacing the per-route
Contains/NotContains checks for each queued command with require.ElementsMatch
comparisons against the complete expected fence/route-key sets, including all
required base keys and excluding extras. In adapter/redis_txn_test.go:1075-1080,
update the route.key assertion to verify equality with the updated routeB value
rather than only checking that it is non-empty.
kv/transcoder_test.go (1)

9-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

ゲート有効時のpinned-zeroエンコードも検証してください。

現在は無効時とsentinelの直接decodeのみで、EncodeObservedRouteVersion(0)がsentinelを返す分岐を通りません。グローバルゲートを復元する非parallelテストを追加してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kv/transcoder_test.go` around lines 9 - 20, Extend
TestEncodeObservedRouteVersionZeroGatedForRollingUpgrade to cover the
enabled-gate path: temporarily enable the global gate used by
EncodeObservedRouteVersion, restore its original value with cleanup, and mark
this test non-parallel. Assert that EncodeObservedRouteVersion(0) returns
ObservedRouteVersionZero, while preserving the existing disabled and
direct-decode assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@adapter/redis_txn_test.go`:
- Around line 694-733: Strengthen the assertions in
adapter/redis_txn_test.go:694-733 by replacing the per-route
Contains/NotContains checks for each queued command with require.ElementsMatch
comparisons against the complete expected fence/route-key sets, including all
required base keys and excluding extras. In adapter/redis_txn_test.go:1075-1080,
update the route.key assertion to verify equality with the updated routeB value
rather than only checking that it is non-empty.

In `@kv/transcoder_test.go`:
- Around line 9-20: Extend
TestEncodeObservedRouteVersionZeroGatedForRollingUpgrade to cover the
enabled-gate path: temporarily enable the global gate used by
EncodeObservedRouteVersion, restore its original value with cleanup, and mark
this test non-parallel. Assert that EncodeObservedRouteVersion(0) returns
ObservedRouteVersionZero, while preserving the existing disabled and
direct-decode assertions.

In `@store/list_helpers.go`:
- Around line 178-180: In the length validation around ukLen, widen both sides
of the comparison to uint64 so lengths above 4 GiB are checked without
truncation, and remove the //nolint:gosec directive. Preserve the existing nil
return for mismatched lengths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 094deaf2-12ac-4852-9384-29ade70a8e84

📥 Commits

Reviewing files that changed from the base of the PR and between cf73c96 and b36da7a.

📒 Files selected for processing (14)
  • adapter/redis_error_prefix_test.go
  • adapter/redis_lists.go
  • adapter/redis_proxy_leader.go
  • adapter/redis_retry.go
  • adapter/redis_retry_test.go
  • adapter/redis_txn.go
  • adapter/redis_txn_test.go
  • kv/shard_key.go
  • kv/shard_key_test.go
  • kv/shard_store.go
  • kv/shard_store_test.go
  • kv/transcoder.go
  • kv/transcoder_test.go
  • store/list_helpers.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • kv/transcoder.go
  • kv/shard_store_test.go
  • kv/shard_store.go
  • adapter/redis_lists.go
  • adapter/redis_txn.go

@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: b36da7a679

ℹ️ 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_txn.go Outdated
Comment on lines +286 to +291
case cmdHSet, cmdHMSet:
return redisHashReadFenceRanges(userKey)
case cmdRPush, cmdLRange:
return redisListReadFenceRanges(userKey)
case cmdZIncrBy:
return redisZSetReadFenceRanges(userKey)

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 Include type-detection ranges for every staged type check

HSET/HMSET, RPUSH/LRANGE, and ZINCRBY all call stagedKeyType, which falls through to the full keyTypeAt scan across the list/hash/set/zset families, but these cases fence only the command's expected collection ranges. When the key actually has a different collection type whose scan prefix intersects another shard, that shard is omitted from the proxy decision and snapshot fence, so the command can miss the existing type and execute instead of returning WRONGTYPE (or LRANGE can return an empty result). Union redisTypeDetectionReadFenceRanges into each of these command footprints before adding its command-specific ranges.

Useful? React with 👍 / 👎.

Comment thread kv/transcoder.go Outdated
Comment on lines +23 to +27
// observedRouteVersionZeroWireEncodingEnabled stays disabled until every Raft
// member advertises support for ObservedRouteVersionZero. Mixed-version groups
// must keep literal zero on the wire so old followers do not treat the sentinel
// as an impossibly new catalog version and diverge from a new leader.
const observedRouteVersionZeroWireEncodingEnabled = false

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 Keep version-zero reads pinned at Raft apply

On a fresh cluster whose valid catalog version is 0, this hard-coded false gate makes EncodeObservedRouteVersion(0) emit the unpinned wire value, so verifyComposed1 skips ownership validation. If the first catalog transition occurs after the adapter's final stability check but before dispatch/apply, a transaction derived from the version-0 owner can therefore commit on the new owner without rejection. Fresh evidence since the prior finding is that the new capability gate is permanently disabled and no member-advertisement path can enable it; retain a tracked/untracked distinction without emitting an old-node-unsafe sentinel, or implement the advertised capability negotiation.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_proxy_leader.go Outdated
Comment on lines +189 to +192
func isRedisExecTerminalProxyError(err error) bool {
return err != nil &&
(errors.Is(err, errRedisExecRouteChangedAfterAmbiguousAttempt) ||
err.Error() == errRedisExecRouteChangedAfterAmbiguousAttempt.Error())

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 Promote split-leader EXEC errors across proxies

If leadership changes after one node chooses a common remote Redis target but before that target resolves its own read-fence groups, the target can return errRedisExecSplitShardLeaders as a top-level EXEC error. go-redis surfaces that decoded error on the pipeline and queued commands, but this predicate recognizes only the ambiguous-route-change text, so the proxy falls through to writeProxyCmdsResult and turns a transaction-level failure into an EXEC result array of command errors. Match the exact split-leader error here as another terminal proxy error.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go Outdated
Comment on lines +116 to +119
func (r *RedisServer) redisTxnReadFenceKeysForRanges(userKey []byte, ranges []redisReadFenceRange) [][]byte {
keys := append([][]byte{}, redisTxnReadFenceKeys(userKey)...)
for _, readRange := range ranges {
keys = append(keys, r.redisReadFenceRangeGroupKeys(readRange.start, readRange.end)...)

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 Remove unused collection representatives from command fences

Fresh evidence after the command-specific range change is that every command still starts from the full redisTxnReadFenceKeys set here, which includes unused raw scan-prefix representatives such as ZSetScoreScanPrefix and StreamEntryScanPrefix. Those wide-column prefixes are routed by their raw bytes rather than normalized to the Redis user key, so in a split catalog they can resolve to unrelated groups even though GET/EXISTS never scan those ranges; MULTI; GET k; EXEC can therefore still fail with errRedisExecSplitShardLeaders solely because an unused collection prefix is led elsewhere. Build the point representatives per command as well as the range list, or omit scan-prefix point keys already covered by range expansion.

Useful? React with 👍 / 👎.

Comment thread kv/shard_key.go
Comment on lines +91 to +92
if store.IsListMetaDeltaKey(key) {
if user := store.ExtractListUserKeyFromDelta(key); user != nil {

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 Reject malformed list routing keys before decoding

In a sharded deployment, an external Redis command can supply an arbitrary binary key beginning with !lst|meta|d|; routing that key now calls ExtractListUserKeyFromDelta, whose uint32 length calculation can wrap before slicing. For example, a short key with an encoded user length of 0xffffffff passes the wrapped bounds check and then evaluates a slice with start 4 and wrapped end 3, panicking during IsLeaderForKey/route resolution before any storage access. The claim decoder added below has the same arithmetic shape. Validate the encoded length without overflowing (for example against len(trimmed)-fixedSuffix) before invoking these extractors on untrusted routing keys.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest nitpick review on the new head:

  • Widened list scan-prefix length validation to uint64 and removed the //nolint:gosec directive.
  • Strengthened command-specific Redis read-fence tests with exact require.ElementsMatch expectations for GET, EXISTS, LRANGE, and HSET.
  • Tightened the proxy route-version retry test by checking the selected proxy storage key exactly and separately asserting the re-resolved range representative is routeB.
  • Added enabled-gate coverage for version-zero observed-route encoding while keeping the disabled rolling-upgrade path as the production default.

Validation:

  • go test ./adapter ./kv ./store -run 'TestRedisExecReadFenceUsesOnlyCommandRelevantRanges|TestRedisExecProxyRouteRetriesWhenReadFenceRouteVersionChanges|TestEncodeObservedRouteVersionZeroGatedForRollingUpgrade|TestEncodeObservedRouteVersionNonZeroPassesThrough' -count=1
  • golangci-lint --config=.golangci.yaml run ./adapter/... ./kv/... ./store/... --timeout=5m
  • go test ./adapter ./kv ./store -count=1
  • git diff --check HEAD~1..HEAD

@codex review

@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 (94fb695):

  • kv/fsm.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.goNext() 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.goRunHLCLeaseRenewal, 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

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@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: 94fb695e56

ℹ️ 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_key.go

func listRouteKey(key []byte) []byte {
if store.IsListMetaDeltaKey(key) {
if user := store.ExtractListUserKeyFromDelta(key); user != nil {

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 Validate full list keys before routing them

When a standalone list command supplies a binary key beginning with !lst|meta|d| or !lst|clm|, listPushCmd passes it directly to IsLeaderForKey, so this newly added routing path invokes the full-key extractor on untrusted bytes. Fresh evidence on this head is that only the new scan-prefix helper was widened to uint64; ExtractListUserKeyFromDelta and ExtractListUserKeyFromClaim still perform overflowing uint32 additions, so an encoded length of 0xffffffff can pass their bounds check and panic on a slice whose end wrapped below its start. Validate the full delta/claim extractors without overflow before calling them here.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest Redis read-fence review findings:

  • Unioned type-detection scan ranges into HSET/HMSET, RPUSH/LRANGE, and ZINCRBY command fences.
  • Removed unused raw scan-prefix representatives from command point fences while preserving command-range fallback when range expansion is unavailable.
  • Promoted split-shard-leader EXEC failures as top-level proxy transaction errors.
  • Hardened list delta/claim user-key extraction against malformed length prefixes and added regression coverage.
  • Audited callers of the list full-key extractors and proxy terminal-error classification after the semantic changes.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedisRangeListFencesEveryStorageReadGroup|TestRedisExecReadFenceUsesOnlyCommandRelevantRanges|TestHandleProxyTxnTerminalExecError|TestHandleProxyTxnCommandError' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./store -run 'TestExtractListUserKeyFromFullWideColumnKeys|TestExtractListUserKeyRejectsMalformedFullKeyLength' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./adapter/... ./kv/... ./store/... --timeout=5m\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./kv ./store -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -count=1\n- git diff --check\n\n@codex review

@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 (d2951fb):

  • kv/fsm.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.goNext() 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.goRunHLCLeaseRenewal, 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

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
store/list_helpers_test.go (1)

18-52: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

scan prefix の回帰テストを追加してください。

store/list_helpers.go で追加されたscan prefix抽出ヘルパは、kv/shard_key.go のルーティングfallbackで使用されますが、このテストではfull keyしか検証していません。正しいprefix、別prefix、切り詰めた長さ、余分な末尾バイトをtable-driven caseとして追加し、誤ったroute key抽出を防いでください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/list_helpers_test.go` around lines 18 - 52, Add table-driven regression
cases for the scan-prefix extraction helpers in store/list_helpers.go, covering
the expected prefix, a different prefix, truncated input, and trailing extra
bytes. Extend the existing TestExtractListUserKeyRejectsMalformedFullKeyLength
or add a focused test using the relevant extraction symbols, asserting only the
valid prefix is accepted and malformed route keys return nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@store/list_helpers_test.go`:
- Around line 18-52: Add table-driven regression cases for the scan-prefix
extraction helpers in store/list_helpers.go, covering the expected prefix, a
different prefix, truncated input, and trailing extra bytes. Extend the existing
TestExtractListUserKeyRejectsMalformedFullKeyLength or add a focused test using
the relevant extraction symbols, asserting only the valid prefix is accepted and
malformed route keys return nil.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab219538-0047-4b68-90f4-b7a679df2b77

📥 Commits

Reviewing files that changed from the base of the PR and between b36da7a and d2951fb.

📒 Files selected for processing (8)
  • adapter/redis_error_prefix_test.go
  • adapter/redis_proxy_leader.go
  • adapter/redis_txn.go
  • adapter/redis_txn_test.go
  • kv/transcoder.go
  • kv/transcoder_test.go
  • store/list_helpers.go
  • store/list_helpers_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • kv/transcoder_test.go
  • kv/transcoder.go
  • adapter/redis_proxy_leader.go
  • adapter/redis_error_prefix_test.go
  • adapter/redis_txn.go
  • adapter/redis_txn_test.go

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest test coverage nitpick:

  • Added table-driven coverage for list delta/claim scan-prefix extraction helpers.
  • Covered valid prefixes, different prefixes, truncated inputs, and trailing extra bytes.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./store -run 'TestExtractListUserKeyFromFullWideColumnKeys|TestExtractListUserKeyRejectsMalformedFullKeyLength|TestExtractListUserKeyFromScanPrefixes' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./store -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./store/... --timeout=5m\n- git diff --check\n\n@codex review

@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 (a5a5520):

  • kv/fsm.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.goNext() 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.goRunHLCLeaseRenewal, 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

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@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: a5a5520a18

ℹ️ 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_key.go
Comment on lines +91 to +93
if store.IsListMetaDeltaKey(key) {
if user := store.ExtractListUserKeyFromDelta(key); user != nil {
return user

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 Do not decode unwrapped Redis keys as list storage keys

When a legal Redis user key is itself a well-formed delta key, such as the bytes returned by store.ListMetaDeltaKey([]byte("other"), 1, 1), direct routing calls like IsLeaderForKey(key) now resolve it to other. However, the Redis/list storage rows created for that user key are wrapped in another internal prefix and route to the literal full user key, so the initial command can be proxied to an unrelated shard leader and perform its snapshot reads without fencing the shard that owns its data. The overflow fix does not address these valid encodings; claim-shaped user keys have the same mismatch, so list decoding must be limited to known storage keys or callers must route using canonical wrapped keys.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest P1 routing finding:

  • Added a Redis user-key route representative so literal Redis keys are not recursively decoded when their bytes look like list storage keys.
  • Routed raw Redis leader/proxy/lease checks through that representative for SET/GET/DEL/EXISTS, list push proxying, Redis leader clients, and background compaction/migration leadership checks.
  • Kept storage-key and read-fence group-key routing unchanged for actual list internal rows and scan representatives.
  • Audited coordinator routing callers after the semantic change.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -run 'TestRedisUserRouteKeyPreservesListStorageShapedUserKey|TestProxyToLeaderRoutesRawRedisKeyThroughLiteralWrapper|TestLeaderAwareGetAtRoutesRedisInternalKeyThroughLiteralWrapper' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./adapter -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./adapter/... --timeout=5m\n- git diff --check\n\n@codex review

@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 (4ed6cae):

  • kv/fsm.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.goNext() 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.goRunHLCLeaseRenewal, 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

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@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: 4ed6cae21b

ℹ️ 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 on lines +256 to +257
func redisUserRouteKey(userKey []byte) []byte {
return append([]byte(redisRoutePrefix), userKey...)

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 empty Redis keys in the route wrapper

For a legal zero-length Redis key, this produces !redis|route|, but kv.redisRouteKey rejects keys with no bytes after the final separator, so the representative routes by the wrapper bytes rather than by the empty logical key. Empty-key storage representatives such as !redis|str| are likewise left as distinct raw prefixes; in a sharded catalog where those prefixes belong to different groups, even MULTI; GET ""; EXEC can appear to span multiple leaders and fail with ERR EXEC read fence spans multiple shard leaders, while standalone proxy checks can select a group different from the stored row. Encode empty keys unambiguously or make routing distinguish a successfully decoded empty key from decode failure.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Addressed the empty Redis key routing follow-up:

  • Made kv.redisRouteKey treat an empty decoded Redis user key as a successful decode instead of falling back to wrapper bytes.
  • Added route-key coverage for !redis|str| and !redis|route|.
  • Added empty-key coverage to the Redis user-route helper regression test.
  • Re-audited route-key and Redis coordinator-routing callers after the semantic change.

Validation:

  • GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./kv ./adapter -run 'TestRouteKey_NormalizesRedisInternalEmptyUserKey|TestRedisUserRouteKeyPreservesListStorageShapedUserKey|TestProxyToLeaderRoutesRawRedisKeyThroughLiteralWrapper|TestLeaderAwareGetAtRoutesRedisInternalKeyThroughLiteralWrapper' -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp go test ./kv -count=1\n- GOCACHE=$(pwd)/.cache GOTMPDIR=$(pwd)/.cache/tmp GOLANGCI_LINT_CACHE=$(pwd)/.golangci-cache golangci-lint --config=.golangci.yaml run ./adapter/... ./kv/... --timeout=5m\n- git diff --check\n\n@codex review

@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 (ae80151):

  • kv/fsm.go
  • kv/sharded_coordinator.go

What to check, by subsystem:

  • kv/hlc*.goNext() 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.goRunHLCLeaseRenewal, 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

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: ae80151ac6

ℹ️ 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".

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.

1 participant