github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/bft/consensus/ticker.go (about)

     1  package consensus
     2  
     3  import (
     4  	"log/slog"
     5  	"time"
     6  
     7  	"github.com/gnolang/gno/tm2/pkg/service"
     8  )
     9  
    10  var tickBufferSize = 10
    11  
    12  // TimeoutTicker is a timer that schedules timeouts
    13  // conditional on the height/round/step in the timeoutInfo.
    14  // The timeoutInfo.Duration may be non-positive.
    15  type TimeoutTicker interface {
    16  	Start() error
    17  	Stop() error
    18  	Chan() <-chan timeoutInfo       // on which to receive a timeout
    19  	ScheduleTimeout(ti timeoutInfo) // reset the timer
    20  
    21  	SetLogger(*slog.Logger)
    22  }
    23  
    24  // timeoutTicker wraps time.Timer,
    25  // scheduling timeouts only for greater height/round/step
    26  // than what it's already seen.
    27  // Timeouts are scheduled along the tickChan,
    28  // and fired on the tockChan.
    29  type timeoutTicker struct {
    30  	service.BaseService
    31  
    32  	timer    *time.Timer
    33  	tickChan chan timeoutInfo // for scheduling timeouts
    34  	tockChan chan timeoutInfo // for notifying about them
    35  }
    36  
    37  // NewTimeoutTicker returns a new TimeoutTicker.
    38  func NewTimeoutTicker() TimeoutTicker {
    39  	tt := &timeoutTicker{
    40  		timer:    time.NewTimer(0),
    41  		tickChan: make(chan timeoutInfo, tickBufferSize),
    42  		tockChan: make(chan timeoutInfo, 1),
    43  	}
    44  	tt.BaseService = *service.NewBaseService(nil, "TimeoutTicker", tt)
    45  	tt.stopTimer() // don't want to fire until the first scheduled timeout
    46  	return tt
    47  }
    48  
    49  // OnStart implements service.Service. It starts the timeout routine.
    50  func (t *timeoutTicker) OnStart() error {
    51  	go t.timeoutRoutine()
    52  
    53  	return nil
    54  }
    55  
    56  // OnStop implements service.Service. It stops the timeout routine.
    57  func (t *timeoutTicker) OnStop() {
    58  	t.BaseService.OnStop()
    59  	t.stopTimer()
    60  }
    61  
    62  // Chan returns a channel on which timeouts are sent.
    63  func (t *timeoutTicker) Chan() <-chan timeoutInfo {
    64  	return t.tockChan
    65  }
    66  
    67  // ScheduleTimeout schedules a new timeout by sending on the internal tickChan.
    68  // The timeoutRoutine is always available to read from tickChan, so this won't block.
    69  // The scheduling may fail if the timeoutRoutine has already scheduled a timeout for a later height/round/step.
    70  func (t *timeoutTicker) ScheduleTimeout(ti timeoutInfo) {
    71  	t.tickChan <- ti
    72  }
    73  
    74  // -------------------------------------------------------------
    75  
    76  // stop the timer and drain if necessary
    77  func (t *timeoutTicker) stopTimer() {
    78  	// Stop() returns false if it was already fired or was stopped
    79  	if !t.timer.Stop() {
    80  		select {
    81  		case <-t.timer.C:
    82  		default:
    83  			t.Logger.Debug("Timer already stopped")
    84  		}
    85  	}
    86  }
    87  
    88  // send on tickChan to start a new timer.
    89  // timers are interrupted and replaced by new ticks from later steps
    90  // timeouts of 0 on the tickChan will be immediately relayed to the tockChan
    91  func (t *timeoutTicker) timeoutRoutine() {
    92  	t.Logger.Debug("Starting timeout routine")
    93  	var ti timeoutInfo
    94  	var ch chan timeoutInfo // to send, if not nil.
    95  	for {
    96  		select {
    97  		case newti := <-t.tickChan:
    98  			t.Logger.Debug("Received tick", "old_ti", ti, "new_ti", newti)
    99  
   100  			// ignore tickers for old height/round/step
   101  			if newti.Height < ti.Height {
   102  				continue
   103  			} else if newti.Height == ti.Height {
   104  				if newti.Round < ti.Round {
   105  					continue
   106  				} else if newti.Round == ti.Round {
   107  					if ti.Step > 0 && newti.Step <= ti.Step {
   108  						continue
   109  					}
   110  				}
   111  			}
   112  
   113  			// stop the last timer
   114  			t.stopTimer()
   115  
   116  			// update timeoutInfo and reset timer
   117  			// NOTE time.Timer allows duration to be non-positive
   118  			ti = newti
   119  			t.timer.Reset(ti.Duration)
   120  			t.Logger.Debug("Scheduled timeout", "dur", ti.Duration, "height", ti.Height, "round", ti.Round, "step", ti.Step)
   121  		case <-t.timer.C:
   122  			t.Logger.Info("Timed out", "dur", ti.Duration, "height", ti.Height, "round", ti.Round, "step", ti.Step)
   123  			select {
   124  			case t.tockChan <- ti:
   125  				// done!
   126  			default:
   127  				ch = t.tockChan
   128  			}
   129  		case ch <- ti:
   130  			ch = nil
   131  		case <-t.Quit():
   132  			return
   133  		}
   134  	}
   135  }