github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/pacemaker/timeout/controller_test.go (about) 1 package timeout 2 3 import ( 4 "math" 5 "testing" 6 "time" 7 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 ) 11 12 const ( 13 minRepTimeout float64 = 100 // Milliseconds 14 maxRepTimeout float64 = 10000 // Milliseconds 15 timeoutAdjustmentFactor float64 = 1.5 // timeout duration adjustment factor 16 happyPathMaxRoundFailures uint64 = 3 // number of failed rounds before increasing timeouts 17 ) 18 19 func initTimeoutController(t *testing.T) *Controller { 20 tc, err := NewConfig(time.Duration(minRepTimeout*1e6), time.Duration(maxRepTimeout*1e6), timeoutAdjustmentFactor, happyPathMaxRoundFailures, time.Duration(maxRepTimeout*1e6)) 21 if err != nil { 22 t.Fail() 23 } 24 return NewController(tc) 25 } 26 27 // Test_TimeoutInitialization timeouts are initialized and reported properly 28 func Test_TimeoutInitialization(t *testing.T) { 29 tc := initTimeoutController(t) 30 assert.Equal(t, tc.replicaTimeout(), minRepTimeout) 31 32 // verify that initially returned timeout channel is closed and `nil` is returned as `TimerInfo` 33 select { 34 case <-tc.Channel(): 35 break 36 default: 37 assert.Fail(t, "timeout channel did not return") 38 } 39 tc.Channel() 40 } 41 42 // Test_TimeoutIncrease verifies that timeout increases exponentially 43 func Test_TimeoutIncrease(t *testing.T) { 44 tc := initTimeoutController(t) 45 46 // advance failed rounds beyond `happyPathMaxRoundFailures`; 47 for r := uint64(0); r < happyPathMaxRoundFailures; r++ { 48 tc.OnTimeout() 49 } 50 51 for r := 1; r <= 10; r += 1 { 52 tc.OnTimeout() 53 assert.Equal(t, 54 tc.replicaTimeout(), 55 minRepTimeout*math.Pow(timeoutAdjustmentFactor, float64(r)), 56 ) 57 } 58 } 59 60 // Test_TimeoutDecrease verifies that timeout decreases exponentially 61 func Test_TimeoutDecrease(t *testing.T) { 62 tc := initTimeoutController(t) 63 64 // failed rounds counter 65 r := uint64(0) 66 67 // advance failed rounds beyond `happyPathMaxRoundFailures`; subsequent progress should reduce timeout again 68 for ; r <= happyPathMaxRoundFailures*2; r++ { 69 tc.OnTimeout() 70 } 71 for ; r > happyPathMaxRoundFailures; r-- { 72 tc.OnProgressBeforeTimeout() 73 assert.Equal(t, 74 tc.replicaTimeout(), 75 minRepTimeout*math.Pow(timeoutAdjustmentFactor, float64(r-1-happyPathMaxRoundFailures)), 76 ) 77 } 78 } 79 80 // Test_MinCutoff verifies that timeout does not decrease below minRepTimeout 81 func Test_MinCutoff(t *testing.T) { 82 tc := initTimeoutController(t) 83 84 for r := uint64(0); r < happyPathMaxRoundFailures; r++ { 85 tc.OnTimeout() // replica timeout doesn't increase since r < happyPathMaxRoundFailures. 86 } 87 88 tc.OnTimeout() // replica timeout increases 100 -> 3/2 * 100 = 150 89 tc.OnTimeout() // replica timeout increases 150 -> 3/2 * 150 = 225 90 tc.OnProgressBeforeTimeout() // replica timeout decreases 225 -> 180 * 2/3 = 150 91 tc.OnProgressBeforeTimeout() // replica timeout decreases 150 -> 153 * 2/3 = 100 92 tc.OnProgressBeforeTimeout() // replica timeout decreases 100 -> 100 * 2/3 = max(66.6, 100) = 100 93 94 tc.OnProgressBeforeTimeout() 95 assert.Equal(t, tc.replicaTimeout(), minRepTimeout) 96 } 97 98 // Test_MaxCutoff verifies that timeout does not increase beyond timeout cap 99 func Test_MaxCutoff(t *testing.T) { 100 tc := initTimeoutController(t) 101 102 // we update the following two values here in the test, which is a naive reference implementation 103 unboundedReferenceTimeout := minRepTimeout 104 r := -1 * int64(happyPathMaxRoundFailures) // only start increasing `unboundedReferenceTimeout` when this becomes positive 105 106 // add timeouts until our `unboundedReferenceTimeout` exceeds the limit 107 for { 108 tc.OnTimeout() 109 if r++; r > 0 { 110 unboundedReferenceTimeout *= timeoutAdjustmentFactor 111 } 112 if unboundedReferenceTimeout > maxRepTimeout { 113 assert.True(t, tc.replicaTimeout() <= maxRepTimeout) 114 return // end of test 115 } 116 } 117 } 118 119 // Test_CombinedIncreaseDecreaseDynamics verifies that timeout increases and decreases 120 // work as expected in combination 121 func Test_CombinedIncreaseDecreaseDynamics(t *testing.T) { 122 increase, decrease := true, false 123 testDynamicSequence := func(seq []bool) { 124 tc := initTimeoutController(t) 125 tc.cfg.HappyPathMaxRoundFailures = 0 // set happy path rounds to zero to simplify calculation 126 numberIncreases, numberDecreases := 0, 0 127 for _, increase := range seq { 128 if increase { 129 numberIncreases += 1 130 tc.OnTimeout() 131 } else { 132 numberDecreases += 1 133 tc.OnProgressBeforeTimeout() 134 } 135 } 136 137 expectedRepTimeout := minRepTimeout * math.Pow(timeoutAdjustmentFactor, float64(numberIncreases-numberDecreases)) 138 numericalError := math.Abs(expectedRepTimeout - tc.replicaTimeout()) 139 require.LessOrEqual(t, numericalError, 1.0) // at most one millisecond numerical error 140 } 141 142 testDynamicSequence([]bool{increase, increase, increase, decrease, decrease, decrease}) 143 testDynamicSequence([]bool{increase, decrease, increase, decrease, increase, decrease}) 144 testDynamicSequence([]bool{increase, increase, increase, increase, increase, decrease}) 145 }