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 }