Skip to content

Add live logical backup control plane#1119

Open
bootjp wants to merge 3 commits into
design/live-backup-pin-substratefrom
design/live-backup-control-plane
Open

Add live logical backup control plane#1119
bootjp wants to merge 3 commits into
design/live-backup-pin-substratefrom
design/live-backup-control-plane

Conversation

@bootjp

@bootjp bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner

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

Summary by CodeRabbit

  • 新機能
    • Admin 経由でバックアップの開始・更新・終了、対象スコープ一覧、キー・バリューのストリーミング取得に対応しました。
    • スコープ単位の絞り込みと、バックアッププロトコルの対応可否をバージョンで判定します。
  • 改善
    • 固定した読み取り時点とルートスナップショットで安定した取得を実現しました。
    • 予約/解除の管理を強化し、キーのみの走査・再利用を最適化しました。
  • 信頼性
    • TTL/ヘッドルーム/容量/互換性の事前検証、部分失敗時の補償、エラー時の後処理を強化しました。
    • バックアップピンの同時上限を引き下げました。

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 907a705e-2f74-40f7-9c25-292733f5fce7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ライブバックアップ用のFSMワイヤ、スナップショット走査、Admin RPC、トークン管理、起動時設定、アダプタ別デコード機能、管理提案転送、統合テストを追加しました。

Changes

ライブバックアップ基盤

Layer / File(s) Summary
バックアップワイヤとFSM処理
proto/admin.proto, kv/backup_codec.go, kv/fsm_backup.go, kv/*_test.go
予約・解除サブタイプ、バックアップFSM適用、HLC観測、関連RPCメッセージとテストを追加しました。
スナップショット走査とスコープ復元
kv/backup_scan.go, internal/backup/*, kv/shard_store_test.go
ルートスナップショット共有、キー専用スキャナ、アダプタ別スコープ復元、S3/SQSキー解析を実装しました。
Adminバックアップライフサイクル
adapter/admin_backup.go, adapter/admin_grpc.go, adapter/admin_backup_test.go
Begin/Renew/End、一覧取得、ストリーミング、HMACトークン、ピア互換性ゲート、セッション更新と統合テストを追加しました。
起動設定とバックアップ配線
main.go, main_admin_test.go, kv/active_timestamp_tracker.go
ライブバックアップ設定の検証、ピン上限、read fence、ピアプローブ、提案者登録を起動処理へ接続しました。
管理提案のリーダー転送
adapter/internal.go, kv/leader_admin_proposer.go, proto/internal.proto, *_test.go
管理提案をローカル提案者またはリーダーへのInternal RPCへ転送する経路を追加しました。

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

Sequence Diagram(s)

sequenceDiagram
  participant AdminClient
  participant AdminServer
  participant LeaderAdminProposer
  participant InternalLeader
  participant RaftGroups
  participant ShardStore

  AdminClient->>AdminServer: BeginBackup
  AdminServer->>LeaderAdminProposer: reserve and pin
  LeaderAdminProposer->>InternalLeader: ForwardAdminProposal when follower
  InternalLeader->>RaftGroups: apply admin proposal
  RaftGroups-->>AdminServer: commit indexes
  AdminServer->>ShardStore: capture route snapshot and scan baseline
  AdminServer-->>AdminClient: pin token and expected keys
  AdminClient->>AdminServer: StreamBackup
  AdminServer->>ShardStore: scan at pinned readTS
  ShardStore-->>AdminServer: BackupKV records
  AdminServer-->>AdminClient: stream BackupKV
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed ライブ論理バックアップの管理プレーン追加を端的に表しており、変更内容と一致しています。

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 18, 2026

Copy link
Copy Markdown
Owner Author

Current signed HEAD 1703991 is ready for review.

Safety evidence:

  • renewal replays the complete read_ts pin on every group so a missing replica-local fence is restored
  • the HMAC token carries the committed hard deadline and rotates after each renewal
  • renew, list, and stream reject expired tokens; EndBackup still accepts them for idempotent cleanup
  • BeginBackup reserves the cluster-wide pin slot before fanout and compensates committed groups on partial failure
  • compactor caller chain uses group-scoped backup pins plus process-wide ordinary read pins

Validation evidence is listed in the PR body; post-merge focused unit, targeted race, lint, proto generation, and proto breaking checks all pass.

@codex review
@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

@bootjp Thanks—reviewing the current signed HEAD with particular attention to the backup lifecycle safety invariants and failure/cleanup paths.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gemini-code-assist gemini-code-assist Bot 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 implements a live backup control plane, adding new gRPC endpoints (BeginBackup, RenewBackup, EndBackup, ListAdaptersAndScopes, and StreamBackup) to the admin service, along with key classification, backup scanners, and Raft FSM integration. The review feedback focuses on improving error handling: specifically, ensuring that errors from scanner.Close() are not silently ignored, and that context cancellation or deadline exceeded errors during backup streaming are correctly propagated as gRPC status codes instead of being masked as FailedPrecondition errors.

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 thread adapter/admin_backup.go Outdated
if scanner == nil {
return status.Errorf(codes.Unavailable, "%s", "backup scanner is nil")
}
defer func() { _ = scanner.Close() }()

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.

medium

The error returned by scanner.Close() is silently ignored using _. Discarding close errors on database scanners can mask resource leaks or cleanup failures. It is recommended to log this error to ensure operational visibility.

	defer func() {
		if err := scanner.Close(); err != nil {
			// Log the error to ensure resource leaks are visible
		}
	}()
References
  1. Do not silently ignore errors from Close() methods on resources like network connections. Log them to ensure resource leaks or other cleanup problems are visible.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 2b2c215. Baseline scanner close errors now flow through finishBackupScan and fail the List/Begin path; the new TestListBackupScopesReportsScannerCloseError covers the RPC-visible failure.

Comment thread adapter/admin_backup.go Outdated
if scanner == nil {
return nil, 0, errors.New("backup key scanner is nil")
}
defer func() { _ = scanner.Close() }()

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.

medium

The error returned by scanner.Close() is silently ignored using _. Discarding close errors on database scanners can mask resource leaks or cleanup failures. It is recommended to log this error to ensure operational visibility.

	defer func() {
		if err := scanner.Close(); err != nil {
			// Log the error to ensure resource leaks are visible
		}
	}()
References
  1. Do not silently ignore errors from Close() methods on resources like network connections. Log them to ensure resource leaks or other cleanup problems are visible.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 2b2c215. Stream scanner close errors return Internal after a successful scan; if the scan already failed, the original gRPC status is preserved and the cleanup failure is logged. The new stream test covers the close-error path.

Comment thread adapter/admin_backup.go
Comment on lines +444 to +447
pair, ok, err := scanner.Next(stream.Context())
if err != nil {
return status.Errorf(codes.FailedPrecondition, "scan backup at read_ts: %v", err)
}

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.

medium

In streamBackupRecords, when scanner.Next(stream.Context()) returns an error, it is wrapped in a FailedPrecondition gRPC status code. If the error is due to client-side context cancellation (context.Canceled) or deadline expiration (context.DeadlineExceeded), returning FailedPrecondition is misleading. It is better to check for these context errors and return the corresponding gRPC status codes (Canceled or DeadlineExceeded) to ensure correct error propagation.

Suggested change
pair, ok, err := scanner.Next(stream.Context())
if err != nil {
return status.Errorf(codes.FailedPrecondition, "scan backup at read_ts: %v", err)
}
pair, ok, err := scanner.Next(stream.Context())
if err != nil {
if errors.Is(err, context.Canceled) {
return status.Error(codes.Canceled, err.Error())
}
if errors.Is(err, context.DeadlineExceeded) {
return status.Error(codes.DeadlineExceeded, err.Error())
}
return status.Errorf(codes.FailedPrecondition, "scan backup at read_ts: %v", err)
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in current signed HEAD 2b2c215. scanner.Next now maps context.Canceled and context.DeadlineExceeded to the matching gRPC status codes before the FailedPrecondition fallback. TestStreamBackupPreservesContextStatusAndReportsCloseErrors covers cancellation.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Current signed HEAD 2b2c215 addresses all current review findings.

Evidence:

  • scanner close failures now fail otherwise-successful baseline and stream operations
  • cleanup failure after an existing stream error is logged without masking its gRPC status
  • context cancellation and deadline expiration retain Canceled/DeadlineExceeded
  • go test ./adapter -run 'Test(StreamBackup|ListBackupScopes)' -count=1 passes\n\nCI is running for this HEAD.\n\n@codex review

Comment thread adapter/admin_backup.go
return nil
}

func streamBackupRecords(

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.

🚫 [golangci] reported by reviewdog 🐶
calculated cyclomatic complexity for function streamBackupRecords is 12, max is 10 (cyclop)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53. Stream error normalization is split into backupScanStreamError, backupSendStreamError, and backupContextStreamError, reducing streamBackupRecords below the cyclop limit. Full repository golangci-lint reports 0 issues.

Comment thread adapter/admin_backup.go Outdated
pair, ok, err := scanner.Next(stream.Context())
if err != nil {
if errors.Is(err, context.Canceled) {
return status.Error(codes.Canceled, err.Error())

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.

🚫 [golangci] reported by reviewdog 🐶
error returned from external package is unwrapped: sig: func google.golang.org/grpc/status.Error(c google.golang.org/grpc/codes.Code, msg string) error (wrapcheck)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53. Cancellation and deadline errors now use the shared status.Errorf-based normalizer; repository golangci-lint reports 0 issues.

Comment thread adapter/admin_backup.go Outdated
return status.Error(codes.Canceled, err.Error())
}
if errors.Is(err, context.DeadlineExceeded) {
return status.Error(codes.DeadlineExceeded, err.Error())

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.

🚫 [golangci] reported by reviewdog 🐶
error returned from external package is unwrapped: sig: func google.golang.org/grpc/status.Error(c google.golang.org/grpc/codes.Code, msg string) error (wrapcheck)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53. Cancellation and deadline errors now use the shared status.Errorf-based normalizer; repository golangci-lint reports 0 issues.

@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: 2b2c2152df

ℹ️ 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/admin_backup.go Outdated
if err != nil {
return err
}
scanner := s.backupStore.NewBackupScanner(nil, nil, tok.readTS, s.backupConfig.scanPageSize)

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 StreamBackup on the pinned route view

When a range split/move applies after BeginBackup builds expected_keys but before the producer calls StreamBackup, this creates a fresh scanner from the current route table for the old read_ts. Keys that were still owned only by the old group at the pinned timestamp can be omitted (or a later owner can be scanned instead), so the streamed backup no longer matches the baseline returned by BeginBackup and can silently miss data if the producer does not treat the count mismatch as fatal. Capture the route snapshot/version used for the baseline in the token, or otherwise block/handle route changes while the pin is active.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53. BeginBackup captures one immutable BackupRouteSnapshot before the read fence, uses it for the expected-key baseline, and stores it under the HMAC-authenticated pin ID for List/Stream. StreamBackup constructs its scanner from that exact snapshot; a missing endpoint-local snapshot fails closed. TestBackupScannerReusesCapturedRouteSnapshot and TestStreamBackupFailsClosedWithoutPinnedRouteSnapshot cover both paths.

Comment thread main.go Outdated
}
clock := coordinate.Clock()
clock.Observe(shardStore.LastCommitTS())
readTS, err := clock.NextFenced()

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 after allocating the backup read timestamp

Because readTS is allocated after the all-groups lease read above, a concurrent write can allocate a commit timestamp before this NextFenced() call but only commit/apply after the lease read has completed. That committed version is then <= read_ts and belongs in the point-in-time backup, but the prior fence did not wait for it, so BeginBackup can immediately scan and miss it. Allocate the backup timestamp first and then issue the cluster-wide read/applied barrier for that timestamp (or an equivalent commit-index fence).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53. adminBackupReadFence now observes the local commit watermark and allocates readTS before LeaseReadAllGroupsThrough. TestAdminBackupReadFenceAllocatesTimestampBeforeBarrier records the HLC order and fails if the barrier moves ahead of timestamp issuance.

Comment thread adapter/admin_backup.go Outdated
}

func (s *AdminServer) gateBackupPeerVersions(ctx context.Context) error {
members := s.snapshotMembers(ctx)

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 Fail backup peer gating on incomplete membership

In a multi-node cluster where any Raft Configuration call times out or errors, snapshotMembers intentionally returns a partial member list instead of failing; using that tolerant view for BeginBackup means any node known only to the failed group (and not in the static seed) is never probed for backup_protocol_version. A rolling-upgrade backup can therefore proceed while an old replica is still in the Raft configuration, defeating the capability gate before new backup FSM entries are proposed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53. The backup capability gate now uses snapshotBackupMembers, which requires every registered Raft group's Configuration call to return successfully before any peer probes or FSM proposals. The tolerant snapshotMembers path remains unchanged for GetClusterOverview. TestBeginBackupFailsClosedOnIncompleteMembership covers the failed Configuration case.

Comment thread kv/backup_scan.go Outdated
if err != nil {
return err
}
s.page = append(s.page[:0], keys...)

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 Filter key-only backup pages through captured routes

When old-owner keys remain in a shard after a split/move, the value scanner drops them via materializeRouteForKey, but the count-only scanner accepts every key returned by scanKeyRoutesWithSourceAt. In that scenario BeginBackup/ListAdaptersAndScopes can count scopes from stale off-route data that StreamBackup will not emit, causing otherwise valid backups to fail baseline-count verification or advertise scopes that cannot actually be streamed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53. backupKeyScanner filters each raw page through the same captured-route ownership resolver as the value scanner while advancing its cursor from the raw page, including all-filtered pages. TestBackupKeyScannerFiltersStaleOffRouteKeys covers stale data followed by an owned key across page boundaries.

Comment thread adapter/admin_backup.go Outdated
Comment on lines +474 to +475
if err := stream.Send(&pb.BackupKV{Key: pair.Key, Value: pair.Value}); 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.

P2 Badge Preserve cancellation status from stream sends

If the client cancels or the stream deadline expires after at least one record is ready to send, the error comes from stream.Send, not scanner.Next; wrapping that raw context.Canceled/DeadlineExceeded with errors.WithStack makes status.Code see Unknown, while only scanner errors are normalized above. Handle context errors from Send the same way before wrapping so canceled backup streams retain the expected gRPC status.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53. Errors from both scanner.Next and stream.Send pass through backupContextStreamError, preserving Canceled and DeadlineExceeded. TestStreamBackupPreservesContextStatusAndReportsCloseErrors now covers a DeadlineExceeded send failure; normal and race backup suites pass.

Comment thread internal/backup/live.go
):
return scopeForS3Key(key)
case hasAnyBackupPrefix(key,
SQSQueueMetaPrefix, SQSQueueGenPrefix, SQSMsgDataPrefix, SQSQueueSeqPrefix,

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 Parse or skip SQS queue sequence rows in live streams

When the producer enables IncludeSQSSideRecords for a live SQS backup, classifying SQSQueueSeqPrefix as a scoped stream record causes StreamBackup to emit !sqs|queue|seq|<queue> rows, but the live decoder routes that prefix through routeSQSSide/HandleSideRecord; with side records enabled, HandleSideRecord calls parseSQSGenericKey, which requires a generation/trailer and rejects bare queue-sequence keys as malformed. A FIFO queue with a sequence row can therefore make the live decode fail; either exclude this prefix from live scope streaming or add a sequence-specific side-record parser.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c9baa53 without dropping the FIFO sequence counter. SQSEncoder.HandleSideRecord now selects parseSQSQueueSequenceKey for the bare queue-sequence shape and keeps the generic parser for generation-bearing side rows. TestLiveDecoderAcceptsSQSQueueSequenceSideRecord exercises the live decoder with IncludeSQSSideRecords enabled.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Current signed HEAD c9baa53 addresses every finding from the previous-head review.\n\nSafety evidence:\n- BeginBackup captures one immutable route view and baseline/List/Stream reuse it; missing endpoint-local state fails closed.\n- read_ts is issued before the all-groups applied barrier.\n- backup membership capability gating requires every Raft Configuration call to succeed; the overview RPC remains tolerant.\n- key-only pages apply the same captured-route ownership filter as value scans.\n- scanner and send cancellation preserve gRPC Canceled/DeadlineExceeded.\n- SQS FIFO sequence side rows use a dedicated bare-key parser and remain restorable.\n\nCaller audit:\n- adminBackupReadFence has one production caller in Admin option wiring.\n- snapshotBackupMembers is used only by the backup capability gate; snapshotMembers remains on GetClusterOverview.\n- captured-snapshot scanner APIs are confined to live-backup control and scanner tests.\n\nValidation:\n- go test ./adapter -run Backup -count=1\n- go test -race ./adapter -run Backup -count=1\n- targeted root/kv/internal-backup/adapter tests\n- targeted root/kv/internal-backup/adapter race tests\n- golangci-lint run ./... --timeout=5m (0 issues)\n- go test ./... passed every package except the pre-existing adapter multi-node/election suite, which hit its 10-minute package timeout; all backup adapter tests pass independently.\n\nCI is running for this HEAD.\n\n@codex review\n@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

@bootjp Thanks for the detailed safety and validation evidence. I’ll review the current signed HEAD, focusing on the revised route snapshot invariants, read timestamp ordering, capability-gate failure handling, cancellation status preservation, and SQS FIFO side-row restoration.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

🤖 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/admin_backup.go`:
- Around line 350-355: After renewBackupGroups succeeds but
requireUnexpiredBackupToken fails, release the renewed pin’s group fences and
capacity reservations before returning the token error. Update the error path in
the surrounding RPC method to perform compensating cleanup for the renewed pin,
while preserving the existing renewal error handling. Add a test covering token
expiry after renewal and verify all renewed resources are released.
🪄 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

Run ID: ab9aa49b-0f99-4fe1-b4f5-b5e9a869e938

📥 Commits

Reviewing files that changed from the base of the PR and between e8c0cb2 and c9baa53.

⛔ Files ignored due to path filters (2)
  • proto/admin.pb.go is excluded by !**/*.pb.go
  • proto/admin_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (18)
  • adapter/admin_backup.go
  • adapter/admin_backup_test.go
  • adapter/admin_grpc.go
  • internal/backup/live.go
  • internal/backup/live_test.go
  • internal/backup/s3.go
  • internal/backup/sqs.go
  • kv/active_timestamp_tracker.go
  • kv/backup_codec.go
  • kv/backup_codec_test.go
  • kv/backup_scan.go
  • kv/compactor_test.go
  • kv/fsm_backup.go
  • kv/fsm_backup_test.go
  • kv/shard_store_test.go
  • main.go
  • main_admin_test.go
  • proto/admin.proto

Comment thread adapter/admin_backup.go
@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Current signed HEAD 693592b addresses the current-head renewal cleanup finding.

Evidence:

  • expiry after successful renewal now compensates all group pins and the capacity reservation
  • endpoint-local route state is removed on that fail-closed path
  • the new clock-controlled regression test verifies reserve/pin/release/unreserve ordering and session removal
  • focused unit and race tests pass
  • all adapter backup tests pass
  • adapter golangci-lint reports 0 issues

CI is running for this HEAD.

@codex review
@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Current commit =="
git rev-parse HEAD
git status --short

echo "== Backup implementation outline =="
ast-grep outline adapter/admin_backup.go --items all --type function,method,struct,interface --match 'Renew|expire|cleanup|Backup|session|propose|release|unreserve' || true

echo "== Relevant implementation symbols =="
rg -n -C 5 -i 'func \(s \*AdminServer\) RenewBackup|expire|cleanup.*backup|backupSessions|unreserve|release.*backup|proposeBackupAll|proposeBackupWithRetry' adapter/admin_backup.go

echo "== Backup test outline =="
ast-grep outline adapter/admin_backup_test.go --items all --type function --match 'Renew|Deadline|expire|cleanup|Backup' || true

echo "== Relevant regression tests =="
rg -n -C 8 -i 'clock|expiry|expire|renew.*(cleanup|deadline)|unreserve|session removal|backupSessions' adapter/admin_backup_test.go

Length of output: 21431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Renew / End paths =="
sed -n '328,395p' adapter/admin_backup.go

echo "== Fanout, compensation, and renewal retry paths =="
sed -n '714,872p' adapter/admin_backup.go

echo "== Session and token/group lookup paths =="
sed -n '910,960p' adapter/admin_backup.go
sed -n '1113,1165p' adapter/admin_backup.go

echo "== Test proposer behavior =="
sed -n '32,155p' adapter/admin_backup_test.go

echo "== Renewal-expiry regression test =="
sed -n '514,586p' adapter/admin_backup_test.go

echo "== Relevant backup configuration initialization =="
sed -n '89,153p' adapter/admin_backup.go
sed -n '227,256p' adapter/admin_backup_test.go

Length of output: 20750


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Begin and baseline lifecycle =="
sed -n '184,328p' adapter/admin_backup.go

echo "== All renewal-helper call sites =="
rg -n -C 6 '\brenewBackupGroups\s*\(' --glob '*.go'

echo "== Tests for renewal pin-fanout failure / cleanup =="
rg -n -i -C 8 'renew.*(fail|error|retry|compensat|release|unreserve)|group pins|capacity reservation' adapter/admin_backup_test.go

echo "== Backup tracker semantics for duplicate reserve/pin/release =="
ast-grep outline kv/active_timestamp_tracker.go --items all --type function,method,struct --match 'Backup|Pin|Reserve|Release|Unreserve' || true
rg -n -C 5 'Reserve|Unreserve|PinWithDeadline|ReleaseBackupPin|backup' kv/active_timestamp_tracker.go

Length of output: 6435


🏁 Script executed:

#!/bin/bash
set -u

echo "== Exact renewal helper references =="
rg -n -F -C 5 'renewBackupGroups(' --glob '*.go' || true

echo "== Active timestamp tracker outline =="
ast-grep outline kv/active_timestamp_tracker.go --items all --type function,method,struct --match 'Backup|Pin|Reserve|Release|Unreserve' || true

echo "== Active timestamp tracker backup methods =="
rg -n -i -C 6 'backup|reserve|unreserve|pinwithdeadline|releasebackuppin' kv/active_timestamp_tracker.go || true

echo "== Existing renewal failure test contexts =="
rg -n -i -C 12 'TestRenew.*(Retry|Fail|Release|Cleanup)|renew backup pin|group pins|capacity reservation' adapter/admin_backup_test.go || true

Length of output: 853


@bootjp I found one remaining renewal cleanup gap.

adapter/admin_backup.go, Line 350: if renewBackupGroups successfully commits the renewed capacity Reserve but exhausts retries while applying the renewed Pin entries, it returns an error. RenewBackup immediately maps that to Unavailable on Line 351 without calling compensateBackupRelease.

That leaves the control-group reservation at its new deadline, plus any groups whose pin proposal did commit during the partially failed fanout. Since the client receives an error—and its existing token may expire—the capacity slot and retention fences can persist until expiry rather than being fail-closed immediately.

Please compensate this error path as well (release all token groups, then unreserve the control group, and remove the endpoint-local session), and add a regression test that injects a pin-fanout failure after Reserve succeeds and asserts Release → Unreserve.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

elastickv/kv/backup_scan.go

Lines 372 to 374 in 693592b

if write > 0 && bytes.Equal(out[write-1].key, item.key) {
out[write-1] = item
continue

P1 Badge Prefer owned duplicate keys before trimming

When a page contains the same raw key from the current owner and from a later-scanned stale/off-route group, such as after a range split where the old group still has a copy, this dedupe overwrites the owned item with the later duplicate before routeForRoutedKey filters ownership. The stale item is then rejected and the live key is omitted from both expected counts and StreamBackup. Filter or rank by captured route ownership before collapsing duplicates.

ℹ️ 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 main.go Outdated
Comment on lines +2044 to +2046
clock := coordinate.Clock()
clock.Observe(shardStore.LastCommitTS())
readTS, err := clock.NextFenced()

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 shard groups before minting read_ts

When BeginBackup runs on a node whose local shard store has not yet applied a write that already committed on another raft-group leader, this mints readTS before the LeaseReadAllGroupsThrough call below. That lease read can then pull in the completed write with a commit timestamp greater than readTS, so the subsequent pinned scan omits data that was already committed before the backup began. The fence should first bring all groups up to date, then observe the resulting max LastCommitTS/allocate a timestamp above it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in bd8d262. The all-groups lease/read barrier now completes before LastCommitTS is observed and read_ts is minted. TestAdminBackupReadFenceAllocatesTimestampAfterBarrier applies a commit during the barrier and proves the returned timestamp is greater.

Comment thread adapter/admin_backup.go Outdated
Comment on lines +255 to +257
routes := s.backupStore.CaptureBackupRouteSnapshot(nil, nil)

readTS, err := s.backupReadFence(ctx)

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 Capture route snapshot after the read fence

When a route split or rebalance commits between this route snapshot and backupReadFence, the returned readTS can include writes routed with the new catalog while later scans are forced to the older routes captured here. A key committed on the new group before readTS is then never scanned even though it belongs in the backup. Capture the route view after the fence, or bind it to a catalog version that is also fenced by readTS.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in bd8d262. prepareBackup now captures the immutable route snapshot only after the read fence succeeds and validates its timestamp. TestBeginBackupCapturesRouteSnapshotAfterReadFence pins the ordering.

Comment thread adapter/admin_backup.go
Comment on lines +703 to +704
if server.Address == "" {
continue

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 Fail closed on members without probe addresses

When a live Raft configuration contains a server with an empty Address and there is no seed entry to backfill it, this branch records the ID as seen but drops it from live.order, so snapshotBackupMembers never returns it and gateBackupPeerVersions never probes it. A dynamically added or metadata-lagging old node can then receive backup FSM entries without proving BackupProtocolVersion >= 1, defeating the rolling-upgrade safety gate. Return FailedPrecondition for blank live addresses instead of continuing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in bd8d262. Live member IDs without a configuration or seed address are added to the deterministic validation order, so BeginBackup returns FailedPrecondition before proposing any FSM entry. TestBeginBackupFailsClosedOnMemberWithoutProbeAddress covers this path.

Comment thread main.go Outdated
for _, rt := range runtimes {
srv.RegisterGroup(rt.spec.id, rt.engine)
if group := shardGroups[rt.spec.id]; group != nil {
srv.RegisterBackupProposer(rt.spec.id, group.Proposer())

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 Route backup proposals to group leaders

In multi-group deployments where leadership is spread across nodes, registering group.Proposer() wires backup pin proposals to the local raft engine for each group. The etcd engine's ProposeAdmin is still a local proposal path and returns not-leader on followers, so BeginBackup fails unless the chosen admin endpoint is leader for every raft group. Use a leader-proxying proposer for these backup FSM entries instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in bd8d262. Backup ProposeAdmin calls now use a leader-routed proposer. Followers forward the raw admin payload over Internal.ForwardAdminProposal; the verified leader applies it through its local wrap-aware ShardGroup.Proposer. Local and follower forwarding tests cover both paths.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Current signed HEAD bd8d262 addresses every current-head finding.

Evidence:

  • captured-route dedupe now preserves the captured owner when a later stale/off-route group returns the same raw key; key-only and value scanners are both covered
  • the all-group read fence now completes before observing LastCommitTS and minting read_ts; route capture follows the successful fence
  • live members without probe addresses fail BeginBackup before any Raft proposal
  • backup admin proposals are leader-routed over Internal gRPC and applied through the leader wrap-aware proposer
  • failed renewal fanout releases all group pins, unreserves the control slot, and removes the endpoint session

Caller audit:

  • adminBackupReadFence has one production wiring site
  • mergeAndTrimRoutedScanKeys has one scanner merge caller
  • RegisterBackupProposer is consumed only through backup ProposeAdmin paths
  • RenewBackup is the only RPC caller whose failure cleanup semantics changed; BeginBackup and its renewal loop retain their existing compensation

Validation:

  • go test ./... -count=1 -timeout=600s
  • go test -race ./adapter ./kv ./internal/backup . -count=1 -timeout=600s
  • golangci-lint --config=.golangci.yaml run --fix
  • golangci-lint run . ./adapter/... ./kv/... ./internal/backup/... --timeout=5m --allow-parallel-runners
  • make -C proto gen (includes buf breaking and generation)
  • git diff --check

@codex review

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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 (1)
adapter/internal_admin_proposal_test.go (1)

63-82: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

テーブル駆動テストへのリファクタリング

As per coding guidelines, unit tests should be co-located with packages in *_test.go files; prefer table-driven test cases. 類似したテストケースをテーブル駆動形式にまとめることを推奨します。

♻️ 提案するリファクタリング
 func TestInternalForwardAdminProposalFailsClosed(t *testing.T) {
 	t.Parallel()
-	t.Run("follower", func(t *testing.T) {
-		internal := NewInternalWithEngine(nil, internalAdminLeaderView{state: raftengine.StateFollower}, nil, nil)
-		_, err := internal.ForwardAdminProposal(context.Background(), &pb.ForwardAdminProposalRequest{})
-		require.ErrorIs(t, err, ErrNotLeader)
-	})
-	t.Run("apply response", func(t *testing.T) {
-		proposer := &internalAdminProposer{response: stderrors.New("apply failed")}
-		internal := NewInternalWithEngine(
-			nil,
-			internalAdminLeaderView{state: raftengine.StateLeader},
-			nil,
-			nil,
-			WithInternalAdminProposer(proposer),
-		)
-		_, err := internal.ForwardAdminProposal(context.Background(), &pb.ForwardAdminProposalRequest{})
-		require.ErrorContains(t, err, "apply failed")
-	})
+	tests := []struct {
+		name       string
+		leaderView internalAdminLeaderView
+		proposer   raftengine.Proposer
+		wantErr    string
+		errTarget  error
+	}{
+		{
+			name:       "follower",
+			leaderView: internalAdminLeaderView{state: raftengine.StateFollower},
+			proposer:   nil,
+			errTarget:  ErrNotLeader,
+		},
+		{
+			name:       "apply response",
+			leaderView: internalAdminLeaderView{state: raftengine.StateLeader},
+			proposer:   &internalAdminProposer{response: stderrors.New("apply failed")},
+			wantErr:    "apply failed",
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			internal := NewInternalWithEngine(
+				nil,
+				tt.leaderView,
+				nil,
+				nil,
+				WithInternalAdminProposer(tt.proposer),
+			)
+			_, err := internal.ForwardAdminProposal(context.Background(), &pb.ForwardAdminProposalRequest{})
+			if tt.errTarget != nil {
+				require.ErrorIs(t, err, tt.errTarget)
+			}
+			if tt.wantErr != "" {
+				require.ErrorContains(t, err, tt.wantErr)
+			}
+		})
+	}
 }
🤖 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/internal_admin_proposal_test.go` around lines 63 - 82, Refactor
TestInternalForwardAdminProposalFailsClosed into a table-driven test covering
the follower and apply-response failure scenarios. Define each case with its
setup and expected error assertion, then iterate with t.Run while preserving the
existing ErrNotLeader and “apply failed” checks.

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 `@kv/backup_scan.go`:
- Around line 393-401: Update preferredRoutedScanKey so that when current and
candidate have the same ownership state, it returns current to preserve the
stable ordering from the preceding sort.SliceStable. Keep returning current when
only currentOwned is true, and return candidate only when candidate is owned
while current is not.

---

Nitpick comments:
In `@adapter/internal_admin_proposal_test.go`:
- Around line 63-82: Refactor TestInternalForwardAdminProposalFailsClosed into a
table-driven test covering the follower and apply-response failure scenarios.
Define each case with its setup and expected error assertion, then iterate with
t.Run while preserving the existing ErrNotLeader and “apply failed” checks.
🪄 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

Run ID: c7c9afca-f492-4391-b91d-516cb75aa5c6

📥 Commits

Reviewing files that changed from the base of the PR and between c9baa53 and bd8d262.

⛔ Files ignored due to path filters (2)
  • proto/internal.pb.go is excluded by !**/*.pb.go
  • proto/internal_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (11)
  • adapter/admin_backup.go
  • adapter/admin_backup_test.go
  • adapter/internal.go
  • adapter/internal_admin_proposal_test.go
  • kv/backup_scan.go
  • kv/leader_admin_proposer.go
  • kv/leader_admin_proposer_test.go
  • kv/shard_store_test.go
  • main.go
  • main_admin_test.go
  • proto/internal.proto
🚧 Files skipped from review as they are similar to previous changes (2)
  • main.go
  • adapter/admin_backup.go

Comment thread kv/backup_scan.go
@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Current signed HEAD f5d9e2f addresses the latest review.

  • duplicate keys now retain stable route order when ownership states tie and select the candidate only when it is the sole captured owner
  • ambiguous enumerated full-range routes have a deterministic first-route contract
  • the Internal admin proposal failure cases are table-driven as requested

Validation:

  • go test ./kv -run TestBackup(ScannerMaterializesFromEnumeratedRoute|ScannersPreferCapturedOwnerForDuplicateKey) -count=1
  • go test ./adapter -run TestInternalForwardAdminProposal -count=1
  • go test -race ./adapter ./kv -run Test(InternalForwardAdminProposal|BackupScannerMaterializesFromEnumeratedRoute|BackupScannersPreferCapturedOwnerForDuplicateKey) -count=1
  • golangci-lint run ./adapter ./kv --timeout=5m --allow-parallel-runners
  • pre-commit full golangci-lint --fix: 0 issues

@codex review

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Signed HEAD f5d9e2f is unchanged. All required CI checks are green, merge state is clean, and the current-head CodeRabbit review has no new findings. Please complete the latest-head review. @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: f5d9e2f42e

ℹ️ 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/fsm_backup.go
if applyErr == nil && f.hlc != nil {
// The pin entry is also the per-group timestamp barrier. Once it
// applies, later writes on this replica must issue above read_ts.
f.hlc.Observe(readTS)

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 Enforce the backup timestamp barrier for stamped writes

When a raw write has already been timestamped but is still waiting in the normal commit path (for example in TransactionManager's raw batching window), a BeginBackup pin can be proposed and applied before that write's Raft entry. This line only advances the HLC for future timestamp allocations; it does not reject or restamp an already-stamped request that applies later with Ts <= read_ts, so a live scan at read_ts can observe a non-atomic cut under concurrent writes.

Useful? React with 👍 / 👎.

Comment thread main.go Outdated
return 0, errors.Wrap(err, "backup: fence raft groups")
}
clock.Observe(shardStore.LastCommitTS())
readTS, err := clock.NextFenced()

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 backup reads through the active timestamp allocator

When --tsoEnabled is in use, writes consume timestamps from the coordinator's BatchAllocator, but the backup fence bypasses that allocator and calls the underlying HLC directly. If the batch allocator already has unused timestamps reserved below the newly returned readTS, later writes can still commit below the backup timestamp after the pin is installed, breaking the snapshot guarantee; the fence needs to coordinate with or invalidate the same allocator that production writes use.

Useful? React with 👍 / 👎.

Comment thread adapter/internal.go
Comment on lines +86 to +89
func (i *Internal) ForwardAdminProposal(
ctx context.Context,
req *pb.ForwardAdminProposalRequest,
) (*pb.ForwardAdminProposalResponse, 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.

P1 Badge Require auth before forwarding admin proposals

In deployments where the raft gRPC listener is reachable by anything other than trusted peers, this new Internal/ForwardAdminProposal method bypasses the Admin bearer-token gate: startRaftServers registers Internal on the same gRPC server, while AdminTokenAuth only checks methods under /Admin/. Because Payload is arbitrary bytes, a caller can submit backup/encryption admin opcodes directly to the leader and reserve/release pins or halt apply without an Admin token.

Useful? React with 👍 / 👎.

Comment thread adapter/internal.go
if err != nil {
return nil, errors.WithStack(err)
}
if err := forwardedAdminProposalResponseError(result); 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 Preserve apply errors across admin forwarding

When the group leader is remote, any FSM/application error from ProposeAdmin is returned here as a normal gRPC error and the sentinel is lost. For example, a remote kv.ErrTooManyActiveBackups from the reservation apply path no longer satisfies errors.Is in pinBackupGroups, so BeginBackup retries/returns Unavailable instead of the deterministic ResourceExhausted response that the local leader path produces.

Useful? React with 👍 / 👎.

Comment thread kv/backup_scan.go
}
s.page = s.page[:0]
for _, item := range keys {
if _, ok := routeForRoutedKey(item, s.routes); ok {

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 partition-routed SQS keys in backup scans

When backing up an SQS HT-FIFO queue whose partition resolver routes partitioned message keys to a non-default group, this filter drops those records: routeForRoutedKey only checks distribution routes using routeKey, while production ShardRouter.ResolveGroup consults the partition resolver before routeKey. The scanner does visit every group, but records from partition-only groups fail this ownership test and are neither counted nor streamed, so live backups of partitioned queues are incomplete.

Useful? React with 👍 / 👎.

Comment thread main.go
}
return func(ctx context.Context) (uint64, error) {
clock := coordinate.Clock()
if err := kv.LeaseReadAllGroupsThrough(coordinate, ctx); 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 Forward the read fence instead of requiring local leaders

In a multi-group deployment where this admin endpoint is follower for any Raft group, BeginBackup fails here before the leader-forwarding proposers are used: LeaseReadAllGroupsThrough on ShardedCoordinator calls each local engine's LinearizableRead, and the etcd engine rejects reads when State != Leader. After leader balancing, no node may lead every group, making the live backup RPC unusable unless callers happen to choose an all-leader endpoint.

Useful? React with 👍 / 👎.

Base automatically changed from design/live-backup-admin-api to design/live-backup-pin-substrate July 19, 2026 06:07
@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Updated the signed HEAD to 830d645475ba651c32e43f5a65dfb1226909cf3f from the refreshed pin-substrate base.

Conflict-resolution evidence:

  • retained the captured route snapshot for backup scans
  • adopted filesystem forward-scan route normalization without consulting the live route catalog during pinned materialization
  • regenerated protobuf outputs from the merged source definitions
  • focused kv, adapter, and root-package tests pass
  • proto breaking/generation checks and full lint pass
  • current-head CI is green

Please review 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: 830d645475

ℹ️ 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 main.go Outdated
coordinate.Clock(),
relay,
internalTimestampOptions(coordinate)...,
append(internalTimestampOptions(coordinate), adapter.WithInternalAdminProposer(proposerForGroup(rt, shardGroups)))...,

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 Gate internal admin proposal forwarding

When the node gRPC listener is reachable by normal clients, this wires ForwardAdminProposal on /Internal unconditionally, even when the Admin service is disabled or protected by a bearer token; the existing admin interceptor only checks /Admin methods. That lets any caller submit arbitrary ProposeAdmin payloads (for example long-lived backup pins with low read_ts, or malformed control entries) to the Raft leader. Please gate this option on the same admin authentication/enablement path or add equivalent auth/validation to the internal RPC.

Useful? React with 👍 / 👎.

Comment thread adapter/admin_backup.go
s.groupsMu.RUnlock()
return nil, status.Errorf(codes.Unavailable, "backup proposer unavailable for raft group %d", id)
}
groups = append(groups, backupGroup{id: id, reader: group, status: group.Status(), every: group.SnapshotEvery(), proposer: proposer})

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 Exclude the reserved TSO group from backups

In deployments that include the reserved TSO Raft group 0, setupAdminService registers every runtime and this loop appends group 0 as a backup group. The resulting token later fails to encode because group IDs of 0 are rejected, and group 0 would also collide with the group-zero capacity-reservation tracker entry. This makes BeginBackup fail after fan-out instead of backing up the data groups, so the reserved/non-data group should be filtered before pinning/tokenization.

Useful? React with 👍 / 👎.

Comment thread adapter/admin_backup.go Outdated
if readTS == 0 || readTS == ^uint64(0) {
return preparedBackup{}, status.Errorf(codes.FailedPrecondition, "%s", "backup read fence returned an invalid timestamp")
}
routes := s.backupStore.CaptureBackupRouteSnapshot(nil, 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 Capture routes at the backup timestamp

When a route split or move applies after backupReadFence returns read_ts but before this call, the token stores the post-change route set while scans still read MVCC data at the older timestamp. Keys that were owned by the old group at read_ts can then be dispatched/materialized from the new captured owner, or have the old copy filtered out, so the live backup can omit data. Capture a route view that is valid at read_ts (or freeze route changes through the pin) instead of reading the live catalog here.

Useful? React with 👍 / 👎.

Comment thread adapter/admin_backup.go
}
deadline := s.nowSnapshot().Add(ttl)
controlGroup := groups[0]
commits, err := s.pinBackupGroups(ctx, groups, controlGroup, pinID, readTS, 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 Drain pre-stamped writes before scanning

If a client write allocates commit_ts < readTS in the gap between the read fence and this backup-pin fan-out, but its Raft entry applies after the pin or during the later scan, the pin's HLC Observe(readTS) only affects future timestamp allocations and does not reject that already-stamped entry. Pages read at readTS can therefore miss or inconsistently include the write depending on whether it has applied before that key is scanned. BeginBackup needs to quiesce/drain already timestamped proposals, or otherwise wait until all entries with timestamps <= readTS have applied, before building the baseline or streaming.

Useful? React with 👍 / 👎.

Comment thread adapter/admin_backup.go
) (map[uint64]uint64, error) {
reserveEntry := kv.EncodeBackupReserveEntry(kv.BackupReserveEntry{PinID: pinID, ReadTS: readTS, Deadline: deadline})
if _, _, err := proposeBackupAll(ctx, []backupGroup{controlGroup}, reserveEntry); err != nil {
if errors.Is(err, kv.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.

P2 Badge Preserve capacity errors across forwarded reserves

When BeginBackup is handled by a follower for the control group, the reserve proposal goes through ForwardAdminProposal; the leader returns ErrTooManyActiveBackups as a gRPC error, so this local errors.Is check no longer matches and the client gets Unavailable instead of ResourceExhausted. That makes capacity exhaustion look retryable and bypasses the API contract tested for local proposals; map the internal RPC error/status back to the sentinel or ResourceExhausted before this check.

Useful? React with 👍 / 👎.

Comment thread kv/fsm_backup.go
switch entry.subtype {
case backupSubtypePin:
err = f.readTracker.ApplyPinWithDeadlineForGroup(entry.pin.PinID, f.shardGroupID, entry.pin.ReadTS, entry.pin.Deadline)
err := f.readTracker.ApplyPinWithDeadlineForGroup(entry.pin.PinID, f.shardGroupID, entry.pin.ReadTS, entry.pin.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 Persist backup pins through Raft snapshots

When a follower installs a Raft snapshot while a backup is active, the BackupPin entry may already be compacted out of the log, but this apply path only records the pin in the in-memory ActiveTimestampTracker; the FSM snapshot/restore path does not carry those pins. The restored replica can therefore run compaction without the read_ts fence until a later renewal reapplies it, and if it becomes the group leader in that window it can serve backup pages with versions below read_ts already removed. Persist active pins in snapshots or block snapshot installation/compaction while pins are live.

Useful? React with 👍 / 👎.

Comment thread adapter/admin_backup.go
if scanner == nil {
return status.Errorf(codes.Unavailable, "%s", "backup scanner is nil")
}
scanErr := streamBackupRecords(stream, scanner, selected)

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 Stop streaming once the backup pin expires

For streams that last past tok.deadline when the client fails to renew, this starts the paging loop after only the upfront expiry check and never revalidates the session deadline. Once the tracker sweeper reaps the pin, FSM compaction can advance below tok.readTS while the RPC keeps reading pages, so a slow dump can silently become incomplete instead of failing when the pin is no longer valid. Recheck the deadline around each page/send or bind the stream lifetime to a live renewed pin.

Useful? React with 👍 / 👎.

@bootjp
bootjp force-pushed the design/live-backup-pin-substrate branch from 13d22a8 to 26d45ba Compare July 19, 2026 11:08
@bootjp
bootjp force-pushed the design/live-backup-control-plane branch from 830d645 to 6a85349 Compare July 19, 2026 11:09
@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

History-only author-compliance repair completed at 6a85349d2deec210799349b420d35c9563666e55.

  • final tree is byte-identical to prior head 830d645475ba651c32e43f5a65dfb1226909cf3f
  • 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 sanitized Add live backup pin FSM substrate #1056 head 26d45baebb34ae6afbfaf39394e62deafcddc481

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

@codex review

@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Current reconstructed head 6a85349d2deec210799349b420d35c9563666e55 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

@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Latest signed head 6a85349d2deec210799349b420d35c9563666e55 remains mergeable with all required checks green and no current-head root findings. @codex review

@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Dependency #1056 has reached latest-head no-findings at 26d45baebb34ae6afbfaf39394e62deafcddc481. Current #1119 head 6a85349d2deec210799349b420d35c9563666e55 remains fully green with zero current-head root findings. Please review this head now. @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: 6a85349d2d

ℹ️ 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/admin_backup.go Outdated
Comment thread adapter/internal.go
Comment thread main.go
Comment thread main.go Outdated
@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 (f30bd74):

  • distribution/engine.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.

@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Evidence for current HEAD f30bd74b5e64064d6a846ac640cf35d639a9d03b:

  • Backup route snapshots are captured from the durable distribution catalog at the fenced readTS.
  • Per-group read barriers forward to each shard leader through authenticated Internal RPCs.
  • Internal admin proposal forwarding is covered by the Admin bearer-token gate and is disabled when the Admin service is disabled.
  • Backup timestamps are allocated through the active TSO-aware coordinator after all-group fencing.
  • Caller audit covered route capture, all-group lease reads, timestamp allocators, forwarded admin proposals, and all coordinator decorators.
  • All four current review threads were replied to and resolved.
  • PR-visible author is bootjp <contact@bootjp.me> and GitHub reports the new commit signature as verified.

Validation:

  • make gen
  • go test ./adapter ./kv ./distribution . -count=1 -timeout=300s
  • focused race tests for route capture, auth forwarding, follower read barriers, and TSO fencing
  • golangci-lint run ./adapter ./kv ./distribution . --timeout=5m --allow-parallel-runners (0 issues)
  • pre-commit full repository lint (0 issues)
  • git diff --check

Requesting a fresh latest-head review.

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

ℹ️ 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/backup_scan.go Outdated
Comment thread main.go
Comment thread adapter/admin_grpc.go
Comment thread adapter/admin_backup.go
@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 (85e98d7):

  • distribution/engine.go
  • 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.

@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Current signed HEAD 85e98d736480612bc71dba11ae4ca84685f5e676 addresses all four current-head findings.

Safety evidence:

  • backup routes are captured from the runtime catalog-owner CatalogStore, after every data-group pin/floor commits
  • replicated backup pins persist a monotone per-group timestamp floor; delayed raw, one-phase, and PREPARE writes at or below the cut fail closed
  • pre-pin COMMIT/ABORT remains resolvable, so the scanner either includes committed data or fails BeginBackup on a pending primary
  • applying the floor advances HLC state and invalidates each process-local cached TSO window once per cut
  • reserved TSO group 0 is excluded from backup fan-out and token encoding
  • forwarded lease-read RPCs require and carry the configured admin bearer token

Caller/semantic audit:

  • requestCommitTS has one production consumer, kvFSM.applyRequestErr; every raw and transaction phase passes through the floor check
  • raw/NONE/PREPARE/COMMIT/ABORT constructors and handlers were audited; only COMMIT/ABORT deliberately bypass the stale-write rejection
  • coordinator and Internal timestamp callers route through the same coordinator allocator, so TSO-window invalidation covers both paths
  • runtime catalog mutations route through the coordinator/FSM; direct CatalogStore.Save is bootstrap-only

Validation:

  • focused unit/integration tests across ./kv ./adapter .: pass
  • the same focused set under -race: pass
  • go test ./adapter -run Backup -count=1: pass
  • related main admin/backup tests: pass
  • go test ./... -run ^$: pass
  • golangci-lint run ./kv ./adapter . --timeout=5m --allow-parallel-runners: 0 issues
  • full adapter package run reached the existing 10-minute multi-node/election timeout; all changed backup/auth/timestamp paths pass independently

@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: 85e98d7364

ℹ️ 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/admin_backup.go
Comment on lines +921 to +925
scope, scoped, err := logicalbackup.ScopeForKey(key)
if err != nil {
return nil, 0, errors.Wrap(err, "classify backup key baseline")
}
if scoped {

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 Fail backups on prepared insert-only transactions

When a transaction prepares an insert before the backup pin, ScanKeysAt only exposes the txn lock/intent records because the user key has no visible version yet; this loop classifies those internal keys as unscoped and continues. Since COMMIT deliberately bypasses the timestamp floor, that transaction can commit later with commit_ts <= read_ts, but the new user key was neither counted nor streamed, so the backup can silently miss committed data instead of failing BeginBackup on the pending primary.

Useful? React with 👍 / 👎.

Comment thread main.go
Comment on lines +2493 to +2498
if err := kv.LeaseReadAllGroupsThrough(coordinate, ctx); err != nil {
return 0, errors.Wrap(err, "backup: fence raft groups")
}
lastCommitTS := shardStore.LastCommitTS()
clock.Observe(lastCommitTS)
readTS, err := allocateBackupReadTimestamp(ctx, coordinate, lastCommitTS)

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 Allocate backup read_ts after remote leader timestamps

When BeginBackup is served by a node whose local replica is behind another shard leader, this only observes the local stores' LastCommitTS() after LeaseReadAllGroups. The lease read fences leader applied indexes, but it does not fetch the leaders' highest commit timestamps, and non-TSO forwarded writes are stamped on those leaders; the backup can therefore choose read_ts below a write that completed before the fence and then omit that committed version from the scan.

Useful? React with 👍 / 👎.

Comment thread main.go
Comment on lines +2403 to +2404
snapshot, err := kv.CaptureBackupRouteSnapshotAt(ctx, s.catalog, ts)
return snapshot, errors.Wrap(err, "capture backup route snapshot")

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 Read catalog snapshots from the fenced owner

On an admin endpoint that is a follower for the distribution-catalog group, this reads the process-local CatalogStore directly. The earlier pin/proposal fan-out returns when the leader applies, not when this local follower has caught up, so a route update committed at or before read_ts can be missing from the captured snapshot; the baseline and stream then scan the wrong historical owner for that key range.

Useful? React with 👍 / 👎.

Comment thread kv/fsm_backup.go
var ErrBackupApply = errors.New("backup fsm apply failed")
var ErrBackupTimestampFenced = errors.New("backup timestamp fence rejects stale write")

var backupTimestampFloorKey = []byte(TxnKeyPrefix + "backup|timestamp_floor")

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 Reserve the backup floor key from user writes

This new durable floor lives at !txn|backup|timestamp_floor, but isTxnInternalKey only rejects the existing lock/intent/commit/rollback/meta prefixes, so RawKV callers can write this exact key. After a restart, reloadBackupTimestampFloor interprets the user value as the replicated fence: a non-8-byte value halts apply, while an arbitrary 8-byte value can raise the floor and make normal writes fail as stale.

Useful? React with 👍 / 👎.

Comment thread internal/backup/live.go
Comment on lines +36 to +40
case hasAnyBackupPrefix(key, DDBTableMetaPrefix, DDBTableGenPrefix, DDBItemPrefix, DDBGSIPrefix):
return scopeForDDBKey(key)
case hasAnyBackupPrefix(key,
S3BucketMetaPrefix, S3BucketGenPrefix, S3ObjectManifestPrefix,
S3UploadMetaPrefix, S3UploadPartPrefix, S3BlobPrefix, S3GCUploadPrefix, S3RoutePrefix,

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 Drop generation counters from live scopes

When a DynamoDB table or S3 bucket has been deleted before read_ts, its generation counter can intentionally remain even though the resource is no longer user-visible. Including the generation prefixes in scope discovery makes ListAdaptersAndScopes and expected_keys report phantom resources, and StreamBackup can emit only operational generation records that the decoder later drops.

Useful? React with 👍 / 👎.

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