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 }