Skip to content

Add physical snapshot object publication#1133

Open
bootjp wants to merge 2 commits into
mainfrom
feature/physical-snapshot-object-m1
Open

Add physical snapshot object publication#1133
bootjp wants to merge 2 commits into
mainfrom
feature/physical-snapshot-object-m1

Conversation

@bootjp

@bootjp bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a strict versioned physical snapshot manifest and immutable payload-first, manifest-last publication protocol
  • add an S3-compatible conditional object store with SHA-256 metadata, checksum headers, and mandatory SSE-S3 or SSE-KMS
  • add verified no-clobber download plus offline publish, download, and restore commands
  • round-trip a real EKVSSTI1 payload through the SST ingest receiver contract

Safety

  • stages and verifies exact length, SHA-256, and source CRC32C before any object publication
  • validates remote object length, kind, checksum metadata, and encryption before committing a manifest
  • checks expected cluster and group identity before fetching payload bytes
  • creates restore downloads with 0600, fsync, and an atomic no-replace link in the destination directory
  • uses only the AWS credential provider chain; explicit HTTP endpoints require the local-testing override

Dependencies

Validation

  • go test ./internal/snapshotoffload ./cmd/elastickv-physical-snapshot -count=1 -timeout=20m
  • go test -race ./internal/snapshotoffload ./cmd/elastickv-physical-snapshot -count=1 -timeout=20m
  • all-package compile with zero tests selected
  • golangci-lint run ./... --timeout=10m --allow-parallel-runners
  • git diff --check

Author: bootjp

Summary by CodeRabbit

  • 新機能

    • S3互換オブジェクトストアを利用した物理スナップショットの公開・ダウンロード・復元CLIを追加。
    • スナップショットのマニフェスト管理、整合性検証、暗号化設定に対応。
    • AWS認証情報、HTTPSエンドポイント、SSE-S3/SSE-KMSを利用可能。
    • 復元前にクラスター・グループ情報を検証し、投票者と学習者を正しく反映。
  • ドキュメント

    • 物理スナップショットのオブジェクトオフロード設計と運用要件を更新。
  • テスト

    • 公開、取得、復元、競合、破損データ、暗号化設定の検証を追加。

@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Current head: 14056d6226387a79d457064853470a8ac268439a

Evidence:

  • author/committer: bootjp <contact@bootjp.me>
  • GitHub signature verification: valid
  • rebased onto current origin/main after Add physical snapshot export substrate #1131 merged
  • payload publication validates exact bytes, SHA-256, and source CRC32C before payload-first/manifest-last commit
  • download validates manifest identity before payload GET and uses a no-clobber same-directory commit
  • real EKVSSTI1 payload round-trips through object publication, fresh Raft preparation, and the existing SST ingest receiver

Validation:

  • focused normal tests: pass
  • focused race tests: pass
  • all-package compile: pass
  • target and full golangci-lint: 0 issues
  • git diff --check: pass

Please review the immutable object commit protocol, manifest trust boundary, conditional S3 semantics, download filesystem safety, and offline restore flow.

@codex review

@coderabbitai

coderabbitai Bot commented Jul 19, 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: 1 second

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20e867e2-011e-49ac-af46-19fa0af978da

📥 Commits

Reviewing files that changed from the base of the PR and between 14056d6 and ce379ce.

📒 Files selected for processing (15)
  • cmd/elastickv-physical-snapshot/main.go
  • cmd/elastickv-physical-snapshot/main_test.go
  • cmd/elastickv-physical-snapshot/operations.go
  • docs/design/2026_06_12_proposed_scaling_roadmap.md
  • docs/design/2026_07_19_partial_physical_snapshot_object_offload.md
  • internal/raftengine/etcd/persisted_snapshot_export.go
  • internal/raftengine/etcd/persisted_snapshot_export_test.go
  • internal/snapshotoffload/download.go
  • internal/snapshotoffload/manifest.go
  • internal/snapshotoffload/manifest_test.go
  • internal/snapshotoffload/publisher.go
  • internal/snapshotoffload/publisher_test.go
  • internal/snapshotoffload/s3_store.go
  • internal/snapshotoffload/s3_store_test.go
  • internal/snapshotoffload/store.go
📝 Walkthrough

Walkthrough

物理スナップショットをマニフェストとペイロードに分離し、S3互換オブジェクトストアへ検証付きで公開・取得・復元する機能を追加した。CLIにはpublish、download、restoreモード、SSE検証、クラスター整合性確認、復元処理を実装した。

Changes

物理スナップショットオフロード

Layer / File(s) Summary
マニフェストとストア契約
internal/snapshotoffload/manifest.go, internal/snapshotoffload/store.go, internal/snapshotoffload/manifest_test.go
マニフェストの型、正規化、厳格なJSON検証、Raft構成変換、オブジェクトストアの共通契約を追加した。
スナップショット公開フロー
internal/snapshotoffload/publisher.go, internal/snapshotoffload/publisher_test.go
ペイロードをステージングして整合性を検証し、条件付き公開、競合検出、冪等リトライ、マニフェスト生成を実装した。
検証付きダウンロード
internal/snapshotoffload/download.go, internal/snapshotoffload/publisher_test.go
マニフェスト整合性、SHA256、CRC32C、末尾データ、宛先競合を検証し、テンポラリファイルから原子的に配置する処理を追加した。
S3オブジェクトストア
internal/snapshotoffload/s3_store.go, internal/snapshotoffload/s3_store_test.go, go.mod
AWS SDK v2を使うS3ストアを追加し、Head、Get、条件付きPut、SSE、KMS、チェックサム、S3エラー分類を実装した。
CLI設定と操作統合
cmd/elastickv-physical-snapshot/*
publish、download、restoreのCLI、S3設定検証、復元ピア解析、JSON出力、物理スナップショット復元を追加した。
設計更新
docs/design/2026_07_19_proposed_physical_snapshot_object_offload.md
M1の実装状況とCLI、SSE、識別子検証、受け入れテスト要件を更新した。

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

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Publisher
  participant ObjectStore
  participant Downloader
  participant Restore
  CLI->>Publisher: publishモードでスナップショットを公開
  Publisher->>ObjectStore: ペイロードとマニフェストを条件付き保存
  CLI->>Downloader: downloadモードでマニフェストを指定
  Downloader->>ObjectStore: マニフェストとペイロードを取得
  Downloader-->>CLI: 検証済みファイルを返却
  CLI->>Restore: restoreモードで取得済みペイロードを処理
  Restore-->>CLI: 復元用DataDirとFSMPathを返却
Loading

Possibly related PRs

  • bootjp/elastickv#1131: operations.go が提供する永続スナップショットのエクスポートおよび復元APIと直接連携している。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 物理スナップショットのオブジェクト公開機能追加を端的に表しており、変更内容の主題と一致しています。
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.

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.

@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 introduces the elastickv-physical-snapshot command-line utility and its supporting snapshotoffload package to enable publishing, downloading, and restoring Raft/FSM snapshots using S3-compatible object storage. Feedback on the changes highlights a critical issue in the membership validation logic that incorrectly rejects valid joint consensus states where a node ID appears in both voters and outgoing voters. Additionally, the reviewer recommended using deferred Close() calls in download.go and publisher.go to prevent resource leaks during panics and to ensure that errors during cleanup are properly logged or propagated.

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 internal/snapshotoffload/manifest.go Outdated
Comment thread internal/snapshotoffload/download.go Outdated
Comment thread internal/snapshotoffload/publisher.go Outdated

@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

Caution

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

⚠️ Outside diff range comments (1)
docs/design/2026_07_19_proposed_physical_snapshot_object_offload.md (1)

143-154: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

ファイル名とヘッダーを partial に揃えてください
M0/M1 は完了済みで、M2/M3 は未完了です。docs/design/README.md の定義ではこの段階は *_partial_*.md に該当するため、2026_07_19_partial_physical_snapshot_object_offload.mdgit mv し、Status: Partial に更新してください。

🤖 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 `@docs/design/2026_07_19_proposed_physical_snapshot_object_offload.md` around
lines 143 - 154, Rename the design document with git mv to
2026_07_19_partial_physical_snapshot_object_offload.md and update its header
status to Partial, reflecting completed M0/M1 and pending M2/M3. Update every
reference, including docs/design/README.md references if applicable, and verify
no references to the old proposed filename remain.

Source: Coding guidelines

🧹 Nitpick comments (1)
cmd/elastickv-physical-snapshot/operations.go (1)

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

必須フィールド検証ロジックが3箇所で重複し、mapのイテレーション順序が非決定的

parsePublishFlagsvalidateDownloadConfigparseRestoreFlags で同じ「map[string]string を作って required を呼ぶ」パターンが重複しています。Goの map イテレーション順序は非決定的なため、複数の必須フラグが同時に欠落した場合、実行ごとに異なるエラーメッセージが返る可能性があります。

順序付きの []struct{name, value string} を使う共通ヘルパーに抽出することで、重複を解消し、エラーメッセージを決定的にできます。

♻️ 修正案(例)
+func requireAll(fields ...[2]string) error {
+	for _, f := range fields {
+		if err := required(f[0], strings.TrimSpace(f[1])); err != nil {
+			return err
+		}
+	}
+	return nil
+}

呼び出し側:

-	for name, value := range map[string]string{
-		"data-dir":   cfg.dataDir,
-		"cluster-id": cfg.clusterID,
-		"prefix":     cfg.prefix,
-	} {
-		if err := required(name, strings.TrimSpace(value)); err != nil {
-			return nil, err
-		}
-	}
+	if err := requireAll(
+		[2]string{"data-dir", cfg.dataDir},
+		[2]string{"cluster-id", cfg.clusterID},
+		[2]string{"prefix", cfg.prefix},
+	); err != nil {
+		return nil, err
+	}

Also applies to: 170-178, 273-282

🤖 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 `@cmd/elastickv-physical-snapshot/operations.go` around lines 97 - 105, Extract
the duplicated required-field validation from parsePublishFlags,
validateDownloadConfig, and parseRestoreFlags into a shared helper accepting an
ordered []struct{name, value string}. Preserve each caller’s existing field
order and required validation behavior so the first missing field and resulting
error are deterministic.
🤖 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 `@cmd/elastickv-physical-snapshot/main.go`:
- Around line 63-68: Update the required function to create its validation error
with the file’s existing github.com/cockroachdb/errors package instead of
fmt.Errorf, preserving the current message and required-value behavior.

---

Outside diff comments:
In `@docs/design/2026_07_19_proposed_physical_snapshot_object_offload.md`:
- Around line 143-154: Rename the design document with git mv to
2026_07_19_partial_physical_snapshot_object_offload.md and update its header
status to Partial, reflecting completed M0/M1 and pending M2/M3. Update every
reference, including docs/design/README.md references if applicable, and verify
no references to the old proposed filename remain.

---

Nitpick comments:
In `@cmd/elastickv-physical-snapshot/operations.go`:
- Around line 97-105: Extract the duplicated required-field validation from
parsePublishFlags, validateDownloadConfig, and parseRestoreFlags into a shared
helper accepting an ordered []struct{name, value string}. Preserve each caller’s
existing field order and required validation behavior so the first missing field
and resulting error are deterministic.
🪄 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: 31fd3fc0-015b-4441-8d13-770c2e174df2

📥 Commits

Reviewing files that changed from the base of the PR and between dafb8d2 and 14056d6.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • cmd/elastickv-physical-snapshot/config.go
  • cmd/elastickv-physical-snapshot/main.go
  • cmd/elastickv-physical-snapshot/main_test.go
  • cmd/elastickv-physical-snapshot/operations.go
  • docs/design/2026_07_19_proposed_physical_snapshot_object_offload.md
  • go.mod
  • internal/snapshotoffload/download.go
  • internal/snapshotoffload/manifest.go
  • internal/snapshotoffload/manifest_test.go
  • internal/snapshotoffload/publisher.go
  • internal/snapshotoffload/publisher_test.go
  • internal/snapshotoffload/s3_store.go
  • internal/snapshotoffload/s3_store_test.go
  • internal/snapshotoffload/store.go

Comment thread cmd/elastickv-physical-snapshot/main.go

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14056d6226

ℹ️ 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 cmd/elastickv-physical-snapshot/operations.go
Comment thread internal/snapshotoffload/s3_store.go Outdated
Comment thread internal/snapshotoffload/s3_store.go
Comment thread internal/snapshotoffload/publisher.go
Comment thread internal/snapshotoffload/publisher.go Outdated
@bootjp

bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Current head: ce379ceff5eea15bb7aa14928fd74c5ff6a755b1

Addressed all nine root findings from the prior head:

  • valid joint-consensus ConfState overlap
  • fail-closed body/file Close handling
  • consistent and deterministic CLI required-field validation
  • legacy inline snapshot CRC32C
  • S3 409 retries and >5 GiB multipart upload
  • exact KMS-key validation
  • byte rehash before payload reuse

Additional design status work:

  • renamed the design document from proposed to partial
  • updated its status and roadmap reference
  • confirmed zero references to the old path

Semantic caller audit:

  • OpenPersistedSnapshotExport callers and tests audited for the newly populated legacy CRC
  • PutIfAbsent callers audited for 409/retry and existing-object semantics
  • all validateRemoteObject call paths audited for KMS enforcement
  • ConfState validation callers remain manifest encode/decode boundaries
  • CLI required-field validation callers retain their existing order

Validation:

  • go test ./internal/snapshotoffload ./internal/raftengine/etcd ./cmd/elastickv-physical-snapshot -count=1: pass
  • go test -race ./internal/snapshotoffload ./cmd/elastickv-physical-snapshot: pass
  • targeted legacy inline CRC race test in internal/raftengine/etcd: pass
  • go test ./... -run '^$': pass
  • full golangci-lint run ./... --allow-parallel-runners: 0 issues
  • git diff --check: pass
  • GitHub signature verification: valid; author/committer are bootjp <contact@bootjp.me>

The full go test -race ./internal/raftengine/etcd suite was attempted but did not complete cleanly in existing full-suite Raft tests; the changed legacy CRC path passes both focused normal and targeted race coverage.

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

ℹ️ About Codex in GitHub

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

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

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

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

return errors.Wrapf(ErrRemoteIntegrity, "key=%s kind=%q expected=%q", key, metadataKind, kind)
case encryption != "" && info.Encryption != encryption:
return errors.Wrapf(ErrRemoteIntegrity, "key=%s encryption=%q expected=%q", key, info.Encryption, encryption)
case encryption == EncryptionKMS && strings.TrimSpace(info.KMSKeyID) != strings.TrimSpace(kmsKeyID):

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 Canonicalize KMS keys before exact comparison

When operators pass a KMS alias or bare key id to --kms-key-id (values S3 accepts for SSE-KMS uploads), S3 can report the canonical key identifier/ARN on HEAD/GET; this exact string comparison then rejects the object after a successful publish, and later downloads fail with ErrRemoteIntegrity, even though the requested key was used. Either require a canonical ARN/key ID at validation time or resolve aliases before comparing.

Useful? React with 👍 / 👎.

s3DefaultMultipartPart = int64(64 * 1024 * 1024)
s3MaxMultipartPart = int64(5 * 1024 * 1024 * 1024)
s3MaxMultipartParts = int64(10_000)
s3MaxObjectBytes = int64(5 * 1024 * 1024 * 1024 * 1024)

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 S3's full multipart object size

For physical snapshots above 5 TiB, multipartPartSize returns ErrObjectInvalid before trying multipart upload, but the current S3 multipart limits allow substantially larger objects with up to 10,000 parts (AWS docs). Since the code already scales part size up to the 5 GiB part limit, this hard cap unnecessarily blocks valid large snapshot uploads; raise the object limit to match S3 or make it backend-configurable.

Useful? React with 👍 / 👎.

}
if prior, ok := seen[id]; ok {
jointVoter := prior == "voters" && names[i] == "voters_outgoing"
if !jointVoter && prior != "learners_next" && names[i] != "learners_next" {

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 Reject invalid LearnersNext overlaps

When a manifest has learners_next containing an incoming voter/learner, this condition exempts the duplicate solely because one side is learners_next, so voters:[1], learners_next:[1] passes DecodeManifest as a validated snapshot witness. In etcd joint consensus, LearnersNext is only for outgoing voters being demoted, so accepting these impossible ConfStates makes corrupted or hand-written manifests look valid instead of failing closed.

Useful? React with 👍 / 👎.

Comment on lines +104 to +108
if _, ok := learners[peers[i].ID]; ok {
peers[i].Suffrage = etcd.SuffrageLearner
} else {
peers[i].Suffrage = etcd.SuffrageVoter
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Reject all-learner restore membership before download

If --learners names every peer, this loop marks the whole restore membership as learners and returns success, so runRestore proceeds to fetch the snapshot from S3 and only later fails inside PreparePhysicalSnapshotRestore with “at least one voter is required”. For large physical snapshots this turns a locally detectable flag error into an unnecessary full download; count voters here and fail before contacting the object store.

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