Skip to content

Add live backup pin FSM substrate - #1056

Open
bootjp wants to merge 13 commits into
mainfrom
design/live-backup-pin-substrate
Open

Add live backup pin FSM substrate#1056
bootjp wants to merge 13 commits into
mainfrom
design/live-backup-pin-substrate

Conversation

@bootjp

@bootjpbootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a live-backup pin/extend/release FSM envelope for retaining read timestamps during future online logical backup scans
  • extend ActiveTimestampTracker with deadline-based backup pins, expiry sweeping, limits, and idempotent release/extend behavior
  • wire shard FSMs to the same ActiveTimestampTracker used by local compaction

Tests

  • go test ./kv -run 'Test(ActiveTimestampTracker|Backup|ApplyBackup)' -count=1 -timeout=240s
  • go test ./kv -count=1 -timeout=300s
  • go test . -run 'TestBuildShardGroupsWithEtcdEngineRoutesAcrossGroups|TestBuildShardGroupsWithEtcdEngineRestartsAcrossGroups' -count=1 -timeout=240s
  • go test . -run 'TestRaftBootstrapMembers_E2E|TestRaftBootstrapMembers_MultiGroup' -count=1 -timeout=300s
  • go test ./... -run TestNonexistent -count=0 -timeout=300s
  • go test . -count=1 -timeout=300s
  • golangci-lint run ./kv . --timeout=5m
  • git diff --check
  • git verify-commit HEAD

Author: bootjp

Summary by CodeRabbit

  • 新機能
    • 期限付きバックアップピンの登録・延長・解放に対応し、期限切れを自動回収できるようになりました(上限数・スイープ間隔などを設定可能)。
    • バックアップピンを複数Raftグループ単位で管理し、最古の参照時刻計算にも反映されます。
    • 管理APIでライブバックアップ(開始/更新/終了、スコープ一覧、ストリーミング)とノードバージョン取得を提供します。
  • 改善
    • 無効なピンや期限切れ操作、不整合を安全に検出して失敗します。バックアップ中はスナップショット生成をブロックします。

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@bootjp, you've reached your PR review limit, so we couldn't start this review.

Next review available in:5 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 641ef022-3c2c-4398-bbb1-677a81e3aa52

📥 Commits

Reviewing files that changed from the base of the PR and between 1eddbb1 and 9609b81.

📒 Files selected for processing (6)
  • adapter/admin_backup.go
  • adapter/admin_backup_test.go
  • internal/raftengine/etcd/cold_start_volatile_replay_test.go
  • kv/backup_scan.go
  • kv/fsm_backup_test.go
  • kv/shard_store_test.go
📝 Walkthrough

Walkthrough

ActiveTimestampTracker、バックアップFSM、固定長ワイヤ、AdminバックアップRPC、ルートスナップショット走査、タイムスタンプフロア、リーダー転送を追加し、起動配線と関連インターフェースを更新しました。

Changes

ライブ論理バックアップ

Layer / File(s)Summary
バックアップピンとワイヤ形式
kv/active_timestamp_tracker.go, kv/backup_codec.go, kv/*_test.go
期限、上限、グループスコープ、期限切れ回収、スイーパー、固定長Pin/Extend/Release/Reserve/Unreserveワイヤを追加しました。
FSM適用とタイムスタンプフロア
kv/fsm.go, kv/fsm_backup.go, kv/fsm_backup_test.go
バックアップ操作をFSMからトラッカーへ適用し、フロア永続化、HLC観測、遅延書き込みフェンス、アクティブピン中のスナップショット拒否を追加しました。
バックアップ走査と出力デコード
kv/backup_scan.go, internal/backup/*, distribution/engine.go, kv/shard_store.go
ルートスナップショット、ページング、所有権解決、スコープ分類、Redis/SQS出力デコードを追加しました。
全グループフェンスと転送
kv/coordinator.go, kv/sharded_coordinator.go, kv/leader_*, adapter/internal.go
全グループのリースTS取得、タイムスタンプフロア観測、管理提案とリース読み取りのリーダー転送を追加しました。
AdminバックアップAPI
adapter/admin_backup.go, adapter/admin_grpc.go, main.go
Begin/Renew/End、スコープ一覧、ストリーミング、署名トークン、ピン・予約fanout、セッション更新、補償処理を追加しました。
プロトコルと起動配線
proto/*.proto, main.go, internal/raftengine/*, main_*_test.go
バックアップRPCと内部転送RPCを定義し、共有トラッカー、設定検証、Admin依存、スナップショット周期契約を配線しました。
コンパクション互換性
kv/compactor.go, kv/txn_keys.go, kv/*_test.go
グループ別バックアップピンをコンパクション境界へ反映し、トランザクション内部キー判定とテスト用エンジン契約を更新しました。

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

Sequence Diagram(s)

sequenceDiagram
participant BackupClient
participant AdminServer
participant ShardedCoordinator
participant kvFSM
participant ActiveTimestampTracker
participant BackupScanner
BackupClient->>AdminServer: BeginBackup
AdminServer->>ShardedCoordinator: LeaseReadAllGroupsTimestamp
AdminServer->>kvFSM: reserve/pin proposals
kvFSM->>ActiveTimestampTracker: apply backup pin
AdminServer->>BackupScanner: capture snapshot and scan at read timestamp
BackupScanner-->>AdminServer: scoped backup records
AdminServer-->>BackupClient: StreamBackup responses
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 7.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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedライブバックアップ用のピン/延長/解放を支えるFSM基盤の追加を適切に要約しており、変更内容と整合しています。
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch design/live-backup-pin-substrate

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

Copy link
Copy Markdown
OwnerAuthor

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

  • kv/fsm.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.

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a backup pinning mechanism to the ActiveTimestampTracker and kvFSM to retain MVCC versions at live-backup read timestamps during background compaction. It adds FSM commands for pinning, extending, and releasing backup pins, alongside a background sweeper to reap expired pins. The reviewer provided critical feedback to improve robustness: first, expired backup pins should be ignored in Oldest() to avoid blocking compaction before the sweeper runs; second, validation and limit errors must not halt the FSM to prevent DoS vulnerabilities; and third, a graceful shutdown mechanism (Close() and stopCh) should be added to the tracker to prevent goroutine leaks from the background sweeper.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment threadkv/active_timestamp_tracker.go Outdated
Comment threadkv/fsm_backup.go
Comment threadkv/active_timestamp_tracker.go
Comment threadkv/active_timestamp_tracker.go
Comment threadkv/active_timestamp_tracker.go
@bootjp
bootjpforce-pushed the design/live-backup-pin-substrate branch from 9a7491c to b9e3e10CompareJuly 10, 2026 19:47
@bootjp

Copy link
Copy Markdown
OwnerAuthor

Addressed latest-head review findings:

  • expired backup pins are ignored by Oldest() before the sweeper runs
  • invalid backup pins and active-backup limit failures now return non-fatal apply errors instead of halting the FSM
  • ActiveTimestampTracker now has an idempotent Close() path for the backup-pin sweeper

Validation:

  • go test ./kv -run 'Test(ActiveTimestampTracker|ApplyBackup|BackupPayload)' -count=1 -timeout=240s\n- go test ./kv -count=1 -timeout=300s\n- golangci-lint run ./kv --timeout=5m\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 (b9e3e10):

  • kv/fsm.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-connectorchatgpt-codex-connectorBot 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:b9e3e104a4

ℹ️ 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 threadkv/active_timestamp_tracker.go Outdated
Comment on lines +149 to +150
if _, exists := t.backupPins[pinID]; !exists && len(t.backupPins) >= t.maxBackupPins {
return errors.WithStack(ErrTooManyActiveBackups)

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 Reap expired pins before enforcing the active limit

When a replica still has maxBackupPins expired entries that Oldest() already ignores but the sweeper has not deleted yet, this raw len(t.backupPins) check returns ErrTooManyActiveBackups for a new valid backup pin. Since applyBackup treats that error as non-fatal, the Raft entry is advanced without recording the pin on that replica; if it later compacts or becomes the backup-serving leader, the backup read timestamp is not retained. Reap or exclude expired pins before applying the cap.

Useful? React with 👍 / 👎.

Comment threadkv/active_timestamp_tracker.go Outdated
Comment on lines +170 to +171
pin.deadline = deadline
t.backupPins[pinID] = pin

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 late renewals for expired backup pins

If a BackupExtend arrives after the previous deadline but before the sweeper has deleted the entry, Oldest() has already stopped honoring this pin, so compaction may have advanced past the backup's read timestamp during that gap. This assignment makes the expired pin active again and reports a successful renewal, allowing a backup to continue even though its retention fence was temporarily absent. Treat expired pins as missing/invalid before extending them.

Useful? React with 👍 / 👎.

Comment threadkv/backup_codec.go
Comment threadmain.go
@bootjp
bootjpforce-pushed the design/live-backup-pin-substrate branch from b9e3e10 to 91a7d04CompareJuly 10, 2026 20:01
@bootjp

Copy link
Copy Markdown
OwnerAuthor

Addressed the latest-head findings:

  • backup pin capacity checks now reap expired pins before enforcing the limit
  • expired or missing backup renewals now return ErrInvalidBackupPin instead of reactivating a stale retention fence
  • deadline_ms=0 decodes to time.Time{} so apply validation rejects it as invalid
  • backup pin tracker entries are scoped by Raft group, so one group's Release cannot remove another group's pin for the same pin_id

Validation:

  • go test ./kv -run 'Test(ActiveTimestampTracker|BackupCodec|ApplyBackup|BackupPayload)' -count=1 -timeout=240s\n- go test ./kv -count=1 -timeout=300s\n- golangci-lint run ./kv --timeout=5m\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 (91a7d04):

  • kv/fsm.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-connectorchatgpt-codex-connectorBot 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:91a7d04265

ℹ️ 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 threadkv/fsm_backup.go Outdated
Comment on lines +27 to +28
if errors.Is(err, ErrTooManyActiveBackups) || errors.Is(err, ErrInvalidBackupPin) {
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 Do not drop committed backup pins as proposal errors

When this branch is reached for the local-state-dependent errors (capacity, missing/expired renewals), applyBackup returns a plain error instead of haltErr, so the Raft engine treats the entry as applied and only the proposer observes the response. In a multi-replica group, one replica can hit this branch because its tracker still has an unexpired pin or its clock has already expired a renewal while the leader installed the new/extended fence; that replica then advances without retaining read_ts, and after a leader change or local compaction the backup can lose the version it was meant to protect.

Useful? React with 👍 / 👎.

Comment threadkv/active_timestamp_tracker.go Outdated
t.mu.Lock()
expired := t.reapExpiredBackupPinsLocked(time.Now())
key := newBackupPinKey(pinID, groupID)
if _, exists := t.backupPins[key]; !exists && len(t.backupPins) >= t.maxBackupPins {

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 Count backup capacity by pin ID, not group entry

Now that backupPins is keyed by (pin_id, groupID) and every shard FSM shares this tracker, len(t.backupPins) charges one slot per Raft group. A single logical backup fan-out with the same pin_id across more than 64 groups will fill the default limit and the next group’s BackupPin returns ErrTooManyActiveBackups, so large sharded deployments cannot start even one backup unless the limit is raised by group count.

Useful? React with 👍 / 👎.

@bootjp
bootjpforce-pushed the design/live-backup-pin-substrate branch from 91a7d04 to b04b7fbCompareJuly 10, 2026 20:19
@bootjp

Copy link
Copy Markdown
OwnerAuthor

Addressed latest-head review findings:

  • committed backup Pin entries now use an apply path that does not enforce the local active-backup cap, so a replica does not advance without installing the retention fence
  • committed backup Extend entries treat missing/expired local pins as no-op apply results instead of proposal errors, while direct Extend still rejects missing/expired renewals
  • the active-backup limit now counts logical pin IDs rather than per-group tracker entries, so one backup fan-out across groups consumes one capacity slot

Validation:

  • go test ./kv -run 'Test(ActiveTimestampTracker|BackupCodec|ApplyBackup|BackupPayload)' -count=1 -timeout=240s
  • go test ./kv -count=1 -timeout=300s
  • golangci-lint run ./kv --timeout=5m
  • git diff --check
  • git verify-commit 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 (b04b7fb):

  • kv/fsm.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. Nice work!

Reviewed commit:b04b7fb235

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
main.go (1)

389-416: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

readTracker.Close() を shutdown cleanup に追加してください。ActiveTimestampTracker はスイーパー goroutine を持つため、cleanup.Add(readTracker.Close) で終了時に止める必要があります。

🤖 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 `@main.go` around lines 389 - 416, Register readTracker.Close with the shutdown
cleanup after creating the ActiveTimestampTracker, using
cleanup.Add(readTracker.Close), so its sweeper goroutine is stopped during
termination.
🧹 Nitpick comments (1)
kv/fsm.go (1)

372-373: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

applyBackup(data)applyHLCLease(data[1:]) でオペコードバイトの扱いが一貫していません。

applyHLCLeasedata[1:](オペコード除外)を渡すのに対し、applyBackupdata(オペコード含む)をそのまま渡しています。decodeBackupEntry がフルデータを期待しているため機能上は問題ありませんが、将来的な保守で混乱を招く可能性があります。

🤖 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/fsm.go` around lines 372 - 373, applyBackup と applyHLCLease
で入力データのオペコード除外方法を統一してください。applyBackup 呼び出し側では applyHLCLease と同様にオペコードバイトを除いた
data[1:] を渡し、decodeBackupEntry の期待する入力形式も確認して関連処理を一貫させてください。
🤖 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.
Outside diff comments:
In `@main.go`:
- Around line 389-416: Register readTracker.Close with the shutdown cleanup
after creating the ActiveTimestampTracker, using cleanup.Add(readTracker.Close),
so its sweeper goroutine is stopped during termination.
---
Nitpick comments:
In `@kv/fsm.go`:
- Around line 372-373: applyBackup と applyHLCLease
で入力データのオペコード除外方法を統一してください。applyBackup 呼び出し側では applyHLCLease と同様にオペコードバイトを除いた
data[1:] を渡し、decodeBackupEntry の期待する入力形式も確認して関連処理を一貫させてください。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b885a775-9adc-47fb-833c-e1990bc6fa44

📥 Commits

Reviewing files that changed from the base of the PR and between cd58354 and b04b7fb.

📒 Files selected for processing (11)
  • kv/active_timestamp_tracker.go
  • kv/active_timestamp_tracker_test.go
  • kv/backup_codec.go
  • kv/backup_codec_test.go
  • kv/fsm.go
  • kv/fsm_backup.go
  • kv/fsm_backup_test.go
  • main.go
  • main_bootstrap_e2e_test.go
  • main_encryption_write_wiring.go
  • multiraft_runtime_test.go

@bootjp

Copy link
Copy Markdown
OwnerAuthor

Addressed latest-head review findings:

  • registered readTracker.Close with the shutdown cleanup stack so the backup-pin sweeper stops on termination
  • made backup FSM apply pass payload bytes consistently with the other reserved-opcode apply paths while preserving the full wire encoder/decoder contract

Validation:

  • go test ./kv -run 'Test(ActiveTimestampTracker|BackupCodec|ApplyBackup|BackupPayload)' -count=1 -timeout=240s\n- go test ./kv -count=1 -timeout=300s\n- go test . -count=1 -timeout=180s\n- golangci-lint run ./kv --timeout=5m\n- golangci-lint run . ./kv --timeout=5m\n- git diff --check\n- git verify-commit HEAD\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 (a39218b):

  • kv/fsm.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. 👍

Reviewed commit:a39218b5b9

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

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

  • kv/fsm.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-connectorchatgpt-codex-connectorBot 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:4d56518ea2

ℹ️ 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 threadkv/active_timestamp_tracker.go Outdated
}
return errors.WithStack(ErrInvalidBackupPin)
}
pin.deadline = deadline

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 the later backup deadline on renewals

When two BackupExtend proposals for the same pin are in flight, or a retry of an older renewal commits after a newer one, this unconditional assignment can move the deadline backwards. If that stale deadline expires before the next renewal, Oldest() stops honoring the pin and local compaction can advance past the backup read timestamp while the backup is still running; apply should keep max(existing deadline, requested deadline) rather than shortening it.

Useful? React with 👍 / 👎.

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

  • kv/fsm.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
bootjpforce-pushed the design/live-backup-pin-substrate branch from 13d22a8 to 26d45baCompareJuly 19, 2026 11:08
@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 (26d45ba):

  • kv/fsm.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.

@bootjp

Copy link
Copy Markdown
OwnerAuthor

History-only author-compliance repair completed at 26d45baebb34ae6afbfaf39394e62deafcddc481.

  • final tree is byte-identical to prior head 13d22a8171d828d6eed4bd87aceed69ece2e801a
  • the PR now exposes one linear commit authored and committed by bootjp <contact@bootjp.me>
  • GitHub reports the commit signature as verified/valid
  • parent is current origin/main (37277ea86cb4cf66bf980b8a09427394f6de69e1)

No runtime or source-tree semantics changed. CI is running on the reconstructed head.

@codex review

@bootjp

Copy link
Copy Markdown
OwnerAuthor

Current reconstructed head 26d45baebb34ae6afbfaf39394e62deafcddc481 is fully green. PR-visible authorship and signature verification pass, the final tree remains identical to the pre-rewrite head, and there are no current-head root findings. Please complete the latest-head review. @codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit:26d45baebb

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

bootjp added 8 commits July 19, 2026 22:25
## Summary
- add BeginBackup, RenewBackup, EndBackup, ListAdaptersAndScopes, and
StreamBackup admin RPCs
- replicate bounded backup pin reservations and per-group retention
fences through Raft
- gate backup start on live member capabilities and snapshot headroom
- scan and classify user-visible keys at one pinned read timestamp using
the existing logical encoders
- rotate HMAC-protected renewal tokens with a hard deadline and reapply
complete pins so partial delivery cannot leave a replica unprotected
## Safety
- enforce a cluster-wide active backup cap with deterministic
reservation and compensating release
- reject expired tokens for renew, list, and stream while allowing
EndBackup cleanup
- retry transient per-group proposals and fail closed when renewal
cannot finish before the prior deadline
- scope compactor retention fences to their Raft group while preserving
process-wide ordinary read pins
## Validation
- go test ./adapter -run
Test\(BeginBackup\|RenewBackup\|BackupToken\|StreamBackup\|BackupProtocol\|GetRaftGroups\|GetNodeVersion\|Admin\)
-count=1 -timeout=240s
- go test ./kv ./internal/backup . -count=1 -timeout=240s
- go test -race ./adapter ./kv ./internal/backup -run
Test\(BeginBackup\|RenewBackup\|BackupToken\|StreamBackup\|BackupProtocol\|GetRaftGroupsLeaderVersion\|GetRaftGroupsSnapshotsEachGroupOnce\|LeaderVersionProbeAttemptTimeout\|ActiveTimestampTracker\|ApplyBackup\|FSMCompactorScopesBackupPinsByGroup\|LiveDecoder\|ScopeForKey\)
-count=1 -timeout=300s
- golangci-lint run . ./adapter/... ./kv/... ./internal/backup/...
--timeout=5m --allow-parallel-runners
- buf generate
- buf breaking --against the stacked admin API base
Author: bootjp
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **新機能**
* Admin 経由でバックアップの開始・更新・終了、対象スコープ一覧、キー・バリューのストリーミング取得に対応しました。
* スコープ単位の絞り込みと、バックアッププロトコルの対応可否をバージョンで判定します。
* **改善**
* 固定した読み取り時点とルートスナップショットで安定した取得を実現しました。
* 予約/解除の管理を強化し、キーのみの走査・再利用を最適化しました。
* **信頼性**
* TTL/ヘッドルーム/容量/互換性の事前検証、部分失敗時の補償、エラー時の後処理を強化しました。
* バックアップピンの同時上限を引き下げました。
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitaicoderabbitaiBot 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 (4)
internal/backup/live.go (1)

20-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

redis 分岐は冗長なデッドコードです。

s.Adapter == "redis" のとき "redis/" + s.Name は default 分岐の s.Adapter + "/" + s.Name と完全に同一の文字列になるため、この特別扱いは効果がありません。あわせて String()(Line 245-247)も同じ adapter/name 形式を返しており重複しています。分岐を削除し、必要なら ID()String() に委譲することを検討してください。

♻️ 冗長分岐の削除案
 func (s Scope) ID() string {
-	if s.Adapter == "redis" {- return "redis/" + s.Name-	}
return s.Adapter + "/" + s.Name
}
🤖 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 `@internal/backup/live.go` around lines 20 - 25, Remove the redundant s.Adapter
== "redis" branch from Scope.ID and keep the single generic adapter/name
construction. Reuse the existing String method if appropriate so ID and String
share the same formatting without duplicating logic.
kv/leader_admin_proposer.go (1)

77-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

リトライ/バックオフの骨格が複数箇所で重複しています。

forwardAdminWithRetry/runAdminForwardCyclekv/leader_proxy.goforwardWithRetry/runForwardCycle および新規の forwardLeaseRead と本質的に同じ「デッドライン計算→ループ→lastErr nilガード→バックオフ→再デッドラインチェック」の骨格です。詳細はファイル末尾の consolidated comment を参照してください。

🤖 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/leader_admin_proposer.go` around lines 77 - 128,
重複しているリトライ/バックオフ処理を共通化し、leader_proxy.go の
forwardWithRetry・runForwardCycle・forwardLeaseRead と leader_admin_proposer.go の
forwardAdminWithRetry・runAdminForwardCycle が同じデッドライン管理、lastErr nil
ガード、バックオフ、再チェックの骨格を共有するよう更新してください。各処理固有の forwardAdmin
などの実行部分とエラー判定は既存の挙動を保ったまま、共通ヘルパーを再利用して重複実装を除去してください。
kv/leader_proxy.go (2)

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

リーダー転送のリトライ/デッドライン/バックオフ骨格が3箇所で重複しています。

kv/leader_proxy.go の既存 forwardWithRetry/runForwardCycle、新規 forwardLeaseRead/forwardLeaseReadOnce、そして kv/leader_admin_proposer.goforwardAdminWithRetry/runAdminForwardCycle は、いずれも「デッドライン計算→ループでフォワード実行→lastErr==nil時にErrLeaderNotFoundへフォールバック→デッドライン超過チェック→バックオフ→再チェック」という同一の制御フローを持ちます。戻り値型が異なるだけなので、Go genericsを使った共通ヘルパーへの抽出でリトライ挙動のバグ修正・変更を1箇所に集約できます。

  • kv/leader_proxy.go#L228-270: forwardLeaseRead/forwardLeaseReadOnce を共通ヘルパーを呼ぶ形に置き換える。
  • kv/leader_admin_proposer.go#L77-128: forwardAdminWithRetry/runAdminForwardCycle も同じ共通ヘルパーを利用する形に置き換える。
🤖 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/leader_proxy.go` at line 1,
リーダー転送のリトライ制御がforwardWithRetry/runForwardCycle、forwardLeaseRead/forwardLeaseReadOnce、forwardAdminWithRetry/runAdminForwardCycleで重複しているため、デッドライン計算・実行ループ・ErrLeaderNotFoundフォールバック・バックオフを扱うGoジェネリック共通ヘルパーを追加する。forwardLeaseReadとforwardAdminWithRetryの各処理をそのヘルパー呼び出しへ置き換え、既存の戻り値型とリトライ挙動を維持し、個別の制御フロー重複を削除する。

228-270: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

forwardLeaseRead/forwardLeaseReadOnce は既存の forwardWithRetry/runForwardCycle とほぼ同一のリトライ骨格です。

デッドライン計算、lastErr==nil ガード、バックオフ後の再デッドラインチェックが同ファイル内で二重実装されています。詳細はファイル末尾の consolidated comment を参照してください。

🤖 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/leader_proxy.go` around lines 228 - 270, Refactor forwardLeaseRead to
reuse the existing forwardWithRetry and runForwardCycle retry helpers instead of
duplicating deadline calculation, lastErr handling, backoff, and deadline
checks. Adapt the lease-read operation through these helpers while preserving
transient-error retries, immediate propagation of non-transient errors, and the
existing ErrLeaderNotFound fallback.
🤖 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 `@internal/backup/live.go`:
- Around line 20-25: Remove the redundant s.Adapter == "redis" branch from
Scope.ID and keep the single generic adapter/name construction. Reuse the
existing String method if appropriate so ID and String share the same formatting
without duplicating logic.
In `@kv/leader_admin_proposer.go`:
- Around line 77-128: 重複しているリトライ/バックオフ処理を共通化し、leader_proxy.go の
forwardWithRetry・runForwardCycle・forwardLeaseRead と leader_admin_proposer.go の
forwardAdminWithRetry・runAdminForwardCycle が同じデッドライン管理、lastErr nil
ガード、バックオフ、再チェックの骨格を共有するよう更新してください。各処理固有の forwardAdmin
などの実行部分とエラー判定は既存の挙動を保ったまま、共通ヘルパーを再利用して重複実装を除去してください。
In `@kv/leader_proxy.go`:
- Line 1:
リーダー転送のリトライ制御がforwardWithRetry/runForwardCycle、forwardLeaseRead/forwardLeaseReadOnce、forwardAdminWithRetry/runAdminForwardCycleで重複しているため、デッドライン計算・実行ループ・ErrLeaderNotFoundフォールバック・バックオフを扱うGoジェネリック共通ヘルパーを追加する。forwardLeaseReadとforwardAdminWithRetryの各処理をそのヘルパー呼び出しへ置き換え、既存の戻り値型とリトライ挙動を維持し、個別の制御フロー重複を削除する。
- Around line 228-270: Refactor forwardLeaseRead to reuse the existing
forwardWithRetry and runForwardCycle retry helpers instead of duplicating
deadline calculation, lastErr handling, backoff, and deadline checks. Adapt the
lease-read operation through these helpers while preserving transient-error
retries, immediate propagation of non-transient errors, and the existing
ErrLeaderNotFound fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d36b16f2-fda5-408f-b7a0-4be2ee194b98

📥 Commits

Reviewing files that changed from the base of the PR and between b04b7fb and 1eddbb1.

⛔ Files ignored due to path filters (5)
  • proto/admin.pb.go is excluded by !**/*.pb.go
  • proto/admin_grpc.pb.go is excluded by !**/*.pb.go
  • proto/internal.pb.go is excluded by !**/*.pb.go
  • proto/internal_grpc.pb.go is excluded by !**/*.pb.go
  • proto/service.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (51)
  • adapter/admin_backup.go
  • adapter/admin_backup_test.go
  • adapter/admin_grpc.go
  • adapter/admin_grpc_test.go
  • adapter/internal.go
  • adapter/internal_admin_proposal_test.go
  • distribution/engine.go
  • docs/design/2026_04_29_proposed_logical_backup.md
  • internal/backup/live.go
  • internal/backup/live_test.go
  • internal/backup/s3.go
  • internal/backup/sqs.go
  • internal/raftadmin/server_test.go
  • internal/raftengine/engine.go
  • internal/raftengine/etcd/engine.go
  • internal/raftengine/etcd/wal_purge_test.go
  • kv/active_timestamp_tracker.go
  • kv/active_timestamp_tracker_test.go
  • kv/backup_codec.go
  • kv/backup_codec_test.go
  • kv/backup_scan.go
  • kv/compactor.go
  • kv/compactor_test.go
  • kv/coordinator.go
  • kv/coordinator_retry_test.go
  • kv/fsm.go
  • kv/fsm_backup.go
  • kv/fsm_backup_test.go
  • kv/keyviz_label.go
  • kv/leader_admin_proposer.go
  • kv/leader_admin_proposer_test.go
  • kv/leader_proxy.go
  • kv/leader_proxy_test.go
  • kv/lease_read_test.go
  • kv/shard_store.go
  • kv/shard_store_test.go
  • kv/sharded_coordinator.go
  • kv/sharded_coordinator_leader_test.go
  • kv/sharded_coordinator_txn_test.go
  • kv/tso_test.go
  • kv/txn_keys.go
  • main.go
  • main_admin.go
  • main_admin_test.go
  • main_bootstrap_e2e_test.go
  • main_encryption_write_wiring.go
  • main_sqs_leadership_refusal_test.go
  • multiraft_runtime_test.go
  • proto/admin.proto
  • proto/internal.proto
  • proto/service.proto
🚧 Files skipped from review as they are similar to previous changes (4)
  • multiraft_runtime_test.go
  • main_encryption_write_wiring.go
  • kv/active_timestamp_tracker_test.go
  • kv/active_timestamp_tracker.go

@chatgpt-codex-connectorchatgpt-codex-connectorBot 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:1eddbb1aa2

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

if err != nil {
return nil, err
}
defer s.forgetBackupSession(tok.pinID)

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 Prevent renewals from resurrecting ended backups

When a client's background RenewBackup overlaps with EndBackup, this defer leaves the session live while the release entries are being proposed. If the group log orders EndBackup's Release before the in-flight renewal's complete Pin, finishRenewBackup can still extend the session before this defer runs, then EndBackup returns and forgets the session without issuing another release; the renewed pin remains active until its deadline and continues blocking compaction/capacity after the backup was ended. Mark the session as closing (or otherwise reject/serialize renewals for the pin) before proposing releases so no renewal can commit after the final release.

Useful? React with 👍 / 👎.

Comment threadadapter/admin_backup.go Outdated
if err != nil {
return nil, err
}
if err := s.requireUnexpiredBackupToken(tok); 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 Allow retrying a committed renewal after the old token expires

If a RenewBackup RPC commits successfully but the response carrying the rotated token is lost, the server has already extended the in-memory session in finishRenewBackup, but the client can only retry with the old token. Once the old embedded deadline passes, this pre-session check rejects the retry even though the pin is still live until the committed later deadline, leaving the backup unable to obtain the current token and eventually losing its retention fence. Check the live session for the same pin/readTS before rejecting, or return the session's current token idempotently.

Useful? React with 👍 / 👎.

Comment threadkv/backup_scan.go Outdated
}

func (s *ShardStore) txnCommitTSAt(ctx context.Context, primaryKey []byte, startTS uint64, ts uint64) (uint64, bool, error) {
b, err := s.GetAt(ctx, txnCommitKey(primaryKey, startTS), 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.

P2 Badge Resolve backup lock status against the captured route

When a backup races a route split or move, ValidateBackupSnapshotAt scans locks from the captured read-ts route set, but this lookup resolves the primary commit record through the live ShardStore routing table. If the primary key has moved after read_ts, GetAt checks the new owner instead of the historical group that contains the commit/rollback record, so an already-resolved transaction is treated as pending and BeginBackup fails despite a clean snapshot. Read the txn status via the captured route/group for the primary key rather than live routing.

Useful? React with 👍 / 👎.

A review round read IsVolatileOnlyPayload as letting the cold-start skip
path seed Raft past an unapplicable backup entry, on the grounds that the
path discards fsm.Apply's return value. It does not: applyNormalCommitted
inspects the response for HaltApply before its `if duplicate` early
return, so a volatile duplicate that cannot be applied halts the loop.
That contract was load-bearing but untested, which is why it read as a
gap. Cover both halves. On the engine side, a volatile-classified
duplicate whose Apply returns a HaltApply response must fail
applyNormalCommitted. On the kv side, a malformed payload, an unknown
future subtype, and a backup entry with no tracker each stay classified
as volatile -- so the entry is delivered rather than dropped -- and each
returns a halting response.
Claude-Session: https://claude.ai/code/session_013rNHooj7NF3giihWVba8QE
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Two review findings on the pin session.
EndBackup left the session live while it proposed its release entries.
An overlapping RenewBackup could therefore still extend the session, and
if the group log ordered the Release before the renewal's Pin the pin
stayed active until its deadline, blocking compaction and capacity for a
backup that had already ended -- EndBackup then forgot the session
without issuing another release. Mark the session closing before
proposing anything, and refuse renewals and extends from that point.
RenewBackup also rejected a token whose embedded deadline had passed
before consulting the live session. A renewal that committed but whose
response was lost leaves the client holding only the old token, so the
retry was refused even though the pin was still live at the committed
later deadline, and the backup could never obtain a current token.
requireRenewableBackupToken accepts that case when the session for the
same pin and read timestamp is still within its own deadline.
Only renewal becomes retryable. ListAdaptersAndScopes and StreamBackup
keep requireUnexpiredBackupToken, so data-plane calls still fail closed
on an expired token; TestBackupTokenDeadlineRotatesAndFailsClosed is
updated to assert exactly that split.
Claude-Session: https://claude.ai/code/session_013rNHooj7NF3giihWVba8QE
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@bootjp

Copy link
Copy Markdown
OwnerAuthor

Codex P2 2 件 — adapter/admin_backup.go:475 (Prevent renewals from resurrecting ended backups) と :422 (Allow retrying a committed renewal after the old token expires) に対応しました(454cc3b1)。

1. EndBackup と Renew の競合 (:475)

ご指摘のとおり defer s.forgetBackupSession(...) は関数末尾で走るため、release エントリを提案している最中もセッションは live のままでした。並行する RenewBackupfinishRenewBackupextendBackupSession を通してしまい、グループログが EndBackup の Release を renewal の Pin より先に順序づけた場合、pin は deadline まで生き残ります。EndBackup は release を再発行しないままセッションを忘れるため、終了済みバックアップが compaction と容量を塞ぎ続けます。

backupSessionclosing を追加し、release を提案する前にcloseBackupSession でマークするようにしました。extendBackupSessionrequireRenewableBackupToken の両方が closing を拒否します。

2. コミット済み renewal のリトライ (:422)

requireUnexpiredBackupToken はセッションを見る前にトークンの埋め込み deadline だけで弾いていました。renewal がコミットしたのに応答が失われた場合、クライアントは旧トークンしか持たず、その deadline を過ぎるとリトライが拒否されます。pin はコミット済みの後ろ倒しされた deadline まで live なのに、現行トークンを取得する手段が無くなり、最終的に retention fence を失います。

requireRenewableBackupToken を追加し、同じ pin / read timestamp のセッションが自身の deadline 内なら期限切れトークンでの renewal を受け付けます(再提案して新しいトークンを返すので、ご提案の「return the session's current token idempotently」と同じ効果です)。

リトライ可能になるのは renewal だけです。ListAdaptersAndScopesStreamBackuprequireUnexpiredBackupToken のままなので、データプレーンは期限切れトークンに対して fail-closed を維持します。

既存テストの意図的な変更(レビューしてほしい点)

TestBackupTokenDeadlineRotatesAndFailsClosed は「rotate 後に旧トークンで renewal すると FailedPrecondition」を固定していました。これはまさに本指摘が「誤り」としている挙動なので、

  • 旧トークンでの renewal は成功して現行トークンが返ること、
  • 同じ旧トークンでの ListAdaptersAndScopes / StreamBackup は引き続き FailedPrecondition であること、
    を検証する形に更新しました。「fails closed」の性質はデータプレーン側で保たれています。

Caller audit

  • requireRenewableBackupToken の呼び出し元は RenewBackup の 2 箇所(提案前・提案後)のみ。提案後のチェックも「提案中にトークンが失効したか」を見るものなので、セッション基準に揃えました。
  • requireUnexpiredBackupTokenListAdaptersAndScopes (:513) と StreamBackup (:549) が引き続き使用。意味論は未変更です。
  • closeBackupSession は新規で、呼び出し元は EndBackup のみ。
  • extendBackupSessionclosing 判定を追加(fail-closed 方向)。呼び出し元は finishRenewBackup のみで、false 時は既に compensateBackupRelease が走ります。
  • forgetBackupSession は不変。

Test evidence

  • 追加 TestEndBackupRejectsOverlappingRenewal — EndBackup が release を始めた状態(closeBackupSession 済み)で renewal が FailedPrecondition になり、extendBackupSession も false を返すことを検証。
  • 追加 TestRequireRenewableBackupToken(table-driven 6 ケース: セッション無しの有効/期限切れ、セッションが後ろ倒しで生きている、セッションも期限切れ、read timestamp 不一致、closing 中)。
  • closing ガードが load-bearing であることを確認: 外すと両テストが「An error is expected but got nil」で失敗します。
  • go test -race -run 'Backup|Admin|Renew|EndBackup' ./adapter/ → ok (9.4s)
  • golangci-lint run adapter/... → 0 issues
  • 補足: go test -race ./adapter/(全体)はローカルの 10 分上限に達したため完走していません。変更は admin_backup.go に閉じており上記サブセットで網羅していますが、全体は CI での確認をお願いします。

Self-review

  1. データ損失: 終了済みバックアップの pin が残って compaction/容量を塞ぎ続ける経路を塞ぎました。逆にリトライ受理側は、live なセッションがある場合のみなので死んだ pin を蘇らせません。
  2. 並行性 / 分散障害: 本指摘の主目的。EndBackup / Renew の競合窓を release 提案前に閉じています。
  3. パフォーマンス: セッションマップ参照が 1 回増えるのみ。
  4. データ整合性: retention fence を失う経路(コミット済み renewal のリトライ不能)が解消。
  5. テスト網羅: closing / 期限切れ / read timestamp 不一致の各分岐を table-driven で網羅し、ガード除去時の失敗も確認。

残る P2 は kv/backup_scan.go:260(captured route ではなく live routing でロック状態を解決している件)です。次の tick で対応します。

@claude review

@claude

claudeBot commented Aug 22, 2026

Copy link
Copy Markdown

Claude finished @bootjp's task in 2m 18s —— View job


Review: 454cc3b (backup: close the renewal races around session lifetime)

  • Read trigger comment and gather context
  • Inspect the latest commit (454cc3b) that addresses the two Codex P2 findings
  • Review closeBackupSession / requireRenewableBackupToken logic for correctness and races
  • Review the new/updated tests
  • Check remaining known-open item (kv/backup_scan.go:260) status
  • Post review findings

Reviewed adapter/admin_backup.go (diff of 454cc3b) against the two P2 findings from the earlier round.

Finding 1 — EndBackup/Renew race (:475): fixed correctly. closeBackupSession is called before either raft proposal in EndBackup (adapter/admin_backup.go:483), and the closing flag is checked in both the gate (requireRenewableBackupToken) and, critically, in extendBackupSession under the same backupStateMu lock (adapter/admin_backup.go:1162). That second check is the one that actually matters: RenewBackup's pre-checks (requireRenewableBackupToken at lines 427 and 443) are advisory/early-exit, but even if a renewal slips past both of them while EndBackup closes the session concurrently, finishRenewBackupextendBackupSession will still see closing=true, return false, and trigger compensateBackupRelease (line 459) — so a Pin re-committed behind a Release gets a follow-up Release proposed by the same admin goroutine. Good defense in depth; the fix doesn't rely solely on the earlier, racier checks.

Finding 2 — retrying a committed renewal after token expiry (:422): requireRenewableBackupToken (adapter/admin_backup.go:1197) correctly falls back to the live session's deadline when the token's own embedded deadline has passed, gated on matching pinID + readTS and the session not being closing. ListAdaptersAndScopes/StreamBackup were correctly left on requireUnexpiredBackupToken, so the data-plane fail-closed behavior is unchanged — only the control-plane renewal path became retryable, which matches the stated intent.

Tests: TestEndBackupRejectsOverlappingRenewal and the table-driven TestRequireRenewableBackupToken (6 cases) look well targeted — they specifically hit the closing guard, the readTS-mismatch case, and the "expired token / live session" retry case. The updated TestBackupTokenDeadlineRotatesAndFailsClosed correctly splits the assertion: renewal succeeds with a rotated token, while ListAdaptersAndScopes/StreamBackup still reject the same stale token. This lines up with the PR comment's claim that removing the closing guard makes both new tests fail.

Minor observation (not a defect): in RenewBackup, both failure paths after renewBackupGroups (lines 439-441 and 444-446) call s.forgetBackupSession(tok.pinID) unconditionally — including on a transient renewBackupGroups error unrelated to closing. That means a live, non-closing session gets deleted outright on any renewal failure (e.g. a transient raft proposal error), rather than just failing that one renewal attempt. This is pre-existing behavior untouched by 454cc3b (confirmed via git show — those lines are unchanged context), so it's out of scope for this fix, but worth a follow-up: a transient failure mid-renewal currently forces the client to restart the whole BeginBackup flow (losing the route snapshot) rather than allowing another RenewBackup retry against the still-open pin.

Remaining open item: the PR comment says the last outstanding P2 (kv/backup_scan.go:260 — resolving primary-txn lock status via live routing instead of the captured route snapshot) is deferred to "the next tick." Confirmed still open: primaryTxnRecordedStatusAt/txnCommitTSAt in kv/backup_scan.go still call s.GetAt (live-routed ShardStore), not a route resolved from the captured BackupRouteSnapshot. No action needed here now, just confirming the status matches what was reported.

Note: I was not able to execute go test/golangci-lint in this sandboxed review session (Bash tool required interactive approval for go invocations that wasn't available), so this review is based on static reading of the diff plus the test code, not a live run. The evidence posted in the trigger comment (go test -race, golangci-lint) is consistent with what the diff should produce.

Overall: the fix is correctly scoped, closes the race via the authoritative lock-protected path rather than just the advisory pre-checks, and the new tests exercise the actual guard rather than just the happy path. No blocking issues found.

ValidateBackupSnapshotAt scans locks from the route set captured at
read_ts, but resolved the owning transaction's commit and rollback
records through ShardStore.GetAt, which follows the live routing table.
When a backup raced a split or move, the primary key's live route pointed
at the new owner, the historical decision record was not there, and an
already-resolved transaction read as pending -- so BeginBackup failed on
a clean snapshot.
Thread the captured snapshot down to the status lookup and read the
decision records from the group that owned the key at read_ts. When the
snapshot has no route covering the key the lookup falls back to live
routing, so callers outside the captured range behave as before.
Claude-Session: https://claude.ai/code/session_013rNHooj7NF3giihWVba8QE
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@bootjp

Copy link
Copy Markdown
OwnerAuthor

Codex P2 kv/backup_scan.go:260Resolve backup lock status against the captured route に対応しました(9609b813)。これで本 PR の未解決指摘 4 件はすべて対応済みです。

根本原因(指摘どおり)
ValidateBackupSnapshotAt は read_ts 時点で捕捉したルート集合からロックをスキャンする一方、そのトランザクションの commit / rollback レコードは ShardStore.GetAt(= ライブのルーティングテーブル)で解決していました。バックアップが split / move とレースすると、primary key のライブルートは新オーナーを指し、履歴上の決定レコードはそこに無いため、解決済みのトランザクションが pending として扱われ、クリーンなスナップショットなのに BeginBackup が失敗します。

修正
捕捉済み snapshot を validateBackupGroupLocksAtplanBackupLockResolutionsAtplanBackupLockResolutionAtcachedBackupLockTxnStatusAtprimaryTxnRecordedStatusAttxnCommitTSAt / hasTxnRollbackAt まで通し、新設の capturedBackupGetAtread_ts 時点でそのキーを所有していたグループから決定レコードを読むようにしました。既存の routeContainsKey をそのまま使っています。

snapshot に該当キーを覆うルートが無い場合はライブルーティングにフォールバックするので、捕捉範囲外のキーは従来どおりの挙動です(非退行)。

Caller audit
変更したのは backup 検証経路に閉じた 6 関数で、いずれも ValidateBackupSnapshotAt からしか到達しません(grep で確認済み。kv/backup_scan.go 内のみ、テスト以外の外部呼び出し元なし)。ShardStore.GetAt 自体は未変更で、他の利用者に影響はありません。

Test evidence

  • 追加 TestValidateBackupSnapshotAtResolvesStatusThroughCapturedRoute — group 1 にロック行と commit レコードを残したまま、ライブエンジンは同じキーを空の group 2 に向ける構成。
  • 修正前に失敗することを確認済み:
    --- FAIL: TestValidateBackupSnapshotAtResolvesStatusThroughCapturedRoute
    Target error should not be in err chain: (= ErrTxnLocked / pending 扱いになっていた)
    
  • 検証は ErrTxnLocked不在をアサートしています。このテストハーネスは proposer を配線していないため resolution の適用自体は ErrNotSupported になりますが、判定したいのは「pending と誤認されないこと」なので、そこを直接固定しました(コメントにも明記しています)。
  • go test -race ./kv/ → ok (14.7s)
  • golangci-lint run kv/... → 0 issues

Self-review

  1. データ損失: なし。検証経路の読み取りのみ。むしろ「クリーンなスナップショットでバックアップが取れない」誤検知が解消します。
  2. 並行性 / 分散障害: 本指摘の主目的。バックアップとルート移動のレースで、ロック解決が履歴と食い違う経路を塞ぎました。
  3. パフォーマンス: 捕捉ルートの線形探索が 1 回増えますが、statusCache により txn ごとに 1 回だけです。
  4. データ整合性: ロックのスキャン元と決定レコードの読み取り元が同じ read_ts のルート集合に揃いました。
  5. テスト網羅: ルート移動後の解決済み txn を回帰テストで固定し、修正前の失敗も確認。

@claude review

@claude

claudeBot commented Aug 22, 2026

Copy link
Copy Markdown

Claude finished @bootjp's task in 3m 18s —— View job


Review: 9609b81 (backup: resolve lock status through the captured route)

  • Read trigger comment and gather context
  • Inspect the latest commit 9609b81
  • Review the kv/backup_scan.go fix for the P2 finding (txn status resolution via captured route vs. live routing)
  • Review the new regression test
  • Confirm caller-audit claims (grep for all call sites of the changed functions)
  • Check the new read path for consistency with the rest of the file's leader-fencing conventions
  • Post review findings

The stated fix is correct and well-targeted.capturedBackupGetAt (kv/backup_scan.go:275) now threads the captured BackupRouteSnapshot down through txnCommitTSAt/hasTxnRollbackAtprimaryTxnRecordedStatusAtcachedBackupLockTxnStatusAtplanBackupLockResolutionAt, and resolves the primary key's commit/rollback record against the group that owned it at read_ts (matched via routeKey + routeContainsKey against snapshot.routes), falling back to live s.GetAt only when the snapshot has no covering route. routeKey() correctly recovers the embedded primary key from !txn|cmt|/!txn|rb| keys via txnRouteKey (kv/txn_keys.go:88-99), so the route lookup actually matches — this isn't a no-op fallback-to-live-routing-every-time.

Caller audit confirmed: grepping for txnCommitTSAt, hasTxnRollbackAt, primaryTxnRecordedStatusAt, capturedBackupGetAt, and the other threaded functions shows the whole call chain is closed and reachable only from ValidateBackupSnapshotAt, as claimed.

Test (TestValidateBackupSnapshotAtResolvesStatusThroughCapturedRoute, kv/shard_store_test.go): sets up a committed transaction whose commit record lives on group 1, then re-routes the live catalog to group 2 (empty) while the captured BackupRouteSnapshot still points at group 1, and asserts the result is not ErrTxnLocked. Tracing lockResolutionForStatus (kv/shard_store.go:1761), a txnStatusPending result returns NewTxnLockedError directly before ever reaching applyScanLockResolutions, so pre-fix this test would fail on ErrTxnLocked exactly as described, and post-fix it resolves to txnStatusCommitted and only fails later on the harness's missing proposer (ErrNotSupported, a different error) — matching the test's own comment. This is a good regression test.

I could not execute go test/golangci-lint in this sandboxed session (go test requires interactive approval that wasn't available here), so the above is verified by static tracing, not a live run — consistent with the same limitation noted in the prior review on this PR.

New finding: the captured-route read path bypasses leader-lease fencing

capturedBackupGetAt's route-matched branch (kv/backup_scan.go:290) calls g.Store.GetAt(ctx, key, ts)directly on the resolved group's local store:

g, ok:=s.groupForID(route.GroupID)
if!ok||g==nil||g.Store==nil {
break
}
val, err:=g.Store.GetAt(ctx, key, ts) // <-- kv/backup_scan.go:290returnval, errors.WithStack(err)

This bypasses the same lease-aware leader check that every other per-group read in this file goes through: ShardStore.GetAt (kv/shard_store.go:153) and ShardStore.GetGroupAt (kv/shard_store.go:173) both route through getGroupAt, which checks isLinearizableRaftLeader and either serves locally (leaderGetAt) or proxies to the actual leader (proxyRawGet) when the current node is not the leader for that group. capturedBackupGetAt skips all of that and reads whatever this node's local replica of g.Store currently has applied.

This file already has the right helper for exactly this situation — getRouteAt (kv/shard_store.go:183), which resolves a distribution.Route to its group and then goes through the same getGroupAt fencing/proxy path. It's already used one function away in the same file, for the same "read a key at a captured route + fixed timestamp" purpose: s.store.getRouteAt(ctx, route, item.key, s.ts) (kv/backup_scan.go:558, in backupScanner.materializeBackupKey).

Failure scenario: BeginBackup/RenewBackup can run on any admin node, not necessarily the Raft leader for every data group involved. If the node executing ValidateBackupSnapshotAt is a follower for the group that owns a transaction's primary key, and that follower is lagging (hasn't yet applied the commit/rollback entry that has already been durably committed), capturedBackupGetAt returns store.ErrKeyNotFound for a commit record that in fact exists — the transaction reads as txnStatusPending, and lockResolutionForStatus returns ErrTxnLocked, failing BeginBackup on a snapshot this fix is specifically meant to validate as clean. This reintroduces a variant of the exact class of bug this commit fixes (a status lookup that doesn't reflect the true committed state), just triggered by replication lag on the correct owner rather than by a route move to the wrong owner.

Suggested fix: replace the direct g.Store.GetAt call with s.getRouteAt(ctx, route, key, ts), mirroring the existing pattern in materializeBackupKey.

Fix this →

Summary

The commit correctly fixes the reported issue (resolving lock status via the captured route instead of live routing) and ships a solid regression test for that specific scenario. However, the new capturedBackupGetAt helper introduces a related but distinct gap: it drops leader-lease fencing/proxying for the common (route-matched) case, which can reintroduce spurious ErrTxnLocked failures on BeginBackup when it runs against a lagging follower — recommend swapping in the existing getRouteAt helper before merge.

Sign up for freeto 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

@bootjp