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  }