package consensus
|
|
|
|
import (
|
|
"time"
|
|
|
|
. "github.com/tendermint/tmlibs/common"
|
|
"github.com/tendermint/tmlibs/log"
|
|
)
|
|
|
|
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() (bool, error)
|
|
Stop() bool
|
|
Chan() <-chan timeoutInfo // on which to receive a timeout
|
|
ScheduleTimeout(ti timeoutInfo) // reset the timer
|
|
|
|
SetLogger(log.Logger)
|
|
}
|
|
|
|
// 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 {
|
|
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),
|
|
}
|
|
tt.BaseService = *NewBaseService(nil, "TimeoutTicker", tt)
|
|
tt.stopTimer() // don't want to fire until the first scheduled timeout
|
|
return tt
|
|
}
|
|
|
|
func (t *timeoutTicker) OnStart() error {
|
|
|
|
go t.timeoutRoutine()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *timeoutTicker) OnStop() {
|
|
t.BaseService.OnStop()
|
|
t.stopTimer()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
//-------------------------------------------------------------
|
|
|
|
// 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:
|
|
t.Logger.Debug("Timer already stopped")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
t.Logger.Debug("Starting timeout routine")
|
|
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 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
|
|
}
|
|
}
|
|
}
|