mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 21:31:37 +00:00
The TestCountdownShouldBeAbleToStop test was failing intermittently due to a race condition in the StopTimer() implementation. Previously, the goroutine used defer to set initilised=false, which executed after close(q) signaled completion to StopTimer(). This allowed StopTimer() to return before the state was properly cleaned up, causing isInitilised() checks to occasionally see stale true values. Fixed by explicitly calling setInitilised(false) before close(q), ensuring the state is updated atomically before StopTimer() returns. This eliminates the race condition and makes the test pass consistently. Verified by running the test 30 times consecutively with no failures.
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
// A countdown timer that will mostly be used by XDPoS v2 consensus engine
|
|
package countdown
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/XinFinOrg/XDPoSChain/core/types"
|
|
"github.com/XinFinOrg/XDPoSChain/log"
|
|
)
|
|
|
|
type TimeoutDurationHelper interface {
|
|
GetTimeoutDuration(types.Round, types.Round) time.Duration
|
|
SetParams(time.Duration, float64, uint8) error
|
|
}
|
|
|
|
type CountdownTimer struct {
|
|
lock sync.RWMutex // Protects the Initilised field
|
|
resetc chan ResetInfo
|
|
quitc chan chan struct{}
|
|
initilised bool
|
|
durationHelper TimeoutDurationHelper
|
|
// Triggered when the countdown timer timeout for the `timeoutDuration` period, it will pass current timestamp to the callback function
|
|
OnTimeoutFn func(time time.Time, i interface{}) error
|
|
}
|
|
|
|
type ResetInfo struct {
|
|
currentRound, highestRound types.Round
|
|
}
|
|
|
|
func NewExpCountDown(duration time.Duration, base float64, max_exponent uint8) (*CountdownTimer, error) {
|
|
durationHelper, err := NewExpTimeoutDuration(duration, base, max_exponent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CountdownTimer{
|
|
resetc: make(chan ResetInfo),
|
|
quitc: make(chan chan struct{}),
|
|
initilised: false,
|
|
durationHelper: durationHelper,
|
|
}, nil
|
|
}
|
|
|
|
// Completely stop the countdown timer from running.
|
|
func (t *CountdownTimer) StopTimer() {
|
|
q := make(chan struct{})
|
|
t.quitc <- q
|
|
<-q
|
|
}
|
|
|
|
func (t *CountdownTimer) SetParams(duration time.Duration, base float64, maxExponent uint8) error {
|
|
return t.durationHelper.SetParams(duration, base, maxExponent)
|
|
}
|
|
|
|
// Reset will start the countdown timer if it's already stopped, or simply reset the countdown time back to the defual `duration`
|
|
func (t *CountdownTimer) Reset(i interface{}, currentRound, highestRound types.Round) {
|
|
if !t.isInitilised() {
|
|
t.setInitilised(true)
|
|
go t.startTimer(i, currentRound, highestRound)
|
|
} else {
|
|
t.resetc <- ResetInfo{currentRound, highestRound}
|
|
}
|
|
}
|
|
|
|
// A long running process that
|
|
func (t *CountdownTimer) startTimer(i interface{}, currentRound, highestRound types.Round) {
|
|
timer := time.NewTimer(t.durationHelper.GetTimeoutDuration(currentRound, highestRound))
|
|
// We start with a inf loop
|
|
for {
|
|
select {
|
|
case q := <-t.quitc:
|
|
log.Debug("Quit countdown timer")
|
|
// Set initilised to false before signaling completion
|
|
// This ensures the state is updated before StopTimer() returns
|
|
t.setInitilised(false)
|
|
close(q)
|
|
return
|
|
case <-timer.C:
|
|
log.Debug("Countdown time reached!")
|
|
go func() {
|
|
err := t.OnTimeoutFn(time.Now(), i)
|
|
if err != nil {
|
|
log.Error("OnTimeoutFn error", "error", err)
|
|
}
|
|
log.Debug("OnTimeoutFn processed")
|
|
}()
|
|
timer.Reset(t.durationHelper.GetTimeoutDuration(currentRound, highestRound))
|
|
case info := <-t.resetc:
|
|
currentRound = info.currentRound
|
|
highestRound = info.highestRound
|
|
duration := t.durationHelper.GetTimeoutDuration(currentRound, highestRound)
|
|
log.Debug("Reset countdown timer", "duration", duration, "currentRound", currentRound, "highestRound", highestRound)
|
|
if !timer.Stop() {
|
|
<-timer.C
|
|
}
|
|
timer.Reset(duration)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the desired value to Initilised with lock to avoid race condition
|
|
func (t *CountdownTimer) setInitilised(value bool) {
|
|
t.lock.Lock()
|
|
defer t.lock.Unlock()
|
|
t.initilised = value
|
|
}
|
|
|
|
func (t *CountdownTimer) isInitilised() bool {
|
|
t.lock.Lock()
|
|
defer t.lock.Unlock()
|
|
return t.initilised
|
|
}
|