From 1c431f6f95f2adabb12f4499d6ecdfaae514673c Mon Sep 17 00:00:00 2001 From: jsvisa Date: Fri, 12 Sep 2025 01:36:20 +0800 Subject: [PATCH] soft truncate head --- core/blockchain.go | 25 ++++----- core/rawdb/chain_freezer.go | 8 +++ core/rawdb/database.go | 10 ++++ core/rawdb/freezer.go | 60 ++++++++++++++++++++++ core/rawdb/freezer_memory.go | 88 ++++++++++++++++++++++++++++---- core/rawdb/freezer_resettable.go | 16 ++++++ core/rawdb/table.go | 12 +++++ ethdb/database.go | 8 +++ ethdb/remotedb/remotedb.go | 10 ++++ triedb/database.go | 20 ++++++++ triedb/pathdb/database.go | 13 +++++ triedb/pathdb/database_test.go | 7 ++- 12 files changed, 250 insertions(+), 27 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index e443f5d791..51fd8e026e 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -994,15 +994,6 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha // and the current freezer limit to start nuking it's underflown. pivot = rawdb.ReadLastPivotNumber(bc.db) ) - const truncateInterval = 100 - truncateFn := func() { - // Truncate triedb to align the state histories with the current state. - if bc.triedb.Scheme() == rawdb.PathScheme { - if err := bc.triedb.TruncateHead(); err != nil { - log.Crit("Failed to truncate trie database", "err", err) - } - } - } updateFn := func(db ethdb.KeyValueWriter, header *types.Header) (*types.Header, bool) { // Rewind the blockchain, ensuring we don't end up with a stateless head // block. Note, depth equality is permitted to allow using SetHead as a @@ -1012,9 +1003,11 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha newHeadBlock, rootNumber = bc.rewindHead(header, root) rawdb.WriteHeadBlockHash(db, newHeadBlock.Hash()) - // The truncate costs a lot of time, no need to run for each block - if header.Number.Uint64()%truncateInterval == 0 { - truncateFn() + // Use soft truncate for fast rewind - actual truncation happens at the end + if bc.triedb.Scheme() == rawdb.PathScheme { + if err := bc.triedb.SoftTruncateHead(); err != nil { + log.Warn("Failed to soft truncate trie database", "err", err) + } } // Degrade the chain markers if they are explicitly reverted. @@ -1110,8 +1103,12 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha bc.blockCache.Purge() bc.txLookupCache.Purge() - // Truncate to align the state histories with the current state. - truncateFn() + // Commit all soft truncations to align the state histories with the current state. + if bc.triedb.Scheme() == rawdb.PathScheme { + if err := bc.triedb.CommitTruncation(); err != nil { + log.Crit("Failed to commit trie database truncation", "err", err) + } + } // Clear safe block, finalized block if needed if safe := bc.CurrentSafeBlock(); safe != nil && head < safe.Number.Uint64() { diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go index c12f2ab8fe..8c2f0a870e 100644 --- a/core/rawdb/chain_freezer.go +++ b/core/rawdb/chain_freezer.go @@ -415,6 +415,14 @@ func (f *chainFreezer) TruncateTail(items uint64) (uint64, error) { return f.ancients.TruncateTail(items) } +func (f *chainFreezer) SoftTruncateHead(items uint64) (uint64, error) { + return f.ancients.SoftTruncateHead(items) +} + +func (f *chainFreezer) CommitTruncation() error { + return f.ancients.CommitTruncation() +} + func (f *chainFreezer) SyncAncient() error { return f.ancients.SyncAncient() } diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 626d390c0d..b7e6a4a3a2 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -130,6 +130,16 @@ func (db *nofreezedb) TruncateTail(items uint64) (uint64, error) { return 0, errNotSupported } +// SoftTruncateHead returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) SoftTruncateHead(items uint64) (uint64, error) { + return 0, errNotSupported +} + +// CommitTruncation returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) CommitTruncation() error { + return errNotSupported +} + // SyncAncient returns an error as we don't have a backing chain freezer. func (db *nofreezedb) SyncAncient() error { return errNotSupported diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index a9600c1eef..ef60885b03 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -61,6 +61,7 @@ type Freezer struct { datadir string frozen atomic.Uint64 // Number of items already frozen tail atomic.Uint64 // Number of the first stored item in the freezer + vHead atomic.Uint64 // Virtual head for soft truncation (0 means not set) // This lock synchronizes writers and the truncate operation, as well as // the "atomic" (batched) read operations. @@ -203,6 +204,10 @@ func (f *Freezer) AncientRange(kind string, start, count, maxBytes uint64) ([][] // Ancients returns the length of the frozen items. func (f *Freezer) Ancients() (uint64, error) { + vHead := f.vHead.Load() + if vHead > 0 { + return vHead, nil + } return f.frozen.Load(), nil } @@ -313,6 +318,61 @@ func (f *Freezer) TruncateTail(tail uint64) (uint64, error) { return old, nil } +// SoftTruncateHead marks items above threshold as logically deleted without physical truncation. +// This is much faster than TruncateHead as it only updates a pointer. +func (f *Freezer) SoftTruncateHead(items uint64) (uint64, error) { + if f.readonly { + return 0, errReadOnly + } + + frozen := f.frozen.Load() + vHead := f.vHead.Load() + + // Determine the current effective head + var currentHead uint64 + if vHead > 0 { + currentHead = vHead + } else { + currentHead = frozen + } + + if currentHead <= items { + return currentHead, nil + } + + // Ensure we don't truncate below the tail + tail := f.tail.Load() + if items < tail { + return 0, fmt.Errorf("soft truncate below tail: tail=%d, target=%d", tail, items) + } + + f.vHead.Store(items) + return currentHead, nil +} + +// CommitTruncation performs the actual file truncation to match virtual head. +// This should be called after all soft truncations are complete. +func (f *Freezer) CommitTruncation() error { + if f.readonly { + return errReadOnly + } + + vHead := f.vHead.Load() + if vHead == 0 { + return nil // No pending truncation + } + + // Perform the actual truncation + _, err := f.TruncateHead(vHead) + if err != nil { + return err + } + + // Clear virtual head since we've committed + f.vHead.Store(0) + return nil +} + // SyncAncient flushes all data tables to disk. func (f *Freezer) SyncAncient() error { var errs []error diff --git a/core/rawdb/freezer_memory.go b/core/rawdb/freezer_memory.go index f5621ac4c6..666e04d8d4 100644 --- a/core/rawdb/freezer_memory.go +++ b/core/rawdb/freezer_memory.go @@ -205,6 +205,7 @@ func (b *memoryBatch) commit(freezer *MemoryFreezer) (items uint64, writeSize in type MemoryFreezer struct { items uint64 // Number of items stored tail uint64 // Number of the first stored item in the freezer + vHead uint64 // Virtual head for soft truncation (0 means not set) readonly bool // Flag if the freezer is only for reading lock sync.RWMutex // Lock to protect fields tables map[string]*memoryTable // Tables for storing everything @@ -262,6 +263,9 @@ func (f *MemoryFreezer) Ancients() (uint64, error) { f.lock.RLock() defer f.lock.RUnlock() + if f.vHead > 0 { + return f.vHead, nil + } return f.items, nil } @@ -338,17 +342,10 @@ func (f *MemoryFreezer) TruncateHead(items uint64) (uint64, error) { if f.readonly { return 0, errReadOnly } - old := f.items - if old <= items { - return old, nil - } - for _, table := range f.tables { - if err := table.truncateHead(items); err != nil { - return 0, err - } - } - f.items = items - return old, nil + + // Clear virtual head if we're doing a real truncation + f.vHead = 0 + return f.truncateHead(items) } // TruncateTail discards all data below the provided threshold number. @@ -376,6 +373,75 @@ func (f *MemoryFreezer) TruncateTail(tail uint64) (uint64, error) { return old, nil } +// SoftTruncateHead marks items above threshold as logically deleted without physical truncation. +func (f *MemoryFreezer) SoftTruncateHead(items uint64) (uint64, error) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.readonly { + return 0, errReadOnly + } + + // Determine the current effective head + var currentHead uint64 + if f.vHead > 0 { + currentHead = f.vHead + } else { + currentHead = f.items + } + + if currentHead <= items { + return currentHead, nil + } + + // Ensure we don't truncate below the tail + if items < f.tail { + return 0, fmt.Errorf("soft truncate below tail: tail=%d, target=%d", f.tail, items) + } + + f.vHead = items + return currentHead, nil +} + +// CommitTruncation performs the actual truncation to match virtual head. +func (f *MemoryFreezer) CommitTruncation() error { + f.lock.Lock() + defer f.lock.Unlock() + + if f.readonly { + return errReadOnly + } + + if f.vHead == 0 { + return nil // No pending truncation + } + + // Perform the actual truncation + _, err := f.truncateHead(f.vHead) + if err != nil { + return err + } + + // Clear virtual head since we've committed + f.vHead = 0 + return nil +} + +// truncateHead is the internal method that does the actual head truncation. +func (f *MemoryFreezer) truncateHead(items uint64) (uint64, error) { + old := f.items + if old <= items { + return old, nil + } + for _, table := range f.tables { + if err := table.truncateHead(items); err != nil { + return 0, err + } + } + f.items = items + return old, nil +} + // SyncAncient flushes all data tables to disk. func (f *MemoryFreezer) SyncAncient() error { return nil diff --git a/core/rawdb/freezer_resettable.go b/core/rawdb/freezer_resettable.go index 9db71cfd0e..363eb89fcf 100644 --- a/core/rawdb/freezer_resettable.go +++ b/core/rawdb/freezer_resettable.go @@ -185,6 +185,22 @@ func (f *resettableFreezer) TruncateTail(tail uint64) (uint64, error) { return f.freezer.TruncateTail(tail) } +// SoftTruncateHead marks items above threshold as logically deleted without physical truncation. +func (f *resettableFreezer) SoftTruncateHead(items uint64) (uint64, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.SoftTruncateHead(items) +} + +// CommitTruncation performs the actual file truncation to match any soft truncations. +func (f *resettableFreezer) CommitTruncation() error { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.CommitTruncation() +} + // SyncAncient flushes all data tables to disk. func (f *resettableFreezer) SyncAncient() error { f.lock.RLock() diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 45c8aecf0c..ac6b716f98 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -101,6 +101,18 @@ func (t *table) TruncateTail(items uint64) (uint64, error) { return t.db.TruncateTail(items) } +// SoftTruncateHead is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) SoftTruncateHead(items uint64) (uint64, error) { + return t.db.SoftTruncateHead(items) +} + +// CommitTruncation is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) CommitTruncation() error { + return t.db.CommitTruncation() +} + // SyncAncient is a noop passthrough that just forwards the request to the underlying // database. func (t *table) SyncAncient() error { diff --git a/ethdb/database.go b/ethdb/database.go index e665a84a61..1c973632d0 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -163,6 +163,14 @@ type AncientWriter interface { // // Note that data marked as non-prunable will still be retained and remain accessible. TruncateTail(n uint64) (uint64, error) + + // SoftTruncateHead marks items above threshold as logically deleted without physical truncation. + // This is much faster than TruncateHead as it only updates a pointer. + SoftTruncateHead(n uint64) (uint64, error) + + // CommitTruncation performs the actual file truncation to match any soft truncations. + // This should be called after all soft truncations are complete. + CommitTruncation() error } // AncientWriteOp is given to the function argument of ModifyAncients. diff --git a/ethdb/remotedb/remotedb.go b/ethdb/remotedb/remotedb.go index 7fe154ea95..ff2b67e5d3 100644 --- a/ethdb/remotedb/remotedb.go +++ b/ethdb/remotedb/remotedb.go @@ -140,6 +140,16 @@ func (db *Database) Close() error { return nil } +// SoftTruncateHead returns an error as we don't have a backing chain freezer. +func (db *Database) SoftTruncateHead(items uint64) (uint64, error) { + panic("not supported") +} + +// CommitTruncation returns an error as we don't have a backing chain freezer. +func (db *Database) CommitTruncation() error { + panic("not supported") +} + func New(client *rpc.Client) ethdb.Database { if client == nil { return nil diff --git a/triedb/database.go b/triedb/database.go index dfebd2d2c9..36c3450f77 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -291,6 +291,26 @@ func (db *Database) TruncateHead() error { return pdb.TruncateHead() } +// SoftTruncateHead marks state histories above the bottom state layer as logically deleted +// without physical truncation. This is much faster than TruncateHead. +func (db *Database) SoftTruncateHead() error { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return errors.New("not supported") + } + return pdb.SoftTruncateHead() +} + +// CommitTruncation performs the actual file truncation to match any soft truncations. +// This should be called after all soft truncations are complete. +func (db *Database) CommitTruncation() error { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return errors.New("not supported") + } + return pdb.CommitTruncation() +} + // Recoverable returns the indicator if the specified state is enabled to be // recovered. It's only supported by path-based database and will return an // error for others. diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 0f55cfd881..9582843deb 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -512,6 +512,19 @@ func (db *Database) TruncateHead() error { return err } +// SoftTruncateHead marks state histories above the bottom state layer as logically deleted +// without physical truncation. This is much faster than TruncateHead. +func (db *Database) SoftTruncateHead() error { + _, err := db.stateFreezer.SoftTruncateHead(db.tree.bottom().stateID()) + return err +} + +// CommitTruncation performs the actual file truncation to match any soft truncations. +// This should be called after all soft truncations are complete. +func (db *Database) CommitTruncation() error { + return db.stateFreezer.CommitTruncation() +} + // Recoverable returns the indicator if the specified state is recoverable. // // The supplied root must be a valid trie hash value. diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go index 3bba99b551..d44ad2c2a9 100644 --- a/triedb/pathdb/database_test.go +++ b/triedb/pathdb/database_test.go @@ -600,8 +600,11 @@ func TestDatabaseRollback(t *testing.T) { if err := tester.db.Recover(parent); err != nil { t.Fatalf("Failed to revert db, err: %v", err) } - if err := tester.db.TruncateHead(); err != nil { - t.Fatalf("Failed to truncate head, err: %v", err) + if err := tester.db.SoftTruncateHead(); err != nil { + t.Fatalf("Failed to soft truncate head, err: %v", err) + } + if err := tester.db.CommitTruncation(); err != nil { + t.Fatalf("Failed to commit truncation, err: %v", err) } if i > 0 { if err := tester.verifyState(parent); err != nil {