core/state/snapshot: properly stop snapshot generation

This commit is contained in:
Jonathan Oppenheimer 2026-01-06 15:09:05 -05:00
parent a8a4804895
commit 1e675e3f85
No known key found for this signature in database
GPG key ID: E4CEF9010EB8B740
2 changed files with 52 additions and 6 deletions

View file

@ -49,6 +49,10 @@ type diskLayer struct {
// Reset() in order to not leak memory.
// OBS: It does not invoke Close on the diskdb
func (dl *diskLayer) Release() error {
// Stop any ongoing snapshot generation to prevent it from accessing
// the database after it's closed during shutdown
dl.stopGeneration()
if dl.cache != nil {
dl.cache.Reset()
}
@ -186,12 +190,9 @@ func (dl *diskLayer) Update(blockHash common.Hash, accounts map[common.Hash][]by
// stopGeneration aborts the state snapshot generation if it is currently running.
func (dl *diskLayer) stopGeneration() {
dl.lock.RLock()
generating := dl.genMarker != nil
dl.lock.RUnlock()
if !generating {
return
}
// Check if generation goroutine is running by checking if genAbort channel exists.
// Note: genMarker can be nil even when the generator is still running (waiting
// for abort signal after completing generation), so we check genAbort instead.
if dl.genAbort != nil {
abort := make(chan *generatorStats)
dl.genAbort <- abort

View file

@ -986,3 +986,48 @@ func testGenerateBrokenSnapshotWithDanglingStorage(t *testing.T, scheme string)
snap.genAbort <- stop
<-stop
}
// TestReleaseStopsGeneration verifies that Release() properly stops ongoing
// snapshot generation without hanging. This prevents a race condition during
// shutdown where the generator could access the database after it's closed.
//
// The generator goroutine waits for an abort signal even after completing
// generation successfully. Without calling stopGeneration(), Release() would
// leave the generator hanging forever, which could prevent clean shutdown.
func TestReleaseStopsGeneration(t *testing.T) {
testReleaseStopsGeneration(t, rawdb.HashScheme)
testReleaseStopsGeneration(t, rawdb.PathScheme)
}
func testReleaseStopsGeneration(t *testing.T, scheme string) {
var helper = newHelper(scheme)
stRoot := helper.makeStorageTrie("", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, false)
helper.addTrieAccount("acc-1", &types.StateAccount{Balance: uint256.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
helper.addTrieAccount("acc-2", &types.StateAccount{Balance: uint256.NewInt(2), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()})
helper.addTrieAccount("acc-3", &types.StateAccount{Balance: uint256.NewInt(3), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
helper.makeStorageTrie("acc-1", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
helper.makeStorageTrie("acc-3", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
_, snap := helper.CommitAndGenerate()
select {
case <-snap.genPending:
case <-time.After(3 * time.Second):
t.Fatal("Snapshot generation failed")
}
// Call Release() - this should stop generation gracefully without hanging
done := make(chan struct{})
go func() {
snap.Release()
close(done)
}()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatal("Release() hung - stopGeneration() was likely not called")
}
}