test(legacy/netsync): fix _NilParentTx flake from SyncedMap random eviction#931
test(legacy/netsync): fix _NilParentTx flake from SyncedMap random eviction#931freemans13 wants to merge 3 commits into
Conversation
buildOOBFixture and buildInRangeFixture created their SyncedMap with limit=2 and then inserted exactly 2 entries. The _NilParentTx tests subsequently Set on an existing key; once len == limit, go-tx-map's setUnlocked evicts a random item from the map before writing — which ~50% of the time drops the child the SUT is about to look up, leaving the test failing with "tx <hash> not found in txMap". Reproduced locally with -count=10 under -race. Use an unbounded SyncedMap in the fixtures (the production code path sizes by block tx count, not 2) and leave a comment explaining the eviction trap so future fixtures don't fall into the same hole.
|
🤖 Claude Code Review Status: Complete The fix correctly addresses the test flake by removing the artificial size limit from both test fixtures. The root cause analysis is sound: Minor Issue The comment at line 648 in Assessment The implementation is correct and test-only. No production code impact. The comments explain the eviction trap to prevent future regressions. |
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-06-02 09:05 UTC |
oskarszoon
left a comment
There was a problem hiding this comment.
Approve. Real bug — SyncedMap.setUnlocked evicts a random entry when full, without first checking if the key already exists. Re-Setting an existing key on a full map can drop a different key. Fixture had NewSyncedMap(2) and re-Set the parent → ~50% the child was evicted → "tx not found in txMap" instead of expected TxInvalid.
Pre-fix reproduction confirmed; post-fix 20× clean.
Worth a follow-up upstream issue on go-tx-map — the bug is dormant in Teranode prod (rejectedTxns has a prior .Get-exists guard; txMap iterates unique hashes) but worth fixing at the source.
Tiny alternative the author could consider: sibling tests _NilOutput and _NilLockingScript mutate the wrapper directly (parentWrapper.Tx.Outputs[0] = nil). _NilParentTx could do parentWrapper.Tx = nil instead of Set(...), keeping the (2) limit and matching sibling style. Current choice is fine — the comment is a useful learning artifact.
buildOOBFixture and buildInRangeFixture created their SyncedMap with limit=2 and then inserted exactly 2 entries. The _NilParentTx tests subsequently Set on an existing key; once len == limit, go-tx-map's setUnlocked evicts a random item from the map before writing — which ~50% of the time drops the child the SUT is about to look up, leaving the test failing with "tx <hash> not found in txMap". Reproduced locally with -count=20 under -race. Use an unbounded SyncedMap in the fixtures (the production path sizes by block tx count, not 2) and leave a comment explaining the eviction trap so future fixtures don't fall into the same hole. Duplicates the fix in PR #931 so this PR's CI is unblocked independently; will fold into theirs if #931 lands first.
buildOOBFixture and buildInRangeFixture created their SyncedMap with limit=2 and then inserted exactly 2 entries. The _NilParentTx tests subsequently Set on an existing key; once len == limit, go-tx-map's setUnlocked evicts a random item from the map before writing — which ~50% of the time drops the child the SUT is about to look up, leaving the test failing with "tx <hash> not found in txMap". Reproduced locally with -count=20 under -race. Use an unbounded SyncedMap in the fixtures (the production path sizes by block tx count, not 2) and leave a comment explaining the eviction trap so future fixtures don't fall into the same hole. Duplicates the fix in PR #931 so this PR's CI is unblocked independently; will fold into theirs if #931 lands first.
buildOOBFixture and buildInRangeFixture created their SyncedMap with limit=2 and then inserted exactly 2 entries. The _NilParentTx tests subsequently Set on an existing key; once len == limit, go-tx-map's setUnlocked evicts a random item from the map before writing — which ~50% of the time drops the child the SUT is about to look up, leaving the test failing with "tx <hash> not found in txMap". Reproduced locally with -count=20 under -race. Use an unbounded SyncedMap in the fixtures (the production path sizes by block tx count, not 2) and leave a comment explaining the eviction trap so future fixtures don't fall into the same hole. Duplicates the fix in PR #931 so this PR's CI is unblocked independently; will fold into theirs if #931 lands first. (Amended to force a fresh CI trigger — the prior push at 13:09 didn't fire a pull_request event.)
# Conflicts: # services/legacy/netsync/handle_block_test.go
|
|
Closing — this change is already fully present on
Together #767 + #933 make every change in this PR a no-op against current Worth noting the underlying root cause still stands as a potential upstream follow-up: |



Summary
TestSyncManager_extendFromTxMap_NilParentTx(and itsExtendTransaction_NilParentTxsibling) intermittently fail in CI with:Root cause
The shared fixtures
buildOOBFixtureandbuildInRangeFixture(services/legacy/netsync/handle_block_test.go) construct atxmap.NewSyncedMap[...](2)and immediately insert 2 entries, leavinglen == limit. The_NilParentTxtests then do a thirdSeton the already-present parent key.go-tx-map'ssetUnlockedruns its eviction branch wheneverlen(m) >= limit, including on key-update, and evicts a random item via Go's randomized map iteration:~50% of runs the child is evicted, so when the SUT calls
extendFromTxMap(ctx, child, txMap)its very first lookup (txMap.Get(*tx.TxIDChainHash())) misses and returns aProcessingErrorinstead of reaching the nil-parent branch under test.Fix
Drop the
(2)limit on both fixtures — the production path sizes the map by block tx count, not 2 — and add a short comment explaining the eviction trap so the next person who copies the fixture doesn't fall into the same hole.This is purely a test-fixture change; no production code is touched.
Repro / verification
Before, locally on
upstream/main:After this patch:
Note
Strictly speaking the eviction logic in
go-tx-mapis also wrong (setUnlockedshould not evict when the key already exists). Worth chasing upstream as a follow-up, but the fixture is the test-side defect that's blocking CI today.Surfaced while investigating CI red on #863, which doesn't touch this package — see #863 for context.