github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/voteaggregator/vote_collectors.go (about) 1 package voteaggregator 2 3 import ( 4 "fmt" 5 "sync" 6 7 "github.com/rs/zerolog" 8 9 "github.com/onflow/flow-go/consensus/hotstuff" 10 "github.com/onflow/flow-go/module/component" 11 "github.com/onflow/flow-go/module/irrecoverable" 12 "github.com/onflow/flow-go/module/mempool" 13 ) 14 15 // NewCollectorFactoryMethod is a factory method to generate a VoteCollector for concrete view 16 type NewCollectorFactoryMethod = func(view uint64, workers hotstuff.Workers) (hotstuff.VoteCollector, error) 17 18 // VoteCollectors implements management of multiple vote collectors indexed by view. 19 // Implements hotstuff.VoteCollectors interface. Creating a VoteCollector for a 20 // particular view is lazy (instances are created on demand). 21 // This structure is concurrently safe. 22 type VoteCollectors struct { 23 *component.ComponentManager 24 log zerolog.Logger 25 lock sync.RWMutex 26 lowestRetainedView uint64 // lowest view, for which we still retain a VoteCollector and process votes 27 collectors map[uint64]hotstuff.VoteCollector // view -> VoteCollector 28 workerPool hotstuff.Workerpool // for processing votes that are already cached in VoteCollectors and waiting for respective block 29 createCollector NewCollectorFactoryMethod // factory method for creating collectors 30 } 31 32 var _ hotstuff.VoteCollectors = (*VoteCollectors)(nil) 33 34 func NewVoteCollectors(logger zerolog.Logger, lowestRetainedView uint64, workerPool hotstuff.Workerpool, factoryMethod NewCollectorFactoryMethod) *VoteCollectors { 35 // Component manager for wrapped worker pool 36 componentBuilder := component.NewComponentManagerBuilder() 37 componentBuilder.AddWorker(func(ctx irrecoverable.SignalerContext, ready component.ReadyFunc) { 38 ready() 39 <-ctx.Done() // wait for parent context to signal shutdown 40 workerPool.StopWait() // wait till all workers exit 41 }) 42 43 return &VoteCollectors{ 44 log: logger, 45 ComponentManager: componentBuilder.Build(), 46 lowestRetainedView: lowestRetainedView, 47 collectors: make(map[uint64]hotstuff.VoteCollector), 48 workerPool: workerPool, 49 createCollector: factoryMethod, 50 } 51 } 52 53 // GetOrCreateCollector retrieves the hotstuff.VoteCollector for the specified 54 // view or creates one if none exists. 55 // - (collector, true, nil) if no collector can be found by the view, and a new collector was created. 56 // - (collector, false, nil) if the collector can be found by the view 57 // - (nil, false, error) if running into any exception creating the vote collector state machine 58 // 59 // Expected error returns during normal operations: 60 // - mempool.BelowPrunedThresholdError - in case view is lower than lowestRetainedView 61 func (v *VoteCollectors) GetOrCreateCollector(view uint64) (hotstuff.VoteCollector, bool, error) { 62 cachedCollector, hasCachedCollector, err := v.getCollector(view) 63 if err != nil { 64 return nil, false, err 65 } 66 67 if hasCachedCollector { 68 return cachedCollector, false, nil 69 } 70 71 collector, err := v.createCollector(view, v.workerPool) 72 if err != nil { 73 return nil, false, fmt.Errorf("could not create vote collector for view %d: %w", view, err) 74 } 75 76 // Initial check showed that there was no collector. However, it's possible that after the 77 // initial check but before acquiring the lock to add the newly-created collector, another 78 // goroutine already added the needed collector. Hence, check again after acquiring the lock: 79 v.lock.Lock() 80 defer v.lock.Unlock() 81 82 clr, found := v.collectors[view] 83 if found { 84 return clr, false, nil 85 } 86 87 v.collectors[view] = collector 88 return collector, true, nil 89 } 90 91 // getCollector retrieves hotstuff.VoteCollector from local cache in concurrent safe way. 92 // Performs check for lowestRetainedView. 93 // Expected error returns during normal operations: 94 // - mempool.BelowPrunedThresholdError - in case view is lower than lowestRetainedView 95 func (v *VoteCollectors) getCollector(view uint64) (hotstuff.VoteCollector, bool, error) { 96 v.lock.RLock() 97 defer v.lock.RUnlock() 98 if view < v.lowestRetainedView { 99 return nil, false, mempool.NewBelowPrunedThresholdErrorf("cannot retrieve collector for pruned view %d (lowest retained view %d)", view, v.lowestRetainedView) 100 } 101 102 clr, found := v.collectors[view] 103 104 return clr, found, nil 105 } 106 107 // PruneUpToView prunes the vote collectors with views _below_ the given value, i.e. 108 // we only retain and process whose view is equal or larger than `lowestRetainedView`. 109 // If `lowestRetainedView` is smaller than the previous value, the previous value is 110 // kept and the method call is a NoOp. 111 func (v *VoteCollectors) PruneUpToView(lowestRetainedView uint64) { 112 v.lock.Lock() 113 defer v.lock.Unlock() 114 if v.lowestRetainedView >= lowestRetainedView { 115 return 116 } 117 if len(v.collectors) == 0 { 118 v.lowestRetainedView = lowestRetainedView 119 return 120 } 121 122 sizeBefore := len(v.collectors) 123 124 // to optimize the pruning of large view-ranges, we compare: 125 // * the number of views for which we have collectors: len(v.collectors) 126 // * the number of views that need to be pruned: view-v.lowestRetainedView 127 // We iterate over the dimension which is smaller. 128 if uint64(len(v.collectors)) < lowestRetainedView-v.lowestRetainedView { 129 for w := range v.collectors { 130 if w < lowestRetainedView { 131 delete(v.collectors, w) 132 } 133 } 134 } else { 135 for w := v.lowestRetainedView; w < lowestRetainedView; w++ { 136 delete(v.collectors, w) 137 } 138 } 139 from := v.lowestRetainedView 140 v.lowestRetainedView = lowestRetainedView 141 142 v.log.Debug(). 143 Uint64("prior_lowest_retained_view", from). 144 Uint64("lowest_retained_view", lowestRetainedView). 145 Int("prior_size", sizeBefore). 146 Int("size", len(v.collectors)). 147 Msgf("pruned vote collectors") 148 }