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  }