github.com/MetalBlockchain/metalgo@v1.11.9/snow/engine/snowman/bootstrap/storage.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package bootstrap
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"time"
    10  
    11  	"go.uber.org/zap"
    12  
    13  	"github.com/MetalBlockchain/metalgo/database"
    14  	"github.com/MetalBlockchain/metalgo/ids"
    15  	"github.com/MetalBlockchain/metalgo/snow/consensus/snowman"
    16  	"github.com/MetalBlockchain/metalgo/snow/engine/common"
    17  	"github.com/MetalBlockchain/metalgo/snow/engine/snowman/block"
    18  	"github.com/MetalBlockchain/metalgo/snow/engine/snowman/bootstrap/interval"
    19  	"github.com/MetalBlockchain/metalgo/utils/logging"
    20  	"github.com/MetalBlockchain/metalgo/utils/set"
    21  	"github.com/MetalBlockchain/metalgo/utils/timer"
    22  )
    23  
    24  const (
    25  	batchWritePeriod      = 64
    26  	iteratorReleasePeriod = 1024
    27  	logPeriod             = 5 * time.Second
    28  	minBlocksToCompact    = 5000
    29  )
    30  
    31  // getMissingBlockIDs returns the ID of the blocks that should be fetched to
    32  // attempt to make a single continuous range from
    33  // (lastAcceptedHeight, highestTrackedHeight].
    34  //
    35  // For example, if the tree currently contains heights [1, 4, 6, 7] and the
    36  // lastAcceptedHeight is 2, this function will return the IDs corresponding to
    37  // blocks [3, 5].
    38  func getMissingBlockIDs(
    39  	ctx context.Context,
    40  	db database.KeyValueReader,
    41  	parser block.Parser,
    42  	tree *interval.Tree,
    43  	lastAcceptedHeight uint64,
    44  ) (set.Set[ids.ID], error) {
    45  	var (
    46  		missingBlocks     set.Set[ids.ID]
    47  		intervals         = tree.Flatten()
    48  		lastHeightToFetch = lastAcceptedHeight + 1
    49  	)
    50  	for _, i := range intervals {
    51  		if i.LowerBound <= lastHeightToFetch {
    52  			continue
    53  		}
    54  
    55  		blkBytes, err := interval.GetBlock(db, i.LowerBound)
    56  		if err != nil {
    57  			return nil, err
    58  		}
    59  
    60  		blk, err := parser.ParseBlock(ctx, blkBytes)
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  
    65  		parentID := blk.Parent()
    66  		missingBlocks.Add(parentID)
    67  	}
    68  	return missingBlocks, nil
    69  }
    70  
    71  // process a series of consecutive blocks starting at [blk].
    72  //
    73  //   - blk is a block that is assumed to have been marked as acceptable by the
    74  //     bootstrapping engine.
    75  //   - ancestors is a set of blocks that can be used to lookup blocks.
    76  //
    77  // If [blk]'s height is <= the last accepted height, then it will be removed
    78  // from the missingIDs set.
    79  //
    80  // Returns a newly discovered blockID that should be fetched.
    81  func process(
    82  	db database.KeyValueWriterDeleter,
    83  	tree *interval.Tree,
    84  	missingBlockIDs set.Set[ids.ID],
    85  	lastAcceptedHeight uint64,
    86  	blk snowman.Block,
    87  	ancestors map[ids.ID]snowman.Block,
    88  ) (ids.ID, bool, error) {
    89  	for {
    90  		// It's possible that missingBlockIDs contain values contained inside of
    91  		// ancestors. So, it's important to remove IDs from the set for each
    92  		// iteration, not just the first block's ID.
    93  		blkID := blk.ID()
    94  		missingBlockIDs.Remove(blkID)
    95  
    96  		height := blk.Height()
    97  		blkBytes := blk.Bytes()
    98  		wantsParent, err := interval.Add(
    99  			db,
   100  			tree,
   101  			lastAcceptedHeight,
   102  			height,
   103  			blkBytes,
   104  		)
   105  		if err != nil || !wantsParent {
   106  			return ids.Empty, false, err
   107  		}
   108  
   109  		// If the parent was provided in the ancestors set, we can immediately
   110  		// process it.
   111  		parentID := blk.Parent()
   112  		parent, ok := ancestors[parentID]
   113  		if !ok {
   114  			return parentID, true, nil
   115  		}
   116  
   117  		blk = parent
   118  	}
   119  }
   120  
   121  // execute all the blocks tracked by the tree. If a block is in the tree but is
   122  // already accepted based on the lastAcceptedHeight, it will be removed from the
   123  // tree but not executed.
   124  //
   125  // execute assumes that getMissingBlockIDs would return an empty set.
   126  //
   127  // TODO: Replace usage of haltable with context cancellation.
   128  func execute(
   129  	ctx context.Context,
   130  	haltable common.Haltable,
   131  	log logging.Func,
   132  	db database.Database,
   133  	parser block.Parser,
   134  	tree *interval.Tree,
   135  	lastAcceptedHeight uint64,
   136  ) error {
   137  	totalNumberToProcess := tree.Len()
   138  	if totalNumberToProcess >= minBlocksToCompact {
   139  		log("compacting database before executing blocks...")
   140  		if err := db.Compact(nil, nil); err != nil {
   141  			// Not a fatal error, log and move on.
   142  			log("failed to compact bootstrap database before executing blocks",
   143  				zap.Error(err),
   144  			)
   145  		}
   146  	}
   147  
   148  	var (
   149  		batch                    = db.NewBatch()
   150  		processedSinceBatchWrite uint
   151  		writeBatch               = func() error {
   152  			if processedSinceBatchWrite == 0 {
   153  				return nil
   154  			}
   155  			processedSinceBatchWrite = 0
   156  
   157  			if err := batch.Write(); err != nil {
   158  				return err
   159  			}
   160  			batch.Reset()
   161  			return nil
   162  		}
   163  
   164  		iterator                      = interval.GetBlockIterator(db)
   165  		processedSinceIteratorRelease uint
   166  
   167  		startTime     = time.Now()
   168  		timeOfNextLog = startTime.Add(logPeriod)
   169  	)
   170  	defer func() {
   171  		iterator.Release()
   172  
   173  		var (
   174  			numProcessed = totalNumberToProcess - tree.Len()
   175  			halted       = haltable.Halted()
   176  		)
   177  		if numProcessed >= minBlocksToCompact && !halted {
   178  			log("compacting database after executing blocks...")
   179  			if err := db.Compact(nil, nil); err != nil {
   180  				// Not a fatal error, log and move on.
   181  				log("failed to compact bootstrap database after executing blocks",
   182  					zap.Error(err),
   183  				)
   184  			}
   185  		}
   186  
   187  		log("executed blocks",
   188  			zap.Uint64("numExecuted", numProcessed),
   189  			zap.Uint64("numToExecute", totalNumberToProcess),
   190  			zap.Bool("halted", halted),
   191  			zap.Duration("duration", time.Since(startTime)),
   192  		)
   193  	}()
   194  
   195  	log("executing blocks",
   196  		zap.Uint64("numToExecute", totalNumberToProcess),
   197  	)
   198  
   199  	for !haltable.Halted() && iterator.Next() {
   200  		blkBytes := iterator.Value()
   201  		blk, err := parser.ParseBlock(ctx, blkBytes)
   202  		if err != nil {
   203  			return err
   204  		}
   205  
   206  		height := blk.Height()
   207  		if err := interval.Remove(batch, tree, height); err != nil {
   208  			return err
   209  		}
   210  
   211  		// Periodically write the batch to disk to avoid memory pressure.
   212  		processedSinceBatchWrite++
   213  		if processedSinceBatchWrite >= batchWritePeriod {
   214  			if err := writeBatch(); err != nil {
   215  				return err
   216  			}
   217  		}
   218  
   219  		// Periodically release and re-grab the database iterator to avoid
   220  		// keeping a reference to an old database revision.
   221  		processedSinceIteratorRelease++
   222  		if processedSinceIteratorRelease >= iteratorReleasePeriod {
   223  			if err := iterator.Error(); err != nil {
   224  				return err
   225  			}
   226  
   227  			// The batch must be written here to avoid re-processing a block.
   228  			if err := writeBatch(); err != nil {
   229  				return err
   230  			}
   231  
   232  			processedSinceIteratorRelease = 0
   233  			iterator.Release()
   234  			// We specify the starting key of the iterator so that the
   235  			// underlying database doesn't need to scan over the, potentially
   236  			// not yet compacted, blocks we just deleted.
   237  			iterator = interval.GetBlockIteratorWithStart(db, height+1)
   238  		}
   239  
   240  		if now := time.Now(); now.After(timeOfNextLog) {
   241  			var (
   242  				numProcessed = totalNumberToProcess - tree.Len()
   243  				eta          = timer.EstimateETA(startTime, numProcessed, totalNumberToProcess)
   244  			)
   245  			log("executing blocks",
   246  				zap.Uint64("numExecuted", numProcessed),
   247  				zap.Uint64("numToExecute", totalNumberToProcess),
   248  				zap.Duration("eta", eta),
   249  			)
   250  			timeOfNextLog = now.Add(logPeriod)
   251  		}
   252  
   253  		if height <= lastAcceptedHeight {
   254  			continue
   255  		}
   256  
   257  		if err := blk.Verify(ctx); err != nil {
   258  			return fmt.Errorf("failed to verify block %s (height=%d, parentID=%s) in bootstrapping: %w",
   259  				blk.ID(),
   260  				height,
   261  				blk.Parent(),
   262  				err,
   263  			)
   264  		}
   265  		if err := blk.Accept(ctx); err != nil {
   266  			return fmt.Errorf("failed to accept block %s (height=%d, parentID=%s) in bootstrapping: %w",
   267  				blk.ID(),
   268  				height,
   269  				blk.Parent(),
   270  				err,
   271  			)
   272  		}
   273  	}
   274  	if err := writeBatch(); err != nil {
   275  		return err
   276  	}
   277  	return iterator.Error()
   278  }