Skip to content

Commit

Permalink
Exp timeout (#764)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wgr523 authored Jan 19, 2025
1 parent 38f4c98 commit 537dc5e
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 40 deletions.
61 changes: 40 additions & 21 deletions common/countdown/countdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand All @@ -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))
}
}
}
Expand Down
31 changes: 18 additions & 13 deletions common/countdown/countdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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)
Expand All @@ -54,7 +56,7 @@ firstReset:
}
break firstReset
case <-resetTimer.C:
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
}
}

Expand All @@ -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)
Expand All @@ -102,7 +105,7 @@ firstReset:
}
break firstReset
case <-resetTimer.C:
countdown.Reset(fakeI)
countdown.Reset(fakeI, 0, 0)
}
}

Expand All @@ -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
Expand All @@ -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
}
70 changes: 70 additions & 0 deletions common/countdown/exp_duration.go
Original file line number Diff line number Diff line change
@@ -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
}
65 changes: 65 additions & 0 deletions common/countdown/exp_duration_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
15 changes: 10 additions & 5 deletions consensus/XDPoS/engines/engine_v2/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion consensus/XDPoS/engines/engine_v2/testing_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 537dc5e

Please sign in to comment.