Exp timeout (#764)

* feat: write duration calculation in countdown as
interface. add more inputs as function argument

* feat: ExpTimeoutDuration

* fix: three dots usage

* feat: refine exp duration

* feat: add exp timeout config and use it in countdown

* feat: remove const countdown

* feat: remove use of interface in countdown, use error

* fix: countdown reset timer problem

* fix: add default ExpTimeoutConfig for config
This commit is contained in:
wgr523 2025-01-19 23:04:03 +08:00 committed by GitHub
parent 38f4c98740
commit 537dc5e6fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 222 additions and 40 deletions

View file

@ -5,26 +5,40 @@ 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 int
quitc chan chan struct{}
initilised bool
timeoutDuration time.Duration
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
}
func NewCountDown(duration time.Duration) *CountdownTimer {
return &CountdownTimer{
resetc: make(chan int),
quitc: make(chan chan struct{}),
initilised: false,
timeoutDuration: duration,
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.
@ -34,25 +48,25 @@ func (t *CountdownTimer) StopTimer() {
<-q
}
func (t *CountdownTimer) SetTimeoutDuration(duration time.Duration) {
t.timeoutDuration = duration
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{}) {
func (t *CountdownTimer) Reset(i interface{}, currentRound, highestRound types.Round) {
if !t.isInitilised() {
t.setInitilised(true)
go t.startTimer(i)
go t.startTimer(i, currentRound, highestRound)
} else {
t.resetc <- 0
t.resetc <- ResetInfo{currentRound, highestRound}
}
}
// A long running process that
func (t *CountdownTimer) startTimer(i interface{}) {
func (t *CountdownTimer) startTimer(i interface{}, currentRound, highestRound types.Round) {
// Make sure we mark Initilised to false when we quit the countdown
defer t.setInitilised(false)
timer := time.NewTimer(t.timeoutDuration)
timer := time.NewTimer(t.durationHelper.GetTimeoutDuration(currentRound, highestRound))
// We start with a inf loop
for {
select {
@ -69,10 +83,15 @@ func (t *CountdownTimer) startTimer(i interface{}) {
}
log.Debug("OnTimeoutFn processed")
}()
timer.Reset(t.timeoutDuration)
case <-t.resetc:
timer.Reset(t.durationHelper.GetTimeoutDuration(currentRound, highestRound))
case info := <-t.resetc:
log.Debug("Reset countdown timer")
timer.Reset(t.timeoutDuration)
currentRound = info.currentRound
highestRound = info.highestRound
if !timer.Stop() {
<-timer.C
}
timer.Reset(t.durationHelper.GetTimeoutDuration(currentRound, highestRound))
}
}
}

View file

@ -16,9 +16,10 @@ func TestCountdownWillCallback(t *testing.T) {
return nil
}
countdown := NewCountDown(1000 * time.Millisecond)
countdown, err := NewExpCountDown(1000*time.Millisecond, 0, 0)
assert.Nil(t, err)
countdown.OnTimeoutFn = OnTimeoutFn
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
<-called
t.Log("Times up, successfully called OnTimeoutFn")
}
@ -31,11 +32,12 @@ func TestCountdownShouldReset(t *testing.T) {
return nil
}
countdown := NewCountDown(5000 * time.Millisecond)
countdown, err := NewExpCountDown(5000*time.Millisecond, 0, 0)
assert.Nil(t, err)
countdown.OnTimeoutFn = OnTimeoutFn
// Check countdown did not start
assert.False(t, countdown.isInitilised())
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
// Now the countdown should already started
assert.True(t, countdown.isInitilised())
expectedCalledTime := time.Now().Add(9000 * time.Millisecond)
@ -54,7 +56,7 @@ firstReset:
}
break firstReset
case <-resetTimer.C:
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
}
}
@ -79,11 +81,12 @@ func TestCountdownShouldResetEvenIfErrored(t *testing.T) {
return errors.New("ERROR!")
}
countdown := NewCountDown(5000 * time.Millisecond)
countdown, err := NewExpCountDown(5000*time.Millisecond, 0, 0)
assert.Nil(t, err)
countdown.OnTimeoutFn = OnTimeoutFn
// Check countdown did not start
assert.False(t, countdown.isInitilised())
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
// Now the countdown should already started
assert.True(t, countdown.isInitilised())
expectedCalledTime := time.Now().Add(9000 * time.Millisecond)
@ -102,7 +105,7 @@ firstReset:
}
break firstReset
case <-resetTimer.C:
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
}
}
@ -127,11 +130,12 @@ func TestCountdownShouldBeAbleToStop(t *testing.T) {
return nil
}
countdown := NewCountDown(5000 * time.Millisecond)
countdown, err := NewExpCountDown(5000*time.Millisecond, 0, 0)
assert.Nil(t, err)
countdown.OnTimeoutFn = OnTimeoutFn
// Check countdown did not start
assert.False(t, countdown.isInitilised())
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
// Now the countdown should already started
assert.True(t, countdown.isInitilised())
// Try manually stop the timer before it triggers the callback
@ -144,14 +148,15 @@ func TestCountdownShouldBeAbleToStop(t *testing.T) {
func TestCountdownShouldAvoidDeadlock(t *testing.T) {
var fakeI interface{}
called := make(chan int)
countdown := NewCountDown(5000 * time.Millisecond)
countdown, err := NewExpCountDown(5000*time.Millisecond, 0, 0)
assert.Nil(t, err)
OnTimeoutFn := func(time.Time, interface{}) error {
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
called <- 1
return nil
}
countdown.OnTimeoutFn = OnTimeoutFn
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
<-called
}

View file

@ -0,0 +1,70 @@
// A countdown timer that will mostly be used by XDPoS v2 consensus engine
package countdown
import (
"fmt"
"math"
"time"
"github.com/XinFinOrg/XDPoSChain/core/types"
)
const maxExponentUpperbound uint8 = 32
type ExpTimeoutDuration struct {
duration time.Duration
base float64
maxExponent uint8
}
func NewExpTimeoutDuration(duration time.Duration, base float64, maxExponent uint8) (*ExpTimeoutDuration, error) {
d := &ExpTimeoutDuration{
duration: duration,
base: base,
maxExponent: maxExponent,
}
err := d.sanityCheck()
return d, err
}
func (d *ExpTimeoutDuration) sanityCheck() error {
if d.maxExponent >= maxExponentUpperbound {
return fmt.Errorf("max_exponent (%d)= >= max_exponent_upperbound (%d)", d.maxExponent, maxExponentUpperbound)
}
if math.Pow(d.base, float64(d.maxExponent)) >= float64(math.MaxUint32) {
return fmt.Errorf("base^max_exponent (%f^%d) should be less than 2^32", d.base, d.maxExponent)
}
return nil
}
// The inputs should be: currentRound, highestQuorumCert's round
func (d *ExpTimeoutDuration) GetTimeoutDuration(currentRound, highestRound types.Round) time.Duration {
power := float64(1)
// below statement must be true, just to prevent negative result
if highestRound < currentRound {
exp := uint8(currentRound-highestRound) - 1
if exp > d.maxExponent {
exp = d.maxExponent
}
power = math.Pow(d.base, float64(exp))
}
return d.duration * time.Duration(power)
}
func (d *ExpTimeoutDuration) SetParams(duration time.Duration, base float64, maxExponent uint8) error {
prevDuration := d.duration
prevBase := d.base
prevME := d.maxExponent
d.duration = duration
d.base = base
d.maxExponent = maxExponent
// if parameters are wrong, should remain instead of change or panic
if err := d.sanityCheck(); err != nil {
d.duration = prevDuration
d.base = prevBase
d.maxExponent = prevME
return err
}
return nil
}

View file

@ -0,0 +1,65 @@
package countdown
import (
"math"
"testing"
"time"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/stretchr/testify/assert"
)
func TestExpDuration(t *testing.T) {
base := float64(2.0)
max_exponent := uint8(2)
duration := time.Second * 59
helper, err := NewExpTimeoutDuration(duration, base, max_exponent)
assert.Nil(t, err)
// round 10 = 9+1, normal case, should be base
currentRound := types.Round(10)
highestQCRound := types.Round(9)
result := helper.GetTimeoutDuration(currentRound, highestQCRound)
assert.Equal(t, duration, result)
// round 11 = 9+2, already 1 round timeout, should be base*exponent
currentRound++
result = helper.GetTimeoutDuration(currentRound, highestQCRound)
assert.Equal(t, duration*time.Duration(base), result)
// round 12 = 9+3, already 2 rounds timeout, should be base*exponent^2
currentRound++
result = helper.GetTimeoutDuration(currentRound, highestQCRound)
assert.Equal(t, duration*time.Duration(base)*time.Duration(base), result)
// test SetParams
duration++
max_exponent++
base++
helper.SetParams(duration, base, max_exponent)
result = helper.GetTimeoutDuration(currentRound, highestQCRound)
assert.Equal(t, duration*time.Duration(base)*time.Duration(base), result)
// round 14 = 9+5, already 4 rounds timeout, but max_exponent=3, should be base*exponent^3
currentRound++
currentRound++
result = helper.GetTimeoutDuration(currentRound, highestQCRound)
assert.Equal(t, duration*time.Duration(math.Pow(base, float64(3))), result)
// extreme case
helper.SetParams(duration, float64(0), uint8(0))
result = helper.GetTimeoutDuration(currentRound, highestQCRound)
assert.Equal(t, duration, result)
}
func TestInvalidParameter(t *testing.T) {
base := float64(2.0)
max_exponent := uint8(32)
duration := time.Second * 59
_, err := NewExpTimeoutDuration(duration, base, max_exponent)
assert.Error(t, err)
base = float64(3.0)
max_exponent = uint8(21)
duration = time.Second * 59
_, err = NewExpTimeoutDuration(duration, base, max_exponent)
assert.Error(t, err)
}

View file

@ -76,7 +76,10 @@ func New(chainConfig *params.ChainConfig, db ethdb.Database, minePeriodCh chan i
config := chainConfig.XDPoS
// Setup timeoutTimer
duration := time.Duration(config.V2.CurrentConfig.TimeoutPeriod) * time.Second
timeoutTimer := countdown.NewCountDown(duration)
timeoutTimer, err := countdown.NewExpCountDown(duration, config.V2.CurrentConfig.ExpTimeoutConfig.Base, config.V2.CurrentConfig.ExpTimeoutConfig.MaxExponent)
if err != nil {
log.Crit("create exp countdown", "err", err)
}
timeoutPool := utils.NewPool()
votePool := utils.NewPool()
@ -139,8 +142,10 @@ func (x *XDPoS_v2) UpdateParams(header *types.Header) {
// Setup timeoutTimer
duration := time.Duration(x.config.V2.CurrentConfig.TimeoutPeriod) * time.Second
x.timeoutWorker.SetTimeoutDuration(duration)
err = x.timeoutWorker.SetParams(duration, x.config.V2.CurrentConfig.ExpTimeoutConfig.Base, x.config.V2.CurrentConfig.ExpTimeoutConfig.MaxExponent)
if err != nil {
log.Error("[UpdateParams] set params failed", "err", err)
}
// avoid deadlock
go func() {
x.minePeriodCh <- x.config.V2.CurrentConfig.MinePeriod
@ -253,7 +258,7 @@ func (x *XDPoS_v2) initial(chain consensus.ChainReader, header *types.Header) er
}()
// Kick-off the countdown timer
x.timeoutWorker.Reset(chain)
x.timeoutWorker.Reset(chain, 0, 0)
x.isInitilised = true
log.Warn("[initial] finish initialisation")
@ -915,7 +920,7 @@ func (x *XDPoS_v2) setNewRound(blockChainReader consensus.ChainReader, round typ
log.Info("[setNewRound] new round and reset pools and workers", "round", round)
x.currentRound = round
x.timeoutCount = 0
x.timeoutWorker.Reset(blockChainReader)
x.timeoutWorker.Reset(blockChainReader, x.currentRound, x.highestQuorumCert.ProposedBlockInfo.Round)
x.timeoutPool.Clear()
// don't need to clean vote pool, we have other process to clean and it's not good to clean here, some edge case may break
// for example round gets bump during collecting vote, so we have to keep vote.

View file

@ -15,7 +15,7 @@ func (x *XDPoS_v2) SetNewRoundFaker(blockChainReader consensus.ChainReader, newR
defer x.lock.Unlock()
// Reset a bunch of things
if resetTimer {
x.timeoutWorker.Reset(blockChainReader)
x.timeoutWorker.Reset(blockChainReader, 0, 0)
}
x.currentRound = newRound
}

View file

@ -47,6 +47,7 @@ var (
TimeoutSyncThreshold: 3,
TimeoutPeriod: 30,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
2000: {
MaxMasternodes: 108,
@ -55,6 +56,7 @@ var (
TimeoutSyncThreshold: 2,
TimeoutPeriod: 600,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
8000: {
MaxMasternodes: 108,
@ -63,6 +65,7 @@ var (
TimeoutSyncThreshold: 2,
TimeoutPeriod: 60,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
220000: {
MaxMasternodes: 108,
@ -71,6 +74,7 @@ var (
TimeoutSyncThreshold: 2,
TimeoutPeriod: 30,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
460000: {
MaxMasternodes: 108,
@ -79,6 +83,7 @@ var (
TimeoutSyncThreshold: 2,
TimeoutPeriod: 20,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
}
@ -90,6 +95,7 @@ var (
TimeoutSyncThreshold: 3,
TimeoutPeriod: 60,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
900000: {
MaxMasternodes: 108,
@ -98,6 +104,7 @@ var (
TimeoutSyncThreshold: 3,
TimeoutPeriod: 60,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
}
@ -109,6 +116,7 @@ var (
TimeoutSyncThreshold: 3,
TimeoutPeriod: 30,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
}
@ -120,6 +128,7 @@ var (
TimeoutSyncThreshold: 2,
TimeoutPeriod: 4,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
10: {
MaxMasternodes: 18,
@ -128,6 +137,7 @@ var (
TimeoutSyncThreshold: 2,
TimeoutPeriod: 4,
MinePeriod: 3,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
900: {
MaxMasternodes: 20,
@ -136,6 +146,7 @@ var (
TimeoutSyncThreshold: 4,
TimeoutPeriod: 5,
MinePeriod: 2,
ExpTimeoutConfig: ExpTimeoutConfig{Base: 1.0, MaxExponent: 0},
},
}
@ -448,6 +459,13 @@ type V2Config struct {
TimeoutSyncThreshold int `json:"timeoutSyncThreshold"` // send syncInfo after number of timeout
TimeoutPeriod int `json:"timeoutPeriod"` // Duration in ms
CertThreshold float64 `json:"certificateThreshold"` // Necessary number of messages from master nodes to form a certificate
ExpTimeoutConfig ExpTimeoutConfig `json:"expTimeoutConfig"`
}
type ExpTimeoutConfig struct {
Base float64 `json:"base"` // base in base^exponent
MaxExponent uint8 `json:"maxExponent"` // max exponent in base^exponent
}
func (c *XDPoSConfig) String() string {