Follow-ups from a review of #23106 (main pick of #23099, which is merged to release/3.6). That change moves the release of superseded fee-merge write sets off the hot loop into a background releaser (recordFeeMerge / queueMapRelease / mapReleases in execution/stagedsync/exec3_parallel.go).
The review confirmed the core of the change is safe: the superseded temp is unreachable once queued, queueing happens only on the exec loop, there is no double-release path, and the touched tests pass under -race. The items below are robustness, efficiency and convention follow-ups, roughly in priority order. Scope is main; the pool-level item was explicitly out of scope for 3.6.
Test shield (one-line fix)
Memory retention and lifecycle
Efficiency
Robustness parity (future-conditional — verified not reachable today)
Coverage gap (pre-existing, from #22883)
Simplification (may resolve several items above at once)
Conventions
cc @AskAlexSharov
Follow-ups from a review of #23106 (main pick of #23099, which is merged to release/3.6). That change moves the release of superseded fee-merge write sets off the hot loop into a background releaser (
recordFeeMerge/queueMapRelease/mapReleasesinexecution/stagedsync/exec3_parallel.go).The review confirmed the core of the change is safe: the superseded temp is unreachable once queued, queueing happens only on the exec loop, there is no double-release path, and the touched tests pass under
-race. The items below are robustness, efficiency and convention follow-ups, roughly in priority order. Scope is main; the pool-level item was explicitly out of scope for 3.6.Test shield (one-line fix)
exec3_fee_merge_temp_test.go, the round-1 negative assertion (txOut must survive the fee merge) is not shielded bybe.awaitMapReleases(). If a future bug wrongly queues a live set for release, the release waits in the channel while the test readsCount(), so the test would still pass most of the time — exactly the regression class it exists to catch. Addbe.awaitMapReleases()after the firstrecordFeeMerge;Waiton a zero counter returns immediately.Memory retention and lifecycle
mapReleasecarries&be.mapReleasing. This interior pointer keeps the wholeblockExecutor(results, tasks, blockIO, caches) reachable until the global releaser drains the entry, heaviest under memory pressure — and the WaitGroup has no production waiter, only the tests callawaitMapReleases. A separately allocated WaitGroup or a done-channel would pin only itself.parallelExecutorin the process, so one storage-heavy release delays all instances. A per-executor releaser managed like the commitment calculator would give lifecycle, isolation and a natural drain point — or see the simplification below.Efficiency
Done()only) is correctness-neutral and keeps the hot path flat; the stale-temp path after re-execution already GC-drops without releasing.ReleaseOutputMapswalks every tx's write set for every block, and the root cost (vwMapPoolclear-on-put with no size cap) sits in the state package where a fix would help all callers. A size cap for pooled maps or a cheaper clear may shrink or remove the need for the queue.Robustness parity (future-conditional — verified not reachable today)
ReleaseMapson a bare goroutine without recover. A recoverable panic there kills the process, while the same panic inline is contained by the exec loop's deferred recover. A recover-and-log wrapper in the consumer restores parity between the two branches.WriteSethas no released-tripwire: a future use-after-supersede reader would hit a nondeterministicconcurrent map read and map writethrow instead of the deterministic empty reads it got before. The siblingReleaseOutputMapspath guards this class withoutputsReleased+assertOutputsLiveunderdbg.AssertEnabled; the fee-merge temps deserve the same guard.Coverage gap (pre-existing, from #22883)
existingWritesreplaced by the merged set) but is not routed throughrecordFeeMerge, so those temps are never pooled and thefeeMergeTempentry goes stale. The ownership argument is identical to the covered site.Simplification (may resolve several items above at once)
recordFeeMergeruns only on the exec loop, so a plain per-executor pending slice, drained at the existing block-end release point next toReleaseOutputMaps, gives the same offload with zero new concurrency primitives — and makes the tests synchronous again, which also resolves the retention, lifecycle and test-shield items. Whether releasing pays at all versus a plain GC drop is still being measured (alex/exec_dbg_37gates it behind aRELEASE_FEE_MERGE_MAPSswitch for A/B); if GC-drop wins, the whole mechanism can go away.Conventions
recordFeeMergeruns on the exec loop (processResults); the apply loop is theapplyResultsconsumer. Fix the wording — the PR body itself has it right.queueMapReleaseis attached totype mapReleaseand starts with a different symbol's name ("ReleaseMaps clears every map…"). Move it ontoqueueMapRelease; give the type a one-liner starting with its name, or none.mapReleases. A small test that fills the channel and asserts the inline release +Donewould close the gap.cc @AskAlexSharov