github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/execution/ingestion/throttle.go (about) 1 package ingestion 2 3 import ( 4 "fmt" 5 "sync" 6 7 "github.com/rs/zerolog" 8 9 "github.com/onflow/flow-go/engine/execution/state" 10 "github.com/onflow/flow-go/model/flow" 11 "github.com/onflow/flow-go/state/protocol" 12 "github.com/onflow/flow-go/storage" 13 ) 14 15 // DefaultCatchUpThreshold is the number of blocks that if the execution is far behind 16 // the finalization then we will only lazy load the next unexecuted finalized 17 // blocks until the execution has caught up 18 const DefaultCatchUpThreshold = 500 19 20 // BlockIDHeight is a helper struct that holds the block ID and height 21 type BlockIDHeight struct { 22 ID flow.Identifier 23 Height uint64 24 } 25 26 func HeaderToBlockIDHeight(header *flow.Header) BlockIDHeight { 27 return BlockIDHeight{ 28 ID: header.ID(), 29 Height: header.Height, 30 } 31 } 32 33 // Throttle is used to throttle the blocks to be added to the processables channel 34 type Throttle interface { 35 // Init initializes the throttle with the processables channel to forward the blocks 36 Init(processables chan<- BlockIDHeight, threshold int) error 37 // OnBlock is called when a block is received, the throttle will check if the execution 38 // is falling far behind the finalization, and add the block to the processables channel 39 // if it's not falling far behind. 40 OnBlock(blockID flow.Identifier, height uint64) error 41 // OnBlockExecuted is called when a block is executed, the throttle will check whether 42 // the execution is caught up with the finalization, and allow all the remaining blocks 43 // to be added to the processables channel. 44 OnBlockExecuted(blockID flow.Identifier, height uint64) error 45 // OnBlockFinalized is called when a block is finalized, the throttle will update the 46 // finalized height. 47 OnBlockFinalized(height uint64) 48 // Done stops the throttle, and stop sending new blocks to the processables channel 49 Done() error 50 } 51 52 var _ Throttle = (*BlockThrottle)(nil) 53 54 // BlockThrottle is a helper struct that helps throttle the unexecuted blocks to be sent 55 // to the block queue for execution. 56 // It is useful for case when execution is falling far behind the finalization, in which case 57 // we want to throttle the blocks to be sent to the block queue for fetching data to execute 58 // them. Without throttle, the block queue will be flooded with blocks, and the network 59 // will be flooded with requests fetching collections, and the EN might quickly run out of memory. 60 type BlockThrottle struct { 61 // when initialized, if the execution is falling far behind the finalization, then 62 // the throttle will only load the next "throttle" number of unexecuted blocks to processables, 63 // and ignore newly received blocks until the execution has caught up the finalization. 64 // During the catching up phase, after a block is executed, the throttle will load the next block 65 // to processables, and keep doing so until the execution has caught up the finalization. 66 // Once caught up, the throttle will process all the remaining unexecuted blocks, including 67 // unfinalized blocks. 68 mu sync.Mutex 69 stopped bool // whether the throttle is stopped, if true, no more block will be loaded 70 loadedAll bool // whether all blocks have been loaded. if true, no block will be throttled. 71 loaded uint64 // the last block height pushed to processables. Used to track if has caught up 72 finalized uint64 // the last finalized height. Used to track if has caught up 73 74 // notifier 75 processables chan<- BlockIDHeight 76 77 // dependencies 78 log zerolog.Logger 79 state protocol.State 80 headers storage.Headers 81 } 82 83 func NewBlockThrottle( 84 log zerolog.Logger, 85 state protocol.State, 86 execState state.ExecutionState, 87 headers storage.Headers, 88 ) (*BlockThrottle, error) { 89 finalizedHead, err := state.Final().Head() 90 if err != nil { 91 return nil, fmt.Errorf("could not get finalized head: %w", err) 92 } 93 94 finalized := finalizedHead.Height 95 executed, err := execState.GetHighestFinalizedExecuted() 96 if err != nil { 97 return nil, fmt.Errorf("could not get highest finalized executed: %w", err) 98 } 99 100 if executed > finalized { 101 return nil, fmt.Errorf("executed finalized %v is greater than finalized %v", executed, finalized) 102 } 103 104 return &BlockThrottle{ 105 loaded: executed, 106 finalized: finalized, 107 stopped: false, 108 loadedAll: false, 109 110 log: log.With().Str("component", "block_throttle").Logger(), 111 state: state, 112 headers: headers, 113 }, nil 114 } 115 116 // inited returns true if the throttle has been inited 117 func (c *BlockThrottle) inited() bool { 118 return c.processables != nil 119 } 120 121 func (c *BlockThrottle) Init(processables chan<- BlockIDHeight, threshold int) error { 122 c.mu.Lock() 123 defer c.mu.Unlock() 124 if c.inited() { 125 return fmt.Errorf("throttle already inited") 126 } 127 128 c.processables = processables 129 130 lastFinalizedToLoad := c.loaded + uint64(threshold) 131 if lastFinalizedToLoad > c.finalized { 132 lastFinalizedToLoad = c.finalized 133 } 134 135 loadedAll := lastFinalizedToLoad == c.finalized 136 137 lg := c.log.With(). 138 Uint64("executed", c.loaded). 139 Uint64("finalized", c.finalized). 140 Uint64("lastFinalizedToLoad", lastFinalizedToLoad). 141 Int("threshold", threshold). 142 Bool("loadedAll", loadedAll). 143 Logger() 144 145 lg.Info().Msgf("finding finalized blocks") 146 147 unexecuted, err := findFinalized(c.state, c.headers, c.loaded, lastFinalizedToLoad) 148 if err != nil { 149 return err 150 } 151 152 if loadedAll { 153 pendings, err := findAllPendingBlocks(c.state, c.headers, c.finalized) 154 if err != nil { 155 return err 156 } 157 unexecuted = append(unexecuted, pendings...) 158 } 159 160 lg = lg.With().Int("unexecuted", len(unexecuted)). 161 Logger() 162 163 lg.Debug().Msgf("initializing throttle") 164 165 // the ingestion core engine must have initialized the 'processables' with 10000 (default) buffer size, 166 // and the 'unexecuted' will only contain up to DefaultCatchUpThreshold (500) blocks, 167 // so pushing all the unexecuted to processables won't be blocked. 168 for _, b := range unexecuted { 169 c.processables <- b 170 c.loaded = b.Height 171 } 172 173 c.loadedAll = loadedAll 174 175 lg.Info().Msgf("throttle initialized unexecuted blocks") 176 177 return nil 178 } 179 180 func (c *BlockThrottle) OnBlockExecuted(_ flow.Identifier, executed uint64) error { 181 c.mu.Lock() 182 defer c.mu.Unlock() 183 184 if !c.inited() { 185 return fmt.Errorf("throttle not inited") 186 } 187 188 if c.stopped { 189 return nil 190 } 191 192 // we have already caught up, ignore 193 if c.caughtUp() { 194 return nil 195 } 196 197 // in this case, c.loaded must be < c.finalized 198 // so we must be able to load the next block 199 err := c.loadNextBlock(c.loaded) 200 if err != nil { 201 return fmt.Errorf("could not load next block: %w", err) 202 } 203 204 if !c.caughtUp() { 205 // after loading the next block, if the execution height is no longer 206 // behind the finalization height 207 return nil 208 } 209 210 c.log.Info().Uint64("executed", executed).Uint64("finalized", c.finalized). 211 Uint64("loaded", c.loaded). 212 Msgf("execution has caught up, processing remaining unexecuted blocks") 213 214 // if the execution have just caught up close enough to the latest finalized blocks, 215 // then process all unexecuted blocks, including finalized unexecuted and pending unexecuted 216 unexecuted, err := findAllPendingBlocks(c.state, c.headers, c.finalized) 217 if err != nil { 218 return fmt.Errorf("could not find unexecuted blocks for processing: %w", err) 219 } 220 221 c.log.Info().Int("unexecuted", len(unexecuted)).Msgf("forwarding unexecuted blocks") 222 223 for _, block := range unexecuted { 224 c.processables <- block 225 c.loaded = block.Height 226 } 227 228 c.log.Info().Msgf("all unexecuted blocks have been processed") 229 230 return nil 231 } 232 233 // Done marks the throttle as done, and no more blocks will be processed 234 func (c *BlockThrottle) Done() error { 235 c.mu.Lock() 236 defer c.mu.Unlock() 237 238 c.log.Info().Msgf("throttle done") 239 240 if !c.inited() { 241 return fmt.Errorf("throttle not inited") 242 } 243 244 c.stopped = true 245 246 return nil 247 } 248 249 func (c *BlockThrottle) OnBlock(blockID flow.Identifier, height uint64) error { 250 c.mu.Lock() 251 defer c.mu.Unlock() 252 c.log.Debug().Msgf("recieved block (%v) height: %v", blockID, height) 253 254 if !c.inited() { 255 return fmt.Errorf("throttle not inited") 256 } 257 258 if c.stopped { 259 return nil 260 } 261 262 // ignore the block if has not caught up. 263 if !c.caughtUp() { 264 return nil 265 } 266 267 // if has caught up, then process the block 268 c.processables <- BlockIDHeight{ 269 ID: blockID, 270 Height: height, 271 } 272 c.loaded = height 273 c.log.Debug().Msgf("processed block (%v), height: %v", blockID, height) 274 275 return nil 276 } 277 278 func (c *BlockThrottle) OnBlockFinalized(finalizedHeight uint64) { 279 c.mu.Lock() 280 defer c.mu.Unlock() 281 if !c.inited() { 282 return 283 } 284 285 if c.caughtUp() { 286 // once caught up, all unfinalized blocks will be loaded, and loadedAll will be set to true 287 // which will always be caught up, so we don't need to update finalized height any more. 288 return 289 } 290 291 if finalizedHeight <= c.finalized { 292 return 293 } 294 295 c.finalized = finalizedHeight 296 } 297 298 func (c *BlockThrottle) loadNextBlock(height uint64) error { 299 c.log.Debug().Uint64("height", height).Msg("loading next block") 300 // load next block 301 next := height + 1 302 blockID, err := c.headers.BlockIDByHeight(next) 303 if err != nil { 304 return fmt.Errorf("could not get block ID by height %v: %w", next, err) 305 } 306 307 c.processables <- BlockIDHeight{ 308 ID: blockID, 309 Height: next, 310 } 311 c.loaded = next 312 c.log.Debug().Uint64("height", next).Msg("loaded next block") 313 314 return nil 315 } 316 317 func (c *BlockThrottle) caughtUp() bool { 318 // load all pending blocks should only happen at most once. 319 // if the execution is already caught up finalization during initialization, 320 // then loadedAll is true, and we don't need to catch up again. 321 // if the execution was falling behind finalization, and has caught up, 322 // then loadedAll is also true, and we don't need to catch up again, because 323 // otherwise we might load the same block twice. 324 if c.loadedAll { 325 return true 326 } 327 328 // in this case, the execution was falling behind the finalization during initialization, 329 // whether the execution has caught up is determined by whether the loaded block is equal 330 // to or above the finalized block. 331 return c.loaded >= c.finalized 332 } 333 334 func findFinalized(state protocol.State, headers storage.Headers, lastExecuted, finalizedHeight uint64) ([]BlockIDHeight, error) { 335 // get finalized height 336 finalized := state.AtHeight(finalizedHeight) 337 final, err := finalized.Head() 338 if err != nil { 339 return nil, fmt.Errorf("could not get finalized block: %w", err) 340 } 341 342 // dynamically bootstrapped execution node will have highest finalized executed as sealed root, 343 // which is lower than finalized root. so we will reload blocks from 344 // [sealedRoot.Height + 1, finalizedRoot.Height] and execute them on startup. 345 unexecutedFinalized := make([]BlockIDHeight, 0) 346 347 // starting from the first unexecuted block, go through each unexecuted and finalized block 348 for height := lastExecuted + 1; height <= final.Height; height++ { 349 finalizedID, err := headers.BlockIDByHeight(height) 350 if err != nil { 351 return nil, fmt.Errorf("could not get block ID by height %v: %w", height, err) 352 } 353 354 unexecutedFinalized = append(unexecutedFinalized, BlockIDHeight{ 355 ID: finalizedID, 356 Height: height, 357 }) 358 } 359 360 return unexecutedFinalized, nil 361 } 362 363 func findAllPendingBlocks(state protocol.State, headers storage.Headers, finalizedHeight uint64) ([]BlockIDHeight, error) { 364 // loaded all pending blocks 365 pendings, err := state.AtHeight(finalizedHeight).Descendants() 366 if err != nil { 367 return nil, fmt.Errorf("could not get descendants of finalized block: %w", err) 368 } 369 370 unexecuted := make([]BlockIDHeight, 0, len(pendings)) 371 for _, id := range pendings { 372 header, err := headers.ByBlockID(id) 373 if err != nil { 374 return nil, fmt.Errorf("could not get header by block ID %v: %w", id, err) 375 } 376 unexecuted = append(unexecuted, BlockIDHeight{ 377 ID: id, 378 Height: header.Height, 379 }) 380 } 381 382 return unexecuted, nil 383 }