Skip to content

snapshot: harden object offload validation#1133

Merged
bootjp merged 4 commits into
mainfrom
feature/physical-snapshot-object-m1
Jul 23, 2026
Merged

snapshot: harden object offload validation#1133
bootjp merged 4 commits into
mainfrom
feature/physical-snapshot-object-m1

Conversation

@bootjp

@bootjp bootjp commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • rebase the physical snapshot object-offload PR onto current main after the M1 substrate merged
  • add strict manifest ConfState validation, including joint-consensus allowances and invalid learners_next rejection
  • require S3 server-side encryption, reject mutable KMS aliases, and verify encryption/KMS metadata on HEAD/GET
  • retry S3 409 conditional conflicts and use multipart upload above the single-PUT limit

Validation

  • go test ./internal/snapshotoffload ./cmd/elastickv-snapshot-offload -count=1
  • go test ./internal/snapshotoffload ./internal/raftengine/etcd ./cmd/elastickv-snapshot-offload -count=1
  • go test ./... -run '^$'
  • golangci-lint run ./internal/snapshotoffload ./cmd/elastickv-snapshot-offload --timeout=5m
  • commit hook: golangci-lint --config=.golangci.yaml run --fix

Author: bootjp

Summary by CodeRabbit

  • 新機能
    • 大容量オブジェクトのS3マルチパートアップロードに対応しました。
    • S3のサーバーサイド暗号化(AES256/AWS KMS)を指定・検証できるようになりました。
    • 保存済みオブジェクトの暗号化情報を確認できるようになりました。
  • 改善
    • スナップショットマニフェストの検証をより厳格化し、無効な構成を拒否します。
    • 条件付き書き込み競合は自動再試行し、解消しない場合は明確なエラーを返します。
  • テスト
    • S3暗号化、マルチパート挙動、マニフェスト検証の追加テストを実施しました。

@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: 31 minutes

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 Plus

Run ID: 35d0f5ab-b5df-4c31-8d6a-a727e25658c7

📥 Commits

Reviewing files that changed from the base of the PR and between fdf5574 and 7db43c8.

📒 Files selected for processing (2)
  • internal/snapshotoffload/s3_store.go
  • internal/snapshotoffload/s3_store_test.go
📝 Walkthrough

Walkthrough

マニフェストのConfState検証を拡張し、S3保存に条件付きリトライとマルチパート処理を追加した。SSE/KMS情報をObjectInfoへ反映し、暗号化設定の検証をCLI、保存、取得処理へ統合した。

Changes

スナップショットオフロード検証と保存

Layer / File(s) Summary
マニフェスト検証の分割とConfState制約
internal/snapshotoffload/manifest.go, internal/snapshotoffload/offload_test.go
payload、identity、ConfStateの検証を分離し、ロール間重複、learners_next の所属、auto_leave のjoint consensus条件を検証するテストを追加した。
S3契約と暗号化設定
internal/snapshotoffload/store.go, internal/snapshotoffload/s3_store.go
ObjectInfo にSSE/KMS情報を追加し、マルチパートAPI、SSE設定検証、KMSキーID検証を追加した。
条件付き・マルチパート保存
internal/snapshotoffload/s3_store.go, internal/snapshotoffload/s3_store_test.go
大きなオブジェクトをマルチパートで保存し、条件付き競合時にリトライし、失敗時にアップロードを中止する処理とテスト用実装を追加した。
S3取得結果の暗号化検証
internal/snapshotoffload/s3_store.go, internal/snapshotoffload/s3_store_test.go
Put、Get、Head、既存オブジェクト検証でSSE方式とKMSキー識別子を検証し、暗号化メタデータを返すようにした。
CLIのS3暗号化設定統合
cmd/elastickv-snapshot-offload/main.go, cmd/elastickv-snapshot-offload/main_test.go
--s3-sse の既定値をAES256に変更し、S3暗号化設定の検証を共通関数へ委譲した。

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

Sequence Diagram(s)

sequenceDiagram
  participant SnapshotOffloadCLI
  participant S3Store
  participant S3ObjectClient
  participant S3
  SnapshotOffloadCLI->>S3Store: S3暗号化設定を検証
  S3Store->>S3ObjectClient: 条件付きPutObjectまたはマルチパート保存
  S3ObjectClient->>S3: オブジェクトを書き込む
  S3->>S3ObjectClient: オブジェクト情報と暗号化メタデータを返す
  S3ObjectClient->>S3Store: 保存結果を返す
  S3Store->>S3Store: SSE/KMSとチェックサムを検証
Loading

Possibly related PRs

  • bootjp/elastickv#1151: 同じマニフェスト検証、ObjectInfo、S3暗号化処理に関連する変更。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.14% 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 物理 snapshot の offload に対する検証強化を端的に表しており、変更内容の主題と一致しています。
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 Outdated

@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 Outdated
Comment thread internal/snapshotoffload/s3_store.go Outdated
Comment thread internal/snapshotoffload/s3_store.go Outdated
Comment thread internal/snapshotoffload/publisher.go Outdated
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".

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

Comment thread internal/snapshotoffload/s3_store.go Outdated
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 👍 / 👎.

Comment thread internal/snapshotoffload/manifest.go Outdated
}
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 👍 / 👎.

@bootjp
bootjp force-pushed the feature/physical-snapshot-object-m1 branch from ce379ce to b9cdf7e Compare July 23, 2026 06:05
@bootjp bootjp changed the title Add physical snapshot object publication snapshot: harden object offload validation Jul 23, 2026
@bootjp

bootjp commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Current head: b9cdf7e09d4b6dd668eb8aed25475e9802e06558

Rebased onto current main after #1151 merged and replaced the old duplicate implementation with a focused hardening diff against the current M1 substrate.

Addressed current safety gaps:

  • manifest ConfState validation now rejects impossible learners/learners_next overlaps while allowing valid joint-consensus voter overlap
  • S3 store now requires SSE-S3 or SSE-KMS, rejects KMS aliases, and validates returned encryption/KMS metadata on reads and writes
  • S3 conditional 409 conflicts are retried instead of being treated as an existing object
  • payloads above the S3 single-PUT limit use multipart upload with abort-on-failure

Validation:

  • go test ./internal/snapshotoffload ./cmd/elastickv-snapshot-offload -count=1: pass
  • go test ./internal/snapshotoffload ./internal/raftengine/etcd ./cmd/elastickv-snapshot-offload -count=1: pass
  • go test ./... -run '^$': pass
  • golangci-lint run ./internal/snapshotoffload ./cmd/elastickv-snapshot-offload --timeout=5m: 0 issues
  • commit hook lint: 0 issues
  • author/committer: bootjp <contact@bootjp.me>; Git signature valid

@codex review

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

🤖 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 `@internal/snapshotoffload/manifest.go`:
- Around line 129-146: Update validateManifestConfState to reject duplicate IDs
within each individual role list, while preserving the existing zero-ID and
cross-role validation. Track IDs separately for each role during iteration and
return ErrInvalidOptions with the role name when a duplicate is found.

In `@internal/snapshotoffload/s3_store.go`:
- Around line 217-306: Update createMultipartUpload and uploadParts to honor
s.disableChecksumHeaders consistently with the single-PUT path: omit
ChecksumAlgorithm and ChecksumSHA256 fields when disabled, including
CompletedPart.ChecksumSHA256, while retaining SHA-256 checksum calculation and
headers when enabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0faa4fd3-65a5-43b4-a7a6-5486cf9b2cf8

📥 Commits

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

📒 Files selected for processing (7)
  • cmd/elastickv-snapshot-offload/main.go
  • cmd/elastickv-snapshot-offload/main_test.go
  • internal/snapshotoffload/manifest.go
  • internal/snapshotoffload/offload_test.go
  • internal/snapshotoffload/s3_store.go
  • internal/snapshotoffload/s3_store_test.go
  • internal/snapshotoffload/store.go

Comment thread internal/snapshotoffload/manifest.go
Comment thread internal/snapshotoffload/s3_store.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: b9cdf7e09d

ℹ️ 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 internal/snapshotoffload/s3_store.go Outdated
input := &s3.CreateMultipartUploadInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,

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 Honor disabled checksum headers in multipart uploads

When --s3-disable-checksum-headers is set and a payload exceeds the single-PUT limit, this multipart path still sends checksum headers because CreateMultipartUpload always sets ChecksumAlgorithm here and UploadPart also unconditionally sends SHA256 checksums. That preserves the failure mode the flag is meant to avoid for S3-compatible stores without checksum-header support, so large snapshots can fail even though the user explicitly disabled those headers.

Useful? React with 👍 / 👎.

Comment thread internal/snapshotoffload/s3_store.go Outdated
Comment on lines +277 to +278
sum := sha256.Sum256(part)
checksum := base64.StdEncoding.EncodeToString(sum[:])

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 Verify the full multipart stream hash

For multipart uploads, this only hashes each part for S3 and never compares an accumulated full-object hash with opts.SHA256 before completing the upload. If the source file changes or is corrupted between spooling and this upload path, S3 accepts the same-length parts and the later HEAD check trusts the pre-written metadata SHA, so publishing a >5 GiB snapshot can succeed while storing bytes that restore will later reject.

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 Raise the multipart object cap

This new hard cap rejects any snapshot payload above 5 TiB before upload, but current S3 multipart limits allow up to 10,000 parts of 5 GiB each, i.e. 48.8 TiB (AWS docs). In deployments with very large persisted FSM snapshots, offload now returns ErrInvalidOptions for valid S3 objects instead of choosing a larger part size and proceeding.

Useful? React with 👍 / 👎.

Comment thread internal/snapshotoffload/s3_store.go Outdated
Comment on lines +286 to +287
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
ChecksumSHA256: aws.String(checksum),

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 Handle S3 multipart checksum metadata on reads

For real AWS S3 multipart uploads, sending per-part SHA256 checksums here makes S3 expose a composite SHA256 checksum on GetObject when the SDK enables checksum mode. s3ChecksumSHA expects a plain full-object base64 digest, so restoring a >5 GiB object uploaded by this path can fail while parsing the returned checksum before the payload body is verified.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Current head: fdf55743f0892e5b922a1070d60dd87f4bfdfbc5

Addressed the current snapshot offload review findings:

  • reject duplicate ConfState node IDs within each role
  • honor disabled checksum headers on multipart create/upload/complete
  • verify the full multipart source SHA-256 before completing upload
  • prefer stored full-object SHA metadata over S3 multipart composite checksum values on reads
  • raise the multipart size cap to the configured S3 part capacity

Validation:

  • go test ./internal/snapshotoffload -count=1
  • golangci-lint run ./internal/snapshotoffload --timeout=5m
  • go test ./... -run '^$'\n\n@codex review

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fdf55743f0

ℹ️ 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 internal/snapshotoffload/s3_store.go Outdated
remaining := totalBytes
for partNumber := int32(1); remaining > 0; partNumber++ {
partBytes := min(remaining, partSize)
part := make([]byte, int(partBytes))

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 Avoid buffering maximum-size multipart parts

For snapshots above about 640 GiB, multipartPartSize grows partBytes beyond the 64 MiB default to stay under 10,000 parts; at the newly accepted S3 limit it reaches 5 GiB. This allocation loads each part fully into RAM before every UploadPart, so a snapshot that passes validation can OOM the offload process before upload completes; stream/hash a section of the spooled file instead of allocating partBytes.

Useful? React with 👍 / 👎.

Comment thread internal/snapshotoffload/s3_store.go Outdated
Comment on lines +609 to +610
if metadataSHA != "" {
return metadataSHA, 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 Keep validating full-object checksums

When an existing S3 object has both our SHA metadata and a real full-object S3 checksum, this early return trusts the user-controlled metadata and never checks whether the S3 checksum disagrees. In that case verifyExistingStoreObject can reuse an overwritten/corrupt payload without downloading it, publish a manifest for the expected SHA, and leave restore to fail later; only composite multipart checksum values should be ignored, not every checksum whenever metadata is present.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@codex review

Addressed the latest snapshot offload review findings in 7cea249:

  • multipart upload no longer allocates a full part buffer; seekable sources are hashed and uploaded through section readers, and the real snapshot payload caller uses a spooled *os.File
  • full-object S3 checksum values are validated against SHA metadata, while COMPOSITE multipart checksum values remain ignored in favor of metadata

Caller audit:

  • snapshot payload PutObject uses the spooled *os.File
  • manifest PutObject uses bytes.Reader
  • both are seekable for multipart checksum headers

Validation:

  • go test ./internal/snapshotoffload -run 'TestS3(StoreUsesMultipartForLargeObject|StoreMultipartStreamsSeekableParts|StoreMultipartHonorsDisabledChecksumHeaders|StoreMultipartRejectsSourceHashMismatch|ObjectSHA256PrefersMetadataForCompositeMultipartChecksum|ObjectSHA256RejectsMetadataFullObjectChecksumMismatch|MultipartPartSizeAllowsS3PartCapacity)' -count=1
  • go test ./internal/snapshotoffload -count=1
  • golangci-lint run ./internal/snapshotoffload --timeout=5m
  • go test ./... -run '^$'
  • git diff --check

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

ℹ️ 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 internal/snapshotoffload/s3_store.go
Comment thread internal/snapshotoffload/s3_store.go Outdated
s3DefaultMultipartPart = int64(64 * 1024 * 1024)
s3MaxMultipartPart = int64(5 * 1024 * 1024 * 1024)
s3MaxMultipartParts = int64(10_000)
s3MaxObjectBytes = s3MaxMultipartPart * s3MaxMultipartParts

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 Cap multipart uploads at S3's object limit

For snapshots larger than S3's 5 TiB object limit but below this computed 50 TiB ceiling, multipartPartSize accepts the input and the store can upload thousands of parts before CompleteMultipartUpload fails because the final object is too large. That turns an unsupported snapshot size into a very expensive late failure; cap this constant at the service object limit instead of deriving it from max part size times max part count.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Evidence for current HEAD 7db43c8:

Addressed the latest snapshot offload review findings:

  • HeadObject/GetObject now request S3 checksum mode before consuming returned checksum fields.
  • The fake S3 client only returns checksum fields when ChecksumMode is enabled, so tests cover the real AWS response contract.
  • Full-object checksum mismatch is rejected for both HeadObject and GetObject once checksum mode is requested.
  • Multipart upload sizing is capped at S3's 5 TiB object limit instead of the theoretical 10,000 x 5 GiB part capacity.

Validation:

  • go test ./internal/snapshotoffload -count=1
  • golangci-lint --config=.golangci.yaml run ./internal/snapshotoffload --timeout=5m --allow-parallel-runners
  • go test ./... -run '^$' -count=1
  • git diff --check
  • commit signature verified locally for bootjp contact@bootjp.me

Resolved the remaining current checksum-mode review thread. Current-head CI is running after the push.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 7db43c8f3e

ℹ️ About Codex in GitHub

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

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

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

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

@bootjp
bootjp merged commit efa96e0 into main Jul 23, 2026
9 checks passed
@bootjp
bootjp deleted the feature/physical-snapshot-object-m1 branch July 23, 2026 15:15
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