Skip to content

[3.4] rpc: eth_getBlockReceipts(): fix OOM - bound goroutines and concurren… - #19725

Merged
AskAlexSharov merged 1 commit into
release/3.4from
receipts_bound_goroutine_and_concur
Mar 8, 2026
Merged

AskAlexSharov merged 1 commit into
release/3.4from
receipts_bound_goroutine_and_concur

Conversation

@lupin012

@lupin012 lupin012 commented Mar 8, 2026 •

Copy link
Copy Markdown
Contributor

cherry-pick of #19710

close #19720

Fix goroutine leak in GetReceipts loop: each tx spawned a goroutine waiting on ctx.Done() to cancel the EVM, but ctx was shared across the entire block execution, so all N goroutines stayed alive until GetReceipts returned. Under concurrent requests for different blocks this caused goroutines and EVMs to accumulate in memory, triggering OOM. Fixed by adding a txDone channel closed immediately after ApplyTransactionWithEVM - the goroutine now exits as soon as its tx completes, keeping at most 1 goroutine alive at a time.

Add execSem semaphore (capacity max(1, GOMAXPROCS/2), env R_EXEC_CONCURRENCY) to limit the number of blocks executing concurrently in GetReceipts. Each parallel block execution holds its own IntraBlockState which can be hundreds of MB for busy mainnet blocks; without a bound, many concurrent requests for different blocks exhaust memory. Requests already served from the LRU cache bypass the semaphore entirely.

./run_perf_tests.py .... _eth_get_block_receipts_21M_20K.tar -t 500:10,5000:1

with current SW:
[3. 1] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=10.597s]
[3. 2] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=13.36s] [3. 3] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=26.353s]
[3. 4] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=1m56s] [3. 5] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=2m44s]

[4. 1] daemon: executes test qps: 5000 time: 1 -> [R=100.00% max=4m11s] [4. 2] daemon: executes test qps: 5000 time: 1 -> test failed: server is Dead for OOM

with new SW:
[3. 1] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=11.591s]
[3. 2] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=5.202s] [3. 3] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=4.947s] [3. 4] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=5.061s] [3. 5] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=5.009s]

[4. 1] daemon: executes test qps: 5000 time: 1 -> [R=100.00% max=13.789s]
[4. 2] daemon: executes test qps: 5000 time: 1 -> [R=100.00% max=14.032s]
[4. 3] daemon: executes test qps: 5000 time: 1 -> [R=100.00% max=14.02s] [4. 4] daemon: executes test qps: 5000 time: 1 -> [R=100.00% max=13.958s]
[4. 5] daemon: executes test qps: 5000 time: 1 -> [R=100.00% max=13.924s]

…cy (#19710)

close #19720 

Fix goroutine leak in GetReceipts loop: each tx spawned a goroutine
waiting on ctx.Done() to cancel the EVM, but ctx was shared across the
entire block execution, so all N goroutines stayed alive until
GetReceipts returned. Under concurrent requests for different blocks
this caused goroutines and EVMs to accumulate in memory, triggering OOM.
Fixed by adding a txDone channel closed immediately after
ApplyTransactionWithEVM - the goroutine now exits as soon as its tx
completes, keeping at most 1 goroutine alive at a time.

Add execSem semaphore (capacity max(1, GOMAXPROCS/2), env
R_EXEC_CONCURRENCY) to limit the number of blocks executing concurrently
in GetReceipts. Each parallel block execution holds its own
IntraBlockState which can be hundreds of MB for busy mainnet blocks;
without a bound, many concurrent requests for different blocks exhaust
memory. Requests already served from the LRU cache bypass the semaphore
entirely.

./run_perf_tests.py .... _eth_get_block_receipts_21M_20K.tar -t
500:10,5000:1

with current SW:
[3. 1] daemon: executes test qps: 500 time: 10 -> [R=100.00%
max=10.597s]
[3. 2] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=13.36s]
[3. 3] daemon: executes test qps: 500 time: 10 -> [R=100.00%
max=26.353s]
[3. 4] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=1m56s]
[3. 5] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=2m44s]

[4. 1] daemon: executes test qps: 5000 time: 1 -> [R=100.00% max=4m11s]
[4. 2] daemon: executes test qps: 5000 time: 1 -> test failed: server is
Dead for OOM

with new SW:
[3. 1] daemon: executes test qps: 500 time: 10 -> [R=100.00%
max=11.591s]
[3. 2] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=5.202s]
[3. 3] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=4.947s]
[3. 4] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=5.061s]
[3. 5] daemon: executes test qps: 500 time: 10 -> [R=100.00% max=5.009s]

[4. 1] daemon: executes test qps: 5000 time: 1 -> [R=100.00%
max=13.789s]
[4. 2] daemon: executes test qps: 5000 time: 1 -> [R=100.00%
max=14.032s]
[4. 3] daemon: executes test qps: 5000 time: 1 -> [R=100.00% max=14.02s]
[4. 4] daemon: executes test qps: 5000 time: 1 -> [R=100.00%
max=13.958s]
[4. 5] daemon: executes test qps: 5000 time: 1 -> [R=100.00%
max=13.924s]

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@lupin012
lupin012 marked this pull request as ready for review March 8, 2026 12:32
@lupin012
lupin012 requested a review from canepat as a code owner March 8, 2026 12:32
@AskAlexSharov
AskAlexSharov merged commit 6663bc1 into release/3.4 Mar 8, 2026
29 of 32 checks passed
@AskAlexSharov
AskAlexSharov deleted the receipts_bound_goroutine_and_concur branch March 8, 2026 13:00
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.

2 participants