github.com/koko1123/flow-go-1@v0.29.6/engine/consensus/approvals/request_tracker.go (about)

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