Skip to content

feat(encryption): Stage 6D-5 - storage-layer cutover toggle (WithStorageEnvelopeGate)#809

Merged
bootjp merged 3 commits into
mainfrom
feat/encryption-6d5-storage-toggle
May 23, 2026
Merged

feat(encryption): Stage 6D-5 - storage-layer cutover toggle (WithStorageEnvelopeGate)#809
bootjp merged 3 commits into
mainfrom
feat/encryption-6d5-storage-toggle

Conversation

@bootjp
Copy link
Copy Markdown
Owner

@bootjp bootjp commented May 23, 2026

Summary

  • Stage 6D-5 lands the §6.2 storage-layer toggle that the Stage 6D-4 sidecar's StorageEnvelopeActive field gates. encryptForKey now consults the new WithStorageEnvelopeGate closure in addition to the existing activeStorageKeyID, so operators get the §7.1 Phase 0 / Phase 1 split: every node has the DEK provisioned before the cluster quiesces the cutover Raft entry that flips the bit.
  • Add StorageEnvelopeActive func() bool and WithStorageEnvelopeGate option (store/encryption_glue.go). Nil argument is a no-op (legacy "encrypt whenever DEK is active" posture), matching the pre-existing WithEncryption convention.
  • Read path is unchanged — on-disk encryption_state == 0b01 versions always go through the cipher regardless of the gate, so mixed cleartext + encrypted versions for the same key stay readable across the cutover (design doc §6.3).

Operator-inert until 6D-6 wires the cipher + gate in main.go and exposes the EnableStorageEnvelope RPC.

Test plan

  • go test -race -timeout 180s ./store/... ./internal/encryption/... ./kv/... — all green
  • golangci-lint --new-from-rev=origin/main — 0 issues
  • TestStorageEnvelopeGate_PreCutoverForcesCleartext — gate=false with cipher + active DEK wired → on-disk encryption_state = 0b00
  • TestStorageEnvelopeGate_PostCutoverEmitsEnvelope — gate=true → on-disk encryption_state = 0b01
  • TestStorageEnvelopeGate_FlipMidLife — pre-cutover write, flip gate, post-cutover write; both reads succeed live, on-disk bits actually differ (pins §6.3 mixed-mode-per-key)
  • TestStorageEnvelopeGate_NilGatePreservesLegacyBehaviornewEncryptedStoreFixture (no gate) still emits envelopes; pre-6D-5 posture unchanged
  • TestStorageEnvelopeGate_NilArgumentIsNoOpWithStorageEnvelopeGate(nil) does not nil-deref on the next Put

Caller audit (semantic change)

The new option is purely additive; existing WithEncryption call sites are not touched. encryptForKey's existing fast-path callers see the gate skipped when the field is nil (the default), so this PR cannot regress any pre-existing test or production code path.

  • All current store.WithEncryption call sites are in store/lsm_store_encryption{,_prop}_test.go (5 sites). None set WithStorageEnvelopeGate; all retain the legacy posture.
  • main.go does NOT currently call store.WithEncryption (storage-layer encryption wiring lands in 6D-6 alongside the cutover RPC), so this PR is operator-inert.

Self-review (5 lenses)

  1. Data loss — no. encryptForKey's only behavioural change is one additional path to encStateCleartext when the gate is wired AND returns false. The Pebble write itself is unchanged (same encodeValue + Set path).
  2. Concurrency / distributed failures — the gate closure is read once per Put and is plain field-read on the caller's view. No new locking; no new races. Read path is untouched. Replicas flip StorageEnvelopeActive deterministically at FSM apply (proved by the 6D-4 tests), so no inter-replica skew window.
  3. Performance — one extra branch + one closure call per Put when wired; nil-check skips both when not wired. No allocations.
  4. Data consistency — the §6.3 mixed-mode invariant (cleartext + encrypted versions for the same key) is exercised by TestStorageEnvelopeGate_FlipMidLife. The §4.1 AAD binding for envelope writes is unchanged.
  5. Test coverage — 5 new tests cover (pre-cutover cleartext, post-cutover envelope, mid-life flip, nil-gate legacy posture, nil-argument no-op). Existing TestEncryption_* tests continue to pass with no modification.

Refs: design doc §6.2 (Stage 6D-5 row of §7 decomposition table).

Summary by CodeRabbit

  • New Features

    • Added a storage envelope gate to control whether writes emit encrypted or cleartext data. Reads remain unaffected regardless of gate state.
  • Documentation

    • Updated design documentation to reflect shipped enhancements to the storage layer.
  • Tests

    • Added comprehensive tests for storage envelope gate behavior including pre-cutover, post-cutover, mid-life toggles, and backward compatibility scenarios.

Review Change Stack

…ageEnvelopeGate)

Stage 6D-5 lands the §6.2 storage-layer toggle that the
Stage 6D-4 sidecar's StorageEnvelopeActive field gates. The
storage layer's encryptForKey path now consults the new toggle
in addition to the existing activeStorageKeyID closure, so
operators get the §7.1 Phase 0 / Phase 1 split: every node has
the DEK provisioned (Phase 0) before the cluster quiesces the
cutover Raft entry that flips the bit (Phase 1).

Operator-inert until 6D-6 wires the cipher + gate in main.go
and exposes the EnableStorageEnvelope RPC.

## Wire-up (store/encryption_glue.go)

- New StorageEnvelopeActive type: func() bool. Doc spells out
  that this is the §7.1 Phase 1 cutover signal, distinct from
  ActiveStorageKeyID's "is a DEK provisioned" signal, and that
  the gate is consulted on writes only - reads always dispatch
  on the per-version encryption_state bit (so mixed cleartext +
  encrypted versions for the same key stay readable across the
  cutover, per design doc §6.3).
- New WithStorageEnvelopeGate PebbleStoreOption. Nil argument is
  a no-op (legacy "encrypt whenever DEK is active" posture)
  matching the pre-existing convention from WithEncryption.
- encryptForKey adds a single AND-gate: if the gate is wired
  AND returns false, force encStateCleartext even though a DEK
  is active. The branch sits AFTER the existing
  (cipher == nil || activeStorageKeyID == nil) early-return so
  the legacy fast paths are unaffected.

## Tests (store/lsm_store_encryption_test.go)

- gatedFixture + newGatedEncryptedStore helper bundles the gate
  wiring with idempotent close-then-on-disk-peek (Pebble panics
  on double-close).
- TestStorageEnvelopeGate_PreCutoverForcesCleartext - gate
  returns false, cipher + active DEK both wired - on-disk
  encryption_state is 0b00 (cleartext).
- TestStorageEnvelopeGate_PostCutoverEmitsEnvelope - gate
  returns true, on-disk encryption_state is 0b01 (encrypted).
- TestStorageEnvelopeGate_FlipMidLife - pre-cutover write,
  flip gate, post-cutover write; both reads succeed live and
  on-disk bits actually differ. Pins the §6.3 mixed-mode-per-key
  property.
- TestStorageEnvelopeGate_NilGatePreservesLegacyBehavior -
  newEncryptedStoreFixture (no gate) still emits envelopes when
  the DEK is active, proving the pre-6D-5 posture is unchanged.
- TestStorageEnvelopeGate_NilArgumentIsNoOp - explicit nil into
  WithStorageEnvelopeGate must not nil-deref on the next Put.

## Caller audit (semantic change)

The new option is purely additive; existing WithEncryption call
sites are not touched. encryptForKey's existing fast-path callers
(every store.Put / PutAt) see the gate skipped when the field is
nil (the default), so this PR cannot regress any pre-existing
test or production code path. Audit:

- All current store.WithEncryption call sites are in
  store/lsm_store_encryption{,_prop}_test.go: 5 sites, none of
  them set WithStorageEnvelopeGate, all retain the legacy
  "encrypt when DEK active" posture.
- main.go does NOT currently call store.WithEncryption at all
  (storage-layer encryption wiring lands in 6D-6 alongside the
  cutover RPC), so this PR is operator-inert.

## Self-review (5 lenses)

1. Data loss - no. encryptForKey's only behavioural change is
   one additional path to encStateCleartext when the gate is
   wired AND returns false. The Pebble write itself is
   unchanged (same encodeValue + Set path).
2. Concurrency / distributed failures - the gate closure is
   read once per Put and is plain field-read on the caller's
   atomic.Bool / sync.Map view (test uses a *bool with the test's
   serial Put pattern). No new locking; no new races. Read path
   is untouched. Replicas flip StorageEnvelopeActive
   deterministically at FSM apply (proved by the 6D-4 tests), so
   no inter-replica skew window.
3. Performance - one extra branch + one closure call per Put
   when wired; nil-check skips both when not wired. No
   allocations.
4. Data consistency - the §6.3 mixed-mode invariant
   (cleartext + encrypted versions for the same key) is
   exercised by TestStorageEnvelopeGate_FlipMidLife. The §4.1
   AAD binding for envelope writes is unchanged.
5. Test coverage - 5 new tests cover (pre-cutover cleartext,
   post-cutover envelope, mid-life flip, nil-gate legacy
   posture, nil-argument no-op). Existing TestEncryption_*
   tests continue to pass with no modification.

## Verification

- go test -race -timeout 180s ./store/... ./internal/encryption/...
  ./kv/... - all green.
- golangci-lint --new-from-rev=origin/main - 0 issues.

Refs: design doc §6.2 (Stage 6D-5 row of §7 decomposition table).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Warning

Review limit reached

@bootjp, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 26 minutes and 24 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d78df1c6-8e6a-425f-bcdd-18fe7cc9e7d9

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4db9b and 4a7ba0d.

📒 Files selected for processing (1)
  • store/lsm_store_encryption_test.go
📝 Walkthrough

Walkthrough

Implements a storage-envelope gate (StorageEnvelopeActive) and WithStorageEnvelopeGate option to decouple DEK availability from encryption emission during Phase 0 → Phase 1 cutover, forcing cleartext writes when the gate returns false while preserving read-path transparency.

Changes

Storage Envelope Gate Feature

Layer / File(s) Summary
Gate type definition and store option
store/encryption_glue.go
StorageEnvelopeActive functional type and WithStorageEnvelopeGate PebbleStoreOption define the gate contract; nil is safe and preserves legacy behavior.
Store struct field addition
store/lsm_store.go
pebbleStore.storageEnvelopeActive field holds the optional gate callback; Stage 6D-5 gate controlling write-path encryption envelope vs. cleartext emission.
Write-path gate integration
store/encryption_glue.go
encryptForKey method consults the gate; when wired and returns false, forces encStateCleartext even if cipher and active DEK are available; reads remain unaffected by gate state.
Test fixtures and gate-aware helpers
store/lsm_store_encryption_test.go
gatedFixture manages store lifecycle, newGatedEncryptedStore creates MVCC stores with mutable gate flags, and rawEncStateAt reads on-disk encryption_state bits to validate wire-level behavior.
Gate behavior validation tests
store/lsm_store_encryption_test.go
Five tests cover pre-cutover cleartext forcing, post-cutover envelope emission, mid-life gate flip with mixed on-disk states, legacy behavior preservation, and nil-argument safety.
Design documentation updates
docs/design/2026_05_18_partial_6d_enable_storage_envelope.md
Marks 6D-5 as shipped, documents gate wiring, Phase 0 → Phase 1 semantics, and read-path transparency across mixed cleartext/encrypted versions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • bootjp/elastickv#786: The design specification for StorageEnvelopeActive write-path semantics that this PR implements and validates through comprehensive encryption tests.

Poem

🐰 A gate stands guard at the cutover's door,
Write-time control like never before—
Phase zero or one, the choice is clear,
Envelope or plaintext, no need for fear! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: introducing a storage-layer cutover toggle for encryption (WithStorageEnvelopeGate) as part of Stage 6D-5, which aligns with the primary objective and all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/encryption-6d5-storage-toggle

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 and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

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 the Stage 6D-5 storage-layer toggle for data-at-rest encryption, introducing a StorageEnvelopeActive gate to control the transition between cleartext and encrypted writes. The changes include the definition of the gate, its integration into the encryptForKey logic, and extensive tests to verify correct behavior across various cutover scenarios. Feedback focused on ensuring that errors from Close() methods in test cleanup functions are properly reported rather than silently ignored.

Comment thread store/lsm_store_encryption_test.go Outdated
g := &gatedFixture{mvcc: mvcc, dir: dir}
t.Cleanup(func() {
if !g.closed {
_ = g.mvcc.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 from g.mvcc.Close() is silently ignored in the cleanup function. Per the general rules, errors from Close() methods on resources should not be ignored to ensure that resource leaks or cleanup problems are visible. Even in test code, it is better to log or report these errors using t.Errorf.

			if err := g.mvcc.Close(); err != nil {
				t.Errorf("cleanup close pebble: %v", err)
			}
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.

Comment thread store/lsm_store_encryption_test.go Outdated
if err != nil {
t.Fatalf("NewPebbleStore: %v", err)
}
t.Cleanup(func() { _ = mvcc.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 from mvcc.Close() is silently ignored in the cleanup function. Following the general rules, errors from Close() methods should be logged or reported to surface potential resource leaks or cleanup failures.

	t.Cleanup(func() {
		if err := mvcc.Close(); err != nil {
			t.Errorf("cleanup close pebble: %v", err)
		}
	})
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

@coderabbitai coderabbitai Bot left a comment

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 `@store/lsm_store_encryption_test.go`:
- Around line 514-520: The test for legacy envelope emission with
WithStorageEnvelopeGate(nil) only checks round-trip data but not that data was
actually stored encrypted; update the test after closing the store to assert the
store's encryption_state equals encStateEncrypted. Specifically, in the test
containing mvcc.PutAt(...) and mustGet(...), add an assertion that reads the
store's encryption_state field (or exposes it via the store API) and compare it
to encStateEncrypted to ensure envelopes were emitted encrypted when using
WithStorageEnvelopeGate(nil).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb15cb46-01d6-4138-b396-3233a5ba9daf

📥 Commits

Reviewing files that changed from the base of the PR and between 9c10609 and 1d4db9b.

📒 Files selected for processing (4)
  • docs/design/2026_05_18_partial_6d_enable_storage_envelope.md
  • store/encryption_glue.go
  • store/lsm_store.go
  • store/lsm_store_encryption_test.go

Comment thread store/lsm_store_encryption_test.go
… minor wire-level assertion

Three review findings on PR809, all in store/lsm_store_encryption_test.go.

## gemini medium - newGatedEncryptedStore cleanup ignored Close error

The fixture's t.Cleanup did `_ = g.mvcc.Close()`, swallowing any
Pebble close error. A silent failure would mask a leaked handle
or a flush regression. Fix: surface the error via t.Errorf when
the test body has not already closed the store.

## gemini medium - TestStorageEnvelopeGate_NilArgumentIsNoOp cleanup ignored Close error

Same shape, separate cleanup. Same fix - surface via t.Errorf.
Added a `closed` flag so the test body can close the store
itself (needed for the on-disk peek - see coderabbit below)
without double-closing in cleanup.

## coderabbit minor - TestStorageEnvelopeGate_NilArgumentIsNoOp lacked wire-level assertion

The test proved round-trip but not the on-disk encryption_state
bits. A regression that wrote cleartext would still round-trip
cleanly. Fix: close the store, peek via rawEncStateAt, assert
encStateEncrypted. Now symmetric with the other gate tests
which already peek the on-disk bits.

## Caller audit

Test-cleanup-only changes; no production code semantics
touched. The encryptForKey gate logic from the initial PR is
unchanged. All 5 gate tests still pass.

## Verification

- go test -race -timeout 60s -run TestStorageEnvelopeGate_ ./store/... - all green
- golangci-lint --new-from-rev=origin/main - 0 issues
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 23, 2026

@claude review

Round-1 (commit eb526094): addressed two gemini-code-assist medium findings + one coderabbit minor.

gemini medium — close-err ignored in t.Cleanup (2 sites)

newGatedEncryptedStore and TestStorageEnvelopeGate_NilArgumentIsNoOp each had a t.Cleanup that did _ = mvcc.Close(), swallowing any Pebble close error. A silent failure would mask a leaked handle or flush regression. Both cleanups now surface via t.Errorf. The nil-argument test uses a closed flag so the test body can close the store itself for the on-disk peek without double-closing in cleanup.

coderabbit minor — TestStorageEnvelopeGate_NilArgumentIsNoOp lacked wire-level assertion

The test only proved round-trip; a regression that wrote cleartext would still pass. Added the same close + rawEncStateAt + encStateEncrypted assertion the other gate tests already use. Now symmetric coverage.

Caller audit

Test-cleanup-only changes; no production code semantics touched. The encryptForKey gate logic from the initial PR is unchanged. All 5 gate tests still pass.

Verification

  • go test -race -timeout 60s -run TestStorageEnvelopeGate_ ./store/... — all green
  • golangci-lint --new-from-rev=origin/main — 0 issues

@claude
Copy link
Copy Markdown

claude Bot commented May 23, 2026

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


Review of Round-1 fixes (commit eb526094)

Todo list:

  • Load tools
  • Read changed files (store/encryption_glue.go, store/lsm_store.go, store/lsm_store_encryption_test.go, design doc)
  • Verify Round-1 fix Configure Renovate #1: close-err surfaced in both t.Cleanup sites
  • Verify Round-1 fix Update dependency go to v1.21.4 #2: wire-level encryption_state assertion in TestStorageEnvelopeGate_NilArgumentIsNoOp
  • Review production code (encryptForKey gate logic)
  • Post final review

Fix #1t.Cleanup close-error surfacing (2 sites)

newGatedEncryptedStore cleanup (lines 301–313): Correctly structured. The if g.closed { return } guard prevents a double-close when the test body calls closeIfOpen for the on-disk peek. t.Errorf is the right call here: t.Fatalf in a cleanup goroutine runs fine but semantically t.Errorf is the convention (defer-style cleanup, don't stop other cleanups). The comment linking back to the review finding is welcome.

TestStorageEnvelopeGate_NilArgumentIsNoOp cleanup (lines 525–532): Also correct. One worth calling out explicitly: closed = true is set before mvcc.Close() is called (line 545 before 546). This matters because t.Fatalf inside the test body calls runtime.Goexit(), which runs all registered cleanups — so setting closed = true before the close call guarantees the cleanup skips the second attempt regardless of whether the Fatalf path fires. The sequencing is sound.

Both sites use t.Errorf (not t.Fatalf) in the cleanup, which is the correct choice: it marks the test failed without stopping other cleanup routines.

Fix #1: ✅ Correct


Fix #2 — Wire-level assertion in TestStorageEnvelopeGate_NilArgumentIsNoOp (lines 544–552)

The added block:

closed = true
if cerr := mvcc.Close(); cerr != nil {
    t.Fatalf("close pebble: %v", cerr)
}
if got := rawEncStateAt(t, dir, []byte("k"), 100); got != encStateEncrypted {
    t.Errorf("nil-gate on-disk encState = %#x, want %#x (encrypted; legacy posture)",
        got, encStateEncrypted)
}

This mirrors the exact pattern used by TestStorageEnvelopeGate_PreCutoverForcesCleartext, PostCutoverEmitsEnvelope, FlipMidLife, and NilGatePreservesLegacyBehavior. The five gate tests are now structurally symmetric. Previously this test only proved round-trip; it can now catch a regression where WithStorageEnvelopeGate(nil) accidentally forces cleartext.

Fix #2: ✅ Correct


Production code — encryptForKey gate (encryption_glue.go:248–250)

if s.storageEnvelopeActive != nil && !s.storageEnvelopeActive() {
    return plaintext, encStateCleartext, nil
}

Gate placement is correct: it fires after both the cipher-nil and activeStorageKeyID-ok checks, so the only new path is "cipher wired, DEK active, but gate says no." The nil guard preserves the legacy "encrypt whenever a DEK is active" posture exactly. Read path is untouched. No functional change to existing callers.

Production code: ✅ No issues


Minor observations (pre-existing, out of scope for this PR)

Three older test helpers still silently swallow Close() errors with _ = ...:

Site Line Note
newEncryptedStoreFixture cleanup ~93 _ = f.mvcc.Close()
newToggleableEncryptedStore cleanup ~243 _ = mvcc.Close()
TestEncryption_SnapshotRestoreAtMaxValueSize ~832 _ = dst.Close()
rawEncStateAt ~336 _ = closer.Close() (Pebble iterator)

These are pre-existing patterns not introduced by this PR. Worth a follow-up pass to align them with the new convention, but not blocking here.


Overall assessment

Round-1 addresses all three reviewer findings cleanly. The close-error suppression is fixed at both new sites with the right guard structure, and the NilArgumentIsNoOp test now proves the legacy-posture at the wire level symmetrically with the other four gate tests. No regression to existing tests or production code paths. LGTM.

…StateAt

Claude bot review on PR809 round-1 noted that rawEncStateAt
(added by this PR) still swallowed the Pebble iterator's
closer.Close error via _ = closer.Close(). Symmetric with the
round-1 fixes for the t.Cleanup sites. Test helpers introduced
by this PR now uniformly surface close errors via t.Errorf.

The three pre-existing close-err swallows flagged by Claude
(newEncryptedStoreFixture, newToggleableEncryptedStore,
TestEncryption_SnapshotRestoreAtMaxValueSize) are out of scope
for PR809 - Claude's review explicitly called them 'worth a
follow-up pass, but not blocking here'.

Verification:
- go test -race -timeout 60s -run TestStorageEnvelopeGate_ ./store/... - all green
- golangci-lint --new-from-rev=origin/main - 0 issues
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 23, 2026

@claude review

Round-2 (commit 4a7ba0d8): addressed claude bot's round-1 self-flagged observation.

rawEncStateAt (the helper I added in this PR) still swallowed the Pebble iterator's closer.Close error via _ = closer.Close(). Now surfaces via t.Errorf, symmetric with the round-1 fixes for the t.Cleanup sites.

The three pre-existing close-err swallow sites (newEncryptedStoreFixture, newToggleableEncryptedStore, TestEncryption_SnapshotRestoreAtMaxValueSize) are out of scope for this PR — claude's round-1 review explicitly called them 'worth a follow-up pass, but not blocking here'.

go test -race + golangci-lint both green.

@claude
Copy link
Copy Markdown

claude Bot commented May 23, 2026

Claude finished @bootjp's task in 1m 6s —— View job


Review of Round-2 fixes (commit 4a7ba0d8)

Todo list:

  • Load tools
  • Read rawEncStateAt in store/lsm_store_encryption_test.go
  • Verify the closer.Close error is now surfaced correctly
  • Check for any remaining issues
  • Post final review

Fix — rawEncStateAt close-error surfacing (store/lsm_store_encryption_test.go:321–345)

The updated function now registers two defer closures instead of the previous _ = closer.Close():

defer func() {
    if cerr := pdb.Close(); cerr != nil {
        t.Errorf("pdb.Close: %v", cerr)
    }
}()
// ...
defer func() {
    if cerr := closer.Close(); cerr != nil {
        t.Errorf("closer.Close: %v", cerr)
    }
}()

Three things worth calling out explicitly:

Defer ordering is correct. Go executes defers LIFO. pdb.Close() is registered first (runs last); closer.Close() is registered second (runs first). This matches the required cleanup order: the closer is a reference-counted buffer tied to the database, so it must be released before the pdb handle is closed. The ordering is not accidental.

t.Errorf over t.Fatalf in defers is idiomatic. While t.Fatalf would still allow other defers to run (via runtime.Goexit()), using t.Errorf lets both cleanup steps complete and return normally from the deferred closures. Consistent with the round-1 t.Cleanup convention.

Symmetric with round-1 t.Cleanup fixes. Both the newGatedEncryptedStore cleanup and TestStorageEnvelopeGate_NilArgumentIsNoOp close-err handling use t.Errorf; this helper now matches.

Fix: ✅ Correct


Pre-existing out-of-scope sites

The three pre-existing _ = *.Close() patterns (newEncryptedStoreFixture ~L93, newToggleableEncryptedStore ~L243, TestEncryption_SnapshotRestoreAtMaxValueSize ~L832) remain. Correctly scoped out — my round-1 review called these "worth a follow-up pass, not blocking." They should be cleaned up in a follow-on, but that is not a concern for this PR.


Overall assessment

Round-2 is a clean, minimal, and correct fix. All three reviewer findings from round-1 are now addressed and the rawEncStateAt helper is fully symmetric with the t.Cleanup convention introduced in round-1. No new issues introduced. LGTM.

@bootjp bootjp merged commit 7c1d91e into main May 23, 2026
9 checks passed
@bootjp bootjp deleted the feat/encryption-6d5-storage-toggle branch May 23, 2026 04:38
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