soft truncate head

This commit is contained in:
jsvisa 2025-09-12 01:36:20 +08:00
parent 5c2bf5b33a
commit 1c431f6f95
12 changed files with 250 additions and 27 deletions

View file

@ -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() {

View file

@ -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()
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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 {

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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 {