package consensus import ( "context" "time" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/libs/service" ) var ( tickTockBufferSize = 10 ) // TimeoutTicker is a timer that schedules timeouts // conditional on the height/round/step in the timeoutInfo. // The timeoutInfo.Duration may be non-positive. type TimeoutTicker interface { Start(context.Context) error Stop() IsRunning() bool Chan() <-chan timeoutInfo // on which to receive a timeout ScheduleTimeout(ti timeoutInfo) // reset the timer } // timeoutTicker wraps time.Timer, // scheduling timeouts only for greater height/round/step // than what it's already seen. // Timeouts are scheduled along the tickChan, // and fired on the tockChan. type timeoutTicker struct { service.BaseService logger log.Logger timer *time.Timer tickChan chan timeoutInfo // for scheduling timeouts tockChan chan timeoutInfo // for notifying about them } // NewTimeoutTicker returns a new TimeoutTicker. func NewTimeoutTicker(logger log.Logger) TimeoutTicker { tt := &timeoutTicker{ logger: logger, timer: time.NewTimer(0), tickChan: make(chan timeoutInfo, tickTockBufferSize), tockChan: make(chan timeoutInfo, tickTockBufferSize), } tt.BaseService = *service.NewBaseService(logger, "TimeoutTicker", tt) tt.stopTimer() // don't want to fire until the first scheduled timeout return tt } // OnStart implements service.Service. It starts the timeout routine. func (t *timeoutTicker) OnStart(ctx context.Context) error { go t.timeoutRoutine(ctx) return nil } // OnStop implements service.Service. It stops the timeout routine. func (t *timeoutTicker) OnStop() { t.stopTimer() } // Chan returns a channel on which timeouts are sent. func (t *timeoutTicker) Chan() <-chan timeoutInfo { return t.tockChan } // ScheduleTimeout schedules a new timeout by sending on the internal tickChan. // The timeoutRoutine is always available to read from tickChan, so this won't block. // The scheduling may fail if the timeoutRoutine has already scheduled a timeout for a later height/round/step. func (t *timeoutTicker) ScheduleTimeout(ti timeoutInfo) { t.tickChan <- ti } //------------------------------------------------------------- // stop the timer and drain if necessary func (t *timeoutTicker) stopTimer() { // Stop() returns false if it was already fired or was stopped if !t.timer.Stop() { select { case <-t.timer.C: default: } } } // send on tickChan to start a new timer. // timers are interupted and replaced by new ticks from later steps // timeouts of 0 on the tickChan will be immediately relayed to the tockChan func (t *timeoutTicker) timeoutRoutine(ctx context.Context) { var ti timeoutInfo for { select { case newti := <-t.tickChan: t.logger.Debug("Received tick", "old_ti", ti, "new_ti", newti) // ignore tickers for old height/round/step if newti.Height < ti.Height { continue } else if newti.Height == ti.Height { if newti.Round < ti.Round { continue } else if newti.Round == ti.Round { if ti.Step > 0 && newti.Step <= ti.Step { continue } } } // stop the last timer t.stopTimer() // update timeoutInfo and reset timer // NOTE time.Timer allows duration to be non-positive ti = newti t.timer.Reset(ti.Duration) t.logger.Debug("Scheduled timeout", "dur", ti.Duration, "height", ti.Height, "round", ti.Round, "step", ti.Step) case <-t.timer.C: t.logger.Info("Timed out", "dur", ti.Duration, "height", ti.Height, "round", ti.Round, "step", ti.Step) // go routine here guarantees timeoutRoutine doesn't block. // Determinism comes from playback in the receiveRoutine. // We can eliminate it by merging the timeoutRoutine into receiveRoutine // and managing the timeouts ourselves with a millisecond ticker go func(toi timeoutInfo) { select { case t.tockChan <- toi: case <-ctx.Done(): } }(ti) case <-ctx.Done(): return } } }