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  }