core: add graceful shutdown checks during block processing

Fixes issue where node would continue processing blocks for several seconds
after receiving shutdown signal (Ctrl+C), ignoring user interruption.

The problem occurred because shutdown signals were only checked at the
beginning of each block processing loop in insertChain(), but not during
the actual block processing in processBlock(). Heavy blocks with high gas
usage or many transactions could take several seconds to process, during
which shutdown signals were completely ignored.

This change adds shutdown signal checks at strategic points in processBlock():
- Before starting block processing
- After state setup but before processing
- After processing but before validation
- After validation but before writing to disk

These checks allow block processing to be interrupted gracefully at safe
points, providing immediate response to shutdown signals.

Fixes #31508
This commit is contained in:
gabornyergesX 2025-06-25 21:24:23 +02:00
parent a92f2b86e3
commit aacd081b5f
No known key found for this signature in database
2 changed files with 60 additions and 0 deletions

View file

@ -1956,6 +1956,11 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
)
defer interrupt.Store(true) // terminate the prefetch at the end
// Short circuit if shutting down
if bc.insertStopped() {
return nil, fmt.Errorf("block processing interrupted during shutdown")
}
if bc.cfg.NoPrefetch {
statedb, err = state.New(parentRoot, bc.statedb)
if err != nil {
@ -2006,6 +2011,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
}(time.Now(), throwaway, block)
}
if bc.insertStopped() {
return nil, fmt.Errorf("block processing interrupted during shutdown")
}
// If we are past Byzantium, enable prefetching to pull in trie node paths
// while processing transactions. Before Byzantium the prefetcher is mostly
// useless due to the intermediate root hashing after each transaction.
@ -2046,6 +2055,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
}
ptime := time.Since(pstart)
if bc.insertStopped() {
return nil, fmt.Errorf("block processing interrupted during shutdown")
}
vstart := time.Now()
if err := bc.validator.ValidateState(block, statedb, res, false); err != nil {
bc.reportBlock(block, res, err)
@ -2053,6 +2066,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
}
vtime := time.Since(vstart)
if bc.insertStopped() {
return nil, fmt.Errorf("block processing interrupted during shutdown")
}
// If witnesses was generated and stateless self-validation requested, do
// that now. Self validation should *never* run in production, it's more of
// a tight integration to enable running *all* consensus tests through the

View file

@ -25,6 +25,7 @@ import (
"math/rand"
"os"
"path"
"strings"
"sync"
"testing"
"time"
@ -4407,3 +4408,45 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64,
}
}
}
// TestBlockProcessingInterrupt tests graceful shutdown during block processing
func TestBlockProcessingInterrupt(t *testing.T) {
testBlockProcessingInterrupt(t, rawdb.HashScheme)
testBlockProcessingInterrupt(t, rawdb.PathScheme)
}
func testBlockProcessingInterrupt(t *testing.T, scheme string) {
var (
genesis = &Genesis{
BaseFee: big.NewInt(params.InitialBaseFee),
Config: params.AllEthashProtocolChanges,
}
engine = ethash.NewFaker()
)
options := DefaultConfig().WithStateScheme(scheme)
blockchain, _ := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, options)
defer blockchain.Stop()
genDb, blocks := makeBlockChainWithGenesis(genesis, 1, engine, canonicalSeed)
defer genDb.Close()
// Test interrupt during block processing
blockchain.InterruptInsert(true)
parent := blockchain.genesisBlock.Header()
_, err := blockchain.processBlock(parent.Root, blocks[0], true, false)
if err == nil {
t.Error("Expected block processing to be interrupted, but it succeeded")
}
if !strings.Contains(fmt.Sprintf("%v", err), "block processing interrupted during shutdown") {
t.Errorf("Expected 'block processing interrupted during shutdown' error, got: %v", err)
}
// Test processing works after reset
blockchain.InterruptInsert(false)
_, err = blockchain.processBlock(parent.Root, blocks[0], true, false)
if err != nil {
t.Errorf("Failed to process block after reset: %v", err)
}
}