You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Summary
Implements the merged 7c design (PR #868). Pre-registers a new node's
writer-registry row from the admin-RPC `AddVoter`/`AddLearner` handler
so that:
1. **Write-window elimination** (optimization): encrypted writes from
the new node never fail closed on the 7a-2 storage gate after the
conf-change commits.
2. **Collision-safe membership change** (correctness): a §6.1 uint16
NodeID collision is caught at the RPC layer with
`ErrWriterUint16Collision` **before** the conf-change is proposed — no
§4.1 case-4 halt-apply on a durable conf-change entry.
### Three changes
1. **`internal/raftadmin`**: `MembershipChangeInterceptor` interface +
`NewServerWithInterceptor` constructor +
`RegisterOperationalServicesWithInterceptor`.
`Server.AddVoter`/`AddLearner` invoke the optional `PreAddMember` hook
before the engine's conf-change proposal. Nil interceptor preserves the
pre-7c posture.
2. **`internal/encryption`**: `ErrWriterUint16Collision` typed error for
the §6.1 collision branch.
3. **`main`**: `encryptionPreRegister` adapter implements the
interceptor — read-before-propose guard against §4.1 case-3
(re-proposing `epoch=0` against an existing `(epoch=0)` row), idempotent
skip on FullNodeID match, typed error on FullNodeID mismatch (§6.1
collision), case-1 first-seen propose otherwise. Wired into
`serversInput → runtimeServerRunner → startRaftServers →
RegisterOperationalServicesWithInterceptor` so the encryption adapter
coupling stays at the main-level wiring point.
### Test coverage (per design §5)
- `internal/raftadmin/server_test.go`:
- `TestServer_AddVoter_InvokesInterceptorBeforeConfChange` — ordering:
hook called, then engine.AddVoter.
- `TestServer_AddVoter_InterceptorErrorAbortsConfChange` —
engine.AddVoter NOT called when hook errs.
- `TestServer_AddVoter_NilInterceptorSkipsPreStep` — pre-7c posture
preserved.
- `TestServer_AddLearner_InterceptorContract` — symmetric sub-tests for
AddLearner.
- `main_encryption_confchange_test.go`:
- `TestEncryptionPreRegister_PreBootstrapSkips` — `ActiveStorageKeyID()
== (0, false)` → nil-skip.
- `TestEncryptionPreRegister_NilCacheReturnsNilInterceptor` — defensive
nil-check at constructor.
- `TestEncryptionPreRegister_IdempotentWhenRowExists` — pins the §3.1
read-before-propose guard against the §4.1 case-3
`ErrLocalEpochRollback` regression (claude round-2 BLOCKING on PR #868).
- `TestEncryptionPreRegister_Uint16CollisionReturnsTypedError` — pins
the §6.1 collision branch.
## 5-lens self review
- **Data loss**: clean. Pre-register propose is additive (`0x03` entry +
§4.1 case-1 insert); no user-data-path change.
- **Concurrency**: pre-register runs on the leader's RPC goroutine; the
guard catches the leader-flip retry race via the durable row, NOT via
case-2 monotonic (which requires strictly greater epoch).
- **Performance**: one registry read + at most one Raft round-trip per
`AddVoter`/`AddLearner` call. Encryption-unaware path unchanged (nil
interceptor skips the pre-step).
- **Data consistency**: §4.1 case-1 first-seen for genuinely new
members; guard returns idempotent-skip or `ErrWriterUint16Collision` for
existing rows. §6.1 case-4 halt-apply is restricted to the narrow TOCTOU
residual (two concurrent AddVoter for uint16-colliding raftIDs).
- **Test coverage**: complete per design §5 (raftadmin contract +
adapter branches). The §3.5 leader-flip retry path is pinned by
`IdempotentWhenRowExists`. End-to-end test deferred per design §5.3
(requires 2-node test cluster fixture; unit + integration above cover
the contract surface).
## Test plan
- [x] `go test -run
'TestApplier|TestApplyRotation|TestApplyBootstrap|TestRuntimeRegistration|TestBuildProcessStartRegistration|TestRegistrationCommittedAtEpoch|TestEncryption_E2E|TestRunWriterRegistration|TestServer|TestEncryptionPreRegister|TestRegisterOperational'
-race -timeout 180s ./internal/encryption ./internal/raftadmin ./.` —
green.
- [x] `make lint` clean.
- [ ] @claude review for correctness of the interceptor wiring,
guard-branch invariants, and main-level construction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
0 commit comments