package consensus import ( "time" . "github.com/tendermint/go-common" ) type TimeoutTicker interface { Start() (bool, error) Stop() bool Chan() <-chan timeoutInfo // on which to receive a timeout ScheduleTimeout(ti timeoutInfo) // reset the timer } type timeoutTicker struct { BaseService timer *time.Timer tickChan chan timeoutInfo tockChan chan timeoutInfo } func NewTimeoutTicker() TimeoutTicker { tt := &timeoutTicker{ timer: time.NewTimer(0), tickChan: make(chan timeoutInfo, tickTockBufferSize), tockChan: make(chan timeoutInfo, tickTockBufferSize), } if !tt.timer.Stop() { <-tt.timer.C } tt.BaseService = *NewBaseService(log, "TimeoutTicker", tt) return tt } func (t *timeoutTicker) OnStart() error { t.BaseService.OnStart() go t.timeoutRoutine() return nil } func (t *timeoutTicker) OnStop() { t.BaseService.OnStop() } func (t *timeoutTicker) Chan() <-chan timeoutInfo { return t.tockChan } // The timeoutRoutine is alwaya available to read from tickChan (it 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 } // 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() { log.Debug("Starting timeout routine") var ti timeoutInfo for { select { case newti := <-t.tickChan: log.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 } } } ti = newti // if the newti has duration == 0, we relay to the tockChan immediately (no timeout) if ti.Duration == time.Duration(0) { go func(toi timeoutInfo) { t.tockChan <- toi }(ti) continue } log.Debug("Scheduling timeout", "dur", ti.Duration, "height", ti.Height, "round", ti.Round, "step", ti.Step) if !t.timer.Stop() { select { case <-t.timer.C: default: } } t.timer.Reset(ti.Duration) case <-t.timer.C: log.Info("Timed out", "dur", ti.Duration, "height", ti.Height, "round", ti.Round, "step", ti.Step) // go routine here gaurantees 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) { t.tockChan <- toi }(ti) case <-t.Quit: return } } }