github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/timeoutcollector/timeout_cache_test.go (about)

     1  package timeoutcollector
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/onflow/flow-go/consensus/hotstuff/helper"
    12  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    13  	"github.com/onflow/flow-go/utils/unittest"
    14  )
    15  
    16  // TestTimeoutObjectsCache_View tests that View returns same value that was set by constructor
    17  func TestTimeoutObjectsCache_View(t *testing.T) {
    18  	view := uint64(100)
    19  	cache := NewTimeoutObjectsCache(view)
    20  	require.Equal(t, view, cache.View())
    21  }
    22  
    23  // TestTimeoutObjectsCache_AddTimeoutObjectRepeatedTimeout tests that AddTimeoutObject skips duplicated timeouts
    24  func TestTimeoutObjectsCache_AddTimeoutObjectRepeatedTimeout(t *testing.T) {
    25  	t.Parallel()
    26  
    27  	view := uint64(100)
    28  	cache := NewTimeoutObjectsCache(view)
    29  	timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(view))
    30  
    31  	require.NoError(t, cache.AddTimeoutObject(timeout))
    32  	err := cache.AddTimeoutObject(timeout)
    33  	require.ErrorIs(t, err, ErrRepeatedTimeout)
    34  	require.Len(t, cache.All(), 1)
    35  }
    36  
    37  // TestTimeoutObjectsCache_AddTimeoutObjectIncompatibleView tests that adding timeout with incompatible view results in error
    38  func TestTimeoutObjectsCache_AddTimeoutObjectIncompatibleView(t *testing.T) {
    39  	t.Parallel()
    40  
    41  	view := uint64(100)
    42  	cache := NewTimeoutObjectsCache(view)
    43  	timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(view + 1))
    44  	err := cache.AddTimeoutObject(timeout)
    45  	require.ErrorIs(t, err, ErrTimeoutForIncompatibleView)
    46  }
    47  
    48  // TestTimeoutObjectsCache_GetTimeout tests that GetTimeout method returns the first added timeout
    49  // for a given signer, if any timeout has been added.
    50  func TestTimeoutObjectsCache_GetTimeout(t *testing.T) {
    51  	view := uint64(100)
    52  	knownTimeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(view))
    53  	doubleTimeout := helper.TimeoutObjectFixture(
    54  		helper.WithTimeoutObjectView(view),
    55  		helper.WithTimeoutObjectSignerID(knownTimeout.SignerID))
    56  
    57  	cache := NewTimeoutObjectsCache(view)
    58  
    59  	// unknown timeout
    60  	timeout, found := cache.GetTimeoutObject(unittest.IdentifierFixture())
    61  	require.Nil(t, timeout)
    62  	require.False(t, found)
    63  
    64  	// known timeout
    65  	err := cache.AddTimeoutObject(knownTimeout)
    66  	require.NoError(t, err)
    67  	timeout, found = cache.GetTimeoutObject(knownTimeout.SignerID)
    68  	require.Equal(t, knownTimeout, timeout)
    69  	require.True(t, found)
    70  
    71  	// for a signer ID with a known timeout, the cache should memorize the _first_ encountered timeout
    72  	err = cache.AddTimeoutObject(doubleTimeout)
    73  	require.True(t, model.IsDoubleTimeoutError(err))
    74  	timeout, found = cache.GetTimeoutObject(doubleTimeout.SignerID)
    75  	require.Equal(t, knownTimeout, timeout)
    76  	require.True(t, found)
    77  }
    78  
    79  // TestTimeoutObjectsCache_All tests that All returns previously added timeouts.
    80  func TestTimeoutObjectsCache_All(t *testing.T) {
    81  	t.Parallel()
    82  
    83  	view := uint64(100)
    84  	cache := NewTimeoutObjectsCache(view)
    85  	expectedTimeouts := make([]*model.TimeoutObject, 5)
    86  	for i := range expectedTimeouts {
    87  		timeout := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(view))
    88  		expectedTimeouts[i] = timeout
    89  		require.NoError(t, cache.AddTimeoutObject(timeout))
    90  	}
    91  	require.ElementsMatch(t, expectedTimeouts, cache.All())
    92  }
    93  
    94  // BenchmarkAdd measured the time it takes to add `numberTimeouts` concurrently to the TimeoutObjectsCache.
    95  // On MacBook with Intel i7-7820HQ CPU @ 2.90GHz:
    96  // adding 1 million timeouts in total, with 20 threads concurrently, took 0.48s
    97  func BenchmarkAdd(b *testing.B) {
    98  	numberTimeouts := 1_000_000
    99  	threads := 20
   100  
   101  	// Setup: create worker routines and timeouts to feed
   102  	view := uint64(10)
   103  	cache := NewTimeoutObjectsCache(view)
   104  
   105  	var start sync.WaitGroup
   106  	start.Add(threads)
   107  	var done sync.WaitGroup
   108  	done.Add(threads)
   109  
   110  	n := numberTimeouts / threads
   111  
   112  	for ; threads > 0; threads-- {
   113  		go func(i int) {
   114  			// create timeouts and signal ready
   115  			timeouts := make([]model.TimeoutObject, 0, n)
   116  			for len(timeouts) < n {
   117  				t := helper.TimeoutObjectFixture(helper.WithTimeoutObjectView(view))
   118  				timeouts = append(timeouts, *t)
   119  			}
   120  			start.Done()
   121  
   122  			// Wait for last worker routine to signal ready. Then,
   123  			// feed all timeouts into cache
   124  			start.Wait()
   125  			for _, v := range timeouts {
   126  				err := cache.AddTimeoutObject(&v)
   127  				require.NoError(b, err)
   128  			}
   129  			done.Done()
   130  		}(threads)
   131  	}
   132  	start.Wait()
   133  	t1 := time.Now()
   134  	done.Wait()
   135  	duration := time.Since(t1)
   136  	fmt.Printf("=> adding %d timeouts to Cache took %f seconds\n", cache.Size(), duration.Seconds())
   137  }