fix(utxo/aerospike): close 4 silent-failure paths in sendStoreBatch#853
Conversation
Each path could result in Create() returning success or hanging while the transaction was not actually persisted, with no error returned: 1. Non-KEY_EXISTS top-level BatchOperate error fell through to the per-record loop, which then sent SafeSend(nil) over closed channels for items the error path had just notified. Add a return after the error notification and guard with resultHandledElsewhere. 2. Per-record errors of any concrete type other than *AerospikeError (e.g. const sentinels like aerospike.ErrTimeout) were silently skipped because the fallback SafeSend was nested inside the `if ok` type-assertion branch. Move it out so any non-nil per-record error is surfaced. 3. KEY_NOT_FOUND_ERROR was unconditionally treated as a NOOP-record marker via `continue` without notifying. Track NOOP intent explicitly via resultHandledElsewhere so a real BatchWrite that returns KEY_NOT_FOUND_ERROR is surfaced as an error. 4. Top-level KEY_EXISTS_ERROR notified every item in the batch with an error message hardcoded to batch[0].txHash. Use each item's own txHash. Also add a batchOperateFn seam so sendStoreBatch is unit-testable, mirroring the existing pattern from storeExternallyWithLock. Tests in send_store_batch_test.go cover each failure mode plus happy-path and mixed-result scenarios.
|
🤖 Claude Code Review Status: Complete Current Review: No issues found. This PR fixes four critical silent-failure paths in Summary: This fix addresses dangerous edge cases where Strengths:
|
There was a problem hiding this comment.
Pull request overview
This PR hardens stores/utxo/aerospike batch-creation behavior by ensuring every sendStoreBatch path either notifies the caller exactly once (success or error) or intentionally delegates notification to an external-storage goroutine, eliminating several previously silent success/hang scenarios.
Changes:
- Fixes top-level
BatchOperateerror handling to avoid fallthrough into per-record processing and to ensure correct per-item tx-hash reporting forKEY_EXISTS_ERROR. - Ensances per-record result handling so non-
*AerospikeErrorerrors (e.g., const sentinel errors likeaerospike.ErrTimeout) and unexpectedKEY_NOT_FOUND_ERRORcases always notify callers. - Adds unit tests (serverless) via a
batchOperateFnseam usage and coverage of the previously silent-failure/hang paths.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
stores/utxo/aerospike/create.go |
Ensures sendStoreBatch reliably sends exactly one notification per item (or delegates ownership), fixes error classification gaps, and corrects per-item error messaging. |
stores/utxo/aerospike/send_store_batch_test.go |
Adds focused unit tests covering top-level/per-record error cases and duplicate-notification regressions without requiring an Aerospike server. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-05-12 17:08 UTC |



Summary
Closes four code paths in
sendStoreBatchwhereCreate()could either return success while the transaction was not actually persisted, or hang the caller indefinitely with no error returned. Companion fix to #784 (which fixed the same class of bug instoreExternallyWithLock).The 4 fixed paths
Missing
returnafter non-KEY_EXISTS top-level error — falling through into the per-record loop sentSafeSend(nil)for any record whose per-recordErrwas unset (the common case when the top-level call failed before reaching the server). Now returns after error notification.Wrong txHash in multi-item top-level KEY_EXISTS_ERROR — was using
batch[0].txHashfor every item in the batch. Now each item's error references its own hash. The comment claimed "this should only be called with 1 record" but the defaultutxostore_storeBatcherSizeis 100.Silent hang on non-
*AerospikeErrorper-record types — the fallbackSafeSendfor unknown aerospike result codes was nested inside theerr.(*aerospike.AerospikeError)type-asserted branch. The Aerospike client exposes const sentinels likeaerospike.ErrTimeout/ErrNetworkwhose concrete type is*constAerospikeError, not*AerospikeError. If one ever appeared inBatchRec().Err, the type assertion failed, noSafeSendran, and the caller hung on<-errCh. Fallback moved outside the type assertion.KEY_NOT_FOUND_ERROR assumption — code unconditionally treated KEY_NOT_FOUND_ERROR as a NOOP-placeholder marker and
continued without notifying. A real BatchWrite returning KEY_NOT_FOUND_ERROR (Aerospike client edge cases, future client changes) would silently hang the caller. NOOP intent is now tracked explicitly via a newresultHandledElsewhere []boolso only intentional NOOPs are skipped.Bonus fix uncovered while implementing
Top-level error notification looped over all items including those already notified by the setup loop (NewKey / GetBinsToStore / external-store failures). The
resultHandledElsewhereflag suppresses those duplicates too.Refactor for testability
Added the
s.batchOperateFnseam tosendStoreBatch(already present instoreExternallyWithLockvia #784) so the per-record paths can be unit-tested without an Aerospike server. Same field, same pattern.Test plan
New file
stores/utxo/aerospike/send_store_batch_test.go— 7 unit tests, no testcontainers required:TopLevelNonKeyExistsError_NoDuplicateNotification— fails before fix (item receives 2 notifications)TopLevelKeyExistsError_EachItemGetsOwnTxHash— fails before fix (item 1's error names batch[0]'s hash)PerRecordConstError_StillNotifies— fails before fix (hangs onaerospike.ErrTimeout)KeyNotFoundOnRealRecord_NotifiesError— fails before fix (silent skip on non-NOOP record)AllSuccess_SingleSuccessNotificationPerItem— happy-path regression guardMixedPerRecordResults— verifies classification of success + KEY_EXISTS + const-error in one batchTopLevelError_DoesNotDuplicateNotifyPreSendItems— top-level error loop doesn't re-notify pre-notified itemsEach error-path test was verified to fail on
mainbefore the fix, then pass after.go build ./...go vet ./stores/utxo/aerospike/go test -race -run TestSendStoreBatch ./stores/utxo/aerospike/— 7/7 pass