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 }