github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/consensus/approvals/request_tracker.go (about)

     1  package approvals
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"time"
     7  
     8  	"github.com/onflow/flow-go/model/flow"
     9  	"github.com/onflow/flow-go/module/mempool"
    10  	"github.com/onflow/flow-go/storage"
    11  	"github.com/onflow/flow-go/utils/rand"
    12  )
    13  
    14  /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    15  RequestTrackerItem
    16  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    17  
    18  // RequestTrackerItem is an object that keeps track of how many times a request
    19  // has been made, as well as the time until a new request can be made.
    20  // It is not concurrency-safe.
    21  type RequestTrackerItem struct {
    22  	Requests          uint
    23  	NextTimeout       time.Time
    24  	blackoutPeriodMin int
    25  	blackoutPeriodMax int
    26  }
    27  
    28  // NewRequestTrackerItem instantiates a new RequestTrackerItem where the
    29  // NextTimeout is evaluated to the current time plus a random blackout period
    30  // contained between min and max.
    31  func NewRequestTrackerItem(blackoutPeriodMin, blackoutPeriodMax int) (RequestTrackerItem, error) {
    32  	item := RequestTrackerItem{
    33  		blackoutPeriodMin: blackoutPeriodMin,
    34  		blackoutPeriodMax: blackoutPeriodMax,
    35  	}
    36  	var err error
    37  	item.NextTimeout, err = randBlackout(blackoutPeriodMin, blackoutPeriodMax)
    38  	if err != nil {
    39  		return RequestTrackerItem{}, err
    40  	}
    41  
    42  	return item, err
    43  }
    44  
    45  // Update creates a _new_ RequestTrackerItem with incremented request number and updated NextTimeout.
    46  // No errors are expected during normal operation.
    47  func (i RequestTrackerItem) Update() (RequestTrackerItem, error) {
    48  	i.Requests++
    49  	var err error
    50  	i.NextTimeout, err = randBlackout(i.blackoutPeriodMin, i.blackoutPeriodMax)
    51  	if err != nil {
    52  		return RequestTrackerItem{}, fmt.Errorf("could not get next timeout: %w", err)
    53  	}
    54  	return i, nil
    55  }
    56  
    57  func (i RequestTrackerItem) IsBlackout() bool {
    58  	return time.Now().Before(i.NextTimeout)
    59  }
    60  
    61  // No errors are expected during normal operation.
    62  func randBlackout(min int, max int) (time.Time, error) {
    63  	random, err := rand.Uint64n(uint64(max - min + 1))
    64  	if err != nil {
    65  		return time.Now(), fmt.Errorf("failed to generate blackout: %w", err)
    66  	}
    67  	blackoutSeconds := random + uint64(min)
    68  	blackout := time.Now().Add(time.Duration(blackoutSeconds) * time.Second)
    69  	return blackout, nil
    70  }
    71  
    72  /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    73  RequestTracker
    74  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    75  
    76  // RequestTracker is an index of RequestTrackerItems indexed by execution result
    77  // Index on result ID, incorporated block ID and chunk index.
    78  // Is concurrency-safe.
    79  type RequestTracker struct {
    80  	headers           storage.Headers
    81  	index             map[flow.Identifier]map[flow.Identifier]map[uint64]RequestTrackerItem
    82  	blackoutPeriodMin int
    83  	blackoutPeriodMax int
    84  	lock              sync.Mutex
    85  	byHeight          map[uint64]flow.IdentifierList
    86  	lowestHeight      uint64
    87  }
    88  
    89  // NewRequestTracker instantiates a new RequestTracker with blackout periods
    90  // between min and max seconds.
    91  func NewRequestTracker(headers storage.Headers, blackoutPeriodMin, blackoutPeriodMax int) *RequestTracker {
    92  	return &RequestTracker{
    93  		headers:           headers,
    94  		index:             make(map[flow.Identifier]map[flow.Identifier]map[uint64]RequestTrackerItem),
    95  		byHeight:          make(map[uint64]flow.IdentifierList),
    96  		blackoutPeriodMin: blackoutPeriodMin,
    97  		blackoutPeriodMax: blackoutPeriodMax,
    98  	}
    99  }
   100  
   101  // TryUpdate tries to update tracker item if it's not in blackout period. Returns the tracker item for a specific chunk
   102  // (creates it if it doesn't exists) and whenever request item was successfully updated or not.
   103  // Since RequestTracker prunes items by height it can't accept items for height lower than cached lowest height.
   104  // If height of executed block pointed by execution result is smaller than the lowest height, sentinel mempool.BelowPrunedThresholdError is returned.
   105  // In case execution result points to unknown executed block exception will be returned.
   106  func (rt *RequestTracker) TryUpdate(result *flow.ExecutionResult, incorporatedBlockID flow.Identifier, chunkIndex uint64) (RequestTrackerItem, bool, error) {
   107  	resultID := result.ID()
   108  	rt.lock.Lock()
   109  	defer rt.lock.Unlock()
   110  	item, ok := rt.index[resultID][incorporatedBlockID][chunkIndex]
   111  	var err error
   112  
   113  	if !ok {
   114  		item, err = NewRequestTrackerItem(rt.blackoutPeriodMin, rt.blackoutPeriodMax)
   115  		if err != nil {
   116  			return item, false, fmt.Errorf("could not create tracker item: %w", err)
   117  		}
   118  		err = rt.set(resultID, result.BlockID, incorporatedBlockID, chunkIndex, item)
   119  		if err != nil {
   120  			return item, false, fmt.Errorf("could not set created tracker item: %w", err)
   121  		}
   122  	}
   123  
   124  	canUpdate := !item.IsBlackout()
   125  	if canUpdate {
   126  		item, err = item.Update()
   127  		if err != nil {
   128  			return item, false, fmt.Errorf("could not update tracker item: %w", err)
   129  		}
   130  		rt.index[resultID][incorporatedBlockID][chunkIndex] = item
   131  	}
   132  
   133  	return item, canUpdate, nil
   134  }
   135  
   136  // set inserts or updates the tracker item for a specific chunk.
   137  func (rt *RequestTracker) set(resultID, executedBlockID, incorporatedBlockID flow.Identifier, chunkIndex uint64, item RequestTrackerItem) error {
   138  	executedBlock, err := rt.headers.ByBlockID(executedBlockID)
   139  	if err != nil {
   140  		return fmt.Errorf("could not retrieve block by id %v: %w", executedBlockID, err)
   141  	}
   142  
   143  	if executedBlock.Height < rt.lowestHeight {
   144  		return mempool.NewBelowPrunedThresholdErrorf(
   145  			"adding height: %d, existing height: %d", executedBlock.Height, rt.lowestHeight)
   146  	}
   147  
   148  	level1, level1found := rt.index[resultID]
   149  	if !level1found {
   150  		level1 = make(map[flow.Identifier]map[uint64]RequestTrackerItem)
   151  		rt.index[resultID] = level1
   152  	}
   153  	level2, level2found := level1[incorporatedBlockID]
   154  	if !level2found {
   155  		level2 = make(map[uint64]RequestTrackerItem)
   156  		level1[incorporatedBlockID] = level2
   157  	}
   158  	level2[chunkIndex] = item
   159  
   160  	// update secondary height based index for correct pruning
   161  	rt.byHeight[executedBlock.Height] = append(rt.byHeight[executedBlock.Height], resultID)
   162  
   163  	return nil
   164  }
   165  
   166  // GetAllIds returns all result IDs that we are indexing
   167  func (rt *RequestTracker) GetAllIds() []flow.Identifier {
   168  	rt.lock.Lock()
   169  	defer rt.lock.Unlock()
   170  	ids := make([]flow.Identifier, 0, len(rt.index))
   171  	for resultID := range rt.index {
   172  		ids = append(ids, resultID)
   173  	}
   174  	return ids
   175  }
   176  
   177  // Remove removes all entries pertaining to an execution result
   178  func (rt *RequestTracker) Remove(resultIDs ...flow.Identifier) {
   179  	if len(resultIDs) == 0 {
   180  		return
   181  	}
   182  	rt.lock.Lock()
   183  	defer rt.lock.Unlock()
   184  	for _, resultID := range resultIDs {
   185  		delete(rt.index, resultID)
   186  	}
   187  }
   188  
   189  // PruneUpToHeight remove all tracker items for blocks whose height is strictly
   190  // smaller that height. Note: items for blocks at height are retained.
   191  // After pruning, items for blocks below the given height are dropped.
   192  //
   193  // Monotonicity Requirement:
   194  // The pruned height cannot decrease, as we cannot recover already pruned elements.
   195  // If `height` is smaller than the previous value, the previous value is kept
   196  // and the sentinel mempool.BelowPrunedThresholdError is returned.
   197  func (rt *RequestTracker) PruneUpToHeight(height uint64) error {
   198  	rt.lock.Lock()
   199  	defer rt.lock.Unlock()
   200  	if height < rt.lowestHeight {
   201  		return mempool.NewBelowPrunedThresholdErrorf(
   202  			"pruning height: %d, existing height: %d", height, rt.lowestHeight)
   203  	}
   204  
   205  	if len(rt.index) == 0 {
   206  		rt.lowestHeight = height
   207  		return nil
   208  	}
   209  
   210  	// Optimization: if there are less elements in the `byHeight` map
   211  	// than the height range to prune: inspect each map element.
   212  	// Otherwise, go through each height to prune.
   213  	if uint64(len(rt.byHeight)) < height-rt.lowestHeight {
   214  		for h := range rt.byHeight {
   215  			if h < height {
   216  				rt.removeByHeight(h)
   217  			}
   218  		}
   219  	} else {
   220  		for h := rt.lowestHeight; h < height; h++ {
   221  			rt.removeByHeight(h)
   222  		}
   223  	}
   224  	rt.lowestHeight = height
   225  	return nil
   226  }
   227  
   228  func (rt *RequestTracker) removeByHeight(height uint64) {
   229  	for _, resultID := range rt.byHeight[height] {
   230  		delete(rt.index, resultID)
   231  	}
   232  	delete(rt.byHeight, height)
   233  }