ceremonyclient/consensus/timeoutcollector/timeout_cache_test.go
Cassandra Heart c797d482f9
v2.1.0.5 (#457)
* wip: conversion of hotstuff from flow into Q-oriented model

* bulk of tests

* remaining non-integration tests

* add integration test, adjust log interface, small tweaks

* further adjustments, restore full pacemaker shape

* add component lifecycle management+supervisor

* further refinements

* resolve timeout hanging

* mostly finalized state for consensus

* bulk of engine swap out

* lifecycle-ify most types

* wiring nearly complete, missing needed hooks for proposals

* plugged in, vetting message validation paths

* global consensus, plugged in and verified

* app shard now wired in too

* do not decode empty keys.yml (#456)

* remove obsolete engine.maxFrames config parameter (#454)

* default to Info log level unless debug is enabled (#453)

* respect config's  "logging" section params, remove obsolete single-file logging (#452)

* Trivial code cleanup aiming to reduce Go compiler warnings (#451)

* simplify range traversal

* simplify channel read for single select case

* delete rand.Seed() deprecated in Go 1.20 and no-op as of Go 1.24

* simplify range traversal

* simplify channel read for single select case

* remove redundant type from array

* simplify range traversal

* simplify channel read for single select case

* RC slate

* finalize 2.1.0.5

* Update comments in StrictMonotonicCounter

Fix comment formatting and clarify description.

---------

Co-authored-by: Black Swan <3999712+blacks1ne@users.noreply.github.com>
2025-11-11 05:00:17 -06:00

173 lines
5.1 KiB
Go

package timeoutcollector
import (
"fmt"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"source.quilibrium.com/quilibrium/monorepo/consensus/helper"
"source.quilibrium.com/quilibrium/monorepo/consensus/models"
)
// TestTimeoutStatesCache_Rank tests that Rank returns same value that was set by constructor
func TestTimeoutStatesCache_Rank(t *testing.T) {
rank := uint64(100)
cache := NewTimeoutStatesCache[*helper.TestVote](rank)
require.Equal(t, rank, cache.Rank())
}
// TestTimeoutStatesCache_AddTimeoutStateRepeatedTimeout tests that AddTimeoutState skips duplicated timeouts
func TestTimeoutStatesCache_AddTimeoutStateRepeatedTimeout(t *testing.T) {
t.Parallel()
rank := uint64(100)
cache := NewTimeoutStatesCache[*helper.TestVote](rank)
timeout := helper.TimeoutStateFixture(
helper.WithTimeoutStateRank[*helper.TestVote](rank),
helper.WithTimeoutVote[*helper.TestVote](&helper.TestVote{
ID: "1",
Rank: rank,
}),
)
require.NoError(t, cache.AddTimeoutState(timeout))
err := cache.AddTimeoutState(timeout)
require.ErrorIs(t, err, ErrRepeatedTimeout)
require.Len(t, cache.All(), 1)
}
// TestTimeoutStatesCache_AddTimeoutStateIncompatibleRank tests that adding timeout with incompatible rank results in error
func TestTimeoutStatesCache_AddTimeoutStateIncompatibleRank(t *testing.T) {
t.Parallel()
rank := uint64(100)
cache := NewTimeoutStatesCache[*helper.TestVote](rank)
timeout := helper.TimeoutStateFixture(
helper.WithTimeoutStateRank[*helper.TestVote](rank+1),
helper.WithTimeoutVote[*helper.TestVote](&helper.TestVote{
ID: "1",
Rank: rank,
}),
)
err := cache.AddTimeoutState(timeout)
require.ErrorIs(t, err, ErrTimeoutForIncompatibleRank)
}
// TestTimeoutStatesCache_GetTimeout tests that GetTimeout method returns the first added timeout
// for a given signer, if any timeout has been added.
func TestTimeoutStatesCache_GetTimeout(t *testing.T) {
rank := uint64(100)
knownTimeout := helper.TimeoutStateFixture(
helper.WithTimeoutStateRank[*helper.TestVote](rank),
helper.WithTimeoutVote[*helper.TestVote](&helper.TestVote{
ID: "1",
Rank: rank,
}),
)
doubleTimeout := helper.TimeoutStateFixture(
helper.WithTimeoutStateRank[*helper.TestVote](rank),
helper.WithTimeoutVote[*helper.TestVote](&helper.TestVote{
ID: "1",
Rank: rank,
}),
)
cache := NewTimeoutStatesCache[*helper.TestVote](rank)
// unknown timeout
timeout, found := cache.GetTimeoutState(helper.MakeIdentity())
require.Nil(t, timeout)
require.False(t, found)
// known timeout
err := cache.AddTimeoutState(knownTimeout)
require.NoError(t, err)
timeout, found = cache.GetTimeoutState((*knownTimeout.Vote).ID)
require.Equal(t, knownTimeout, timeout)
require.True(t, found)
// for a signer ID with a known timeout, the cache should memorize the _first_ encountered timeout
err = cache.AddTimeoutState(doubleTimeout)
require.True(t, models.IsDoubleTimeoutError[*helper.TestVote](err))
timeout, found = cache.GetTimeoutState((*doubleTimeout.Vote).ID)
require.Equal(t, knownTimeout, timeout)
require.True(t, found)
}
// TestTimeoutStatesCache_All tests that All returns previously added timeouts.
func TestTimeoutStatesCache_All(t *testing.T) {
t.Parallel()
rank := uint64(100)
cache := NewTimeoutStatesCache[*helper.TestVote](rank)
expectedTimeouts := make([]*models.TimeoutState[*helper.TestVote], 5)
for i := range expectedTimeouts {
timeout := helper.TimeoutStateFixture(
helper.WithTimeoutStateRank[*helper.TestVote](rank),
helper.WithTimeoutVote[*helper.TestVote](&helper.TestVote{
ID: fmt.Sprintf("%d", i),
Rank: rank,
}),
)
expectedTimeouts[i] = timeout
require.NoError(t, cache.AddTimeoutState(timeout))
}
require.ElementsMatch(t, expectedTimeouts, cache.All())
}
// BenchmarkAdd measured the time it takes to add `numberTimeouts` concurrently to the TimeoutStatesCache.
// On MacBook with Intel i7-7820HQ CPU @ 2.90GHz:
// adding 1 million timeouts in total, with 20 threads concurrently, took 0.48s
func BenchmarkAdd(b *testing.B) {
numberTimeouts := 1_000_000
threads := 20
// Setup: create worker routines and timeouts to feed
rank := uint64(10)
cache := NewTimeoutStatesCache[*helper.TestVote](rank)
var start sync.WaitGroup
start.Add(threads)
var done sync.WaitGroup
done.Add(threads)
n := numberTimeouts / threads
for ; threads > 0; threads-- {
go func(i int) {
// create timeouts and signal ready
timeouts := make([]models.TimeoutState[*helper.TestVote], 0, n)
for len(timeouts) < n {
t := helper.TimeoutStateFixture(
helper.WithTimeoutStateRank[*helper.TestVote](rank),
helper.WithTimeoutVote[*helper.TestVote](&helper.TestVote{
ID: helper.MakeIdentity(),
Rank: rank,
}),
)
timeouts = append(timeouts, *t)
}
start.Done()
// Wait for last worker routine to signal ready. Then,
// feed all timeouts into cache
start.Wait()
for _, v := range timeouts {
err := cache.AddTimeoutState(&v)
require.NoError(b, err)
}
done.Done()
}(threads)
}
start.Wait()
t1 := time.Now()
done.Wait()
duration := time.Since(t1)
fmt.Printf("=> adding %d timeouts to Cache took %f seconds\n", cache.Size(), duration.Seconds())
}