diff --git a/cmix/attempts/histrogram.go b/cmix/attempts/histrogram.go index 0b6955dea3239edecd9f598191751edb9c315c63..37e7292592aeae1cdda482a82b0fdcf5530933d9 100644 --- a/cmix/attempts/histrogram.go +++ b/cmix/attempts/histrogram.go @@ -18,11 +18,19 @@ const ( optimalAttemptsInitValue = -1 ) +// SendAttemptTracker tracks the number of attempts it took to send a cMix +// message in order to predict how many attempt are needed. type SendAttemptTracker interface { + // SubmitProbeAttempt feeds the number of attempts it took to send a cMix + // message into the tracker and updates the optimal number of attempts. SubmitProbeAttempt(numAttemptsUntilSuccessful int) + + // GetOptimalNumAttempts returns the number of optimal sends. If there is + // insufficient data to calculate, then ready is false. GetOptimalNumAttempts() (attempts int, ready bool) } +// sendAttempts tracks the number of attempts to send a cMix message. type sendAttempts struct { optimalAttempts *int32 isFull bool @@ -31,6 +39,7 @@ type sendAttempts struct { lock sync.Mutex } +// NewSendAttempts initialises a new SendAttemptTracker. func NewSendAttempts() SendAttemptTracker { optimalAttempts := int32(optimalAttemptsInitValue) sa := &sendAttempts{ @@ -43,6 +52,8 @@ func NewSendAttempts() SendAttemptTracker { return sa } +// SubmitProbeAttempt feeds the number of attempts it took to send a cMix +// message into the tracker and updates the optimal number of attempts. func (sa *sendAttempts) SubmitProbeAttempt(numAttemptsUntilSuccessful int) { sa.lock.Lock() defer sa.lock.Unlock() @@ -58,6 +69,8 @@ func (sa *sendAttempts) SubmitProbeAttempt(numAttemptsUntilSuccessful int) { sa.computeOptimalUnsafe() } +// GetOptimalNumAttempts returns the number of optimal sends. If there is +// insufficient data to calculate, then ready is false. func (sa *sendAttempts) GetOptimalNumAttempts() (attempts int, ready bool) { optimalAttempts := atomic.LoadInt32(sa.optimalAttempts) @@ -68,6 +81,7 @@ func (sa *sendAttempts) GetOptimalNumAttempts() (attempts int, ready bool) { return int(optimalAttempts), true } +// computeOptimalUnsafe updates the optimal send attempts. func (sa *sendAttempts) computeOptimalUnsafe() { toCopy := maxHistogramSize if !sa.isFull { diff --git a/cmix/attempts/histrogram_test.go b/cmix/attempts/histrogram_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6934b1f332e44cd17dd5cf2030932a8ec364fbd3 --- /dev/null +++ b/cmix/attempts/histrogram_test.go @@ -0,0 +1,89 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package attempts + +import ( + "math/rand" + "reflect" + "testing" +) + +// Tests that NewSendAttempts returns a new sendAttempts with the expected +// fields. +func TestNewSendAttempts(t *testing.T) { + optimalAttempts := int32(optimalAttemptsInitValue) + expected := &sendAttempts{ + optimalAttempts: &optimalAttempts, + isFull: false, + currentIndex: 0, + numAttempts: make([]int, maxHistogramSize), + } + + sa := NewSendAttempts() + + if !reflect.DeepEqual(expected, sa) { + t.Errorf("New SendAttemptTracker does not match expected."+ + "\nexpected: %+v\nreceivedL %+v", expected, sa) + } +} + +// Tests that sendAttempts.SubmitProbeAttempt properly increments and stores the +// attempts. +func Test_sendAttempts_SubmitProbeAttempt(t *testing.T) { + sa := NewSendAttempts().(*sendAttempts) + + for i := 0; i < maxHistogramSize+20; i++ { + sa.SubmitProbeAttempt(i) + + if sa.currentIndex != (i+1)%maxHistogramSize { + t.Errorf("Incorrect currentIndex (%d).\nexpected: %d\nreceived: %d", + i, (i+1)%maxHistogramSize, sa.currentIndex) + } else if sa.numAttempts[i%maxHistogramSize] != i { + t.Errorf("Incorrect numAttempts at %d.\nexpected: %d\nreceived: %d", + i, i, sa.numAttempts[i%maxHistogramSize]) + } else if i > maxHistogramSize && !sa.isFull { + t.Errorf("Should be marked full when numAttempts > %d.", + maxHistogramSize) + } + } +} + +// Tests sendAttempts.GetOptimalNumAttempts returns numbers close to 70% of the +// average of attempts feeding in. +func Test_sendAttempts_GetOptimalNumAttempts(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + sa := NewSendAttempts().(*sendAttempts) + + attempts, ready := sa.GetOptimalNumAttempts() + if ready { + t.Errorf("Marked ready when no attempts have been made.") + } else if attempts != 0 { + t.Errorf("Incorrect number of attempt.\nexpected: %d\nreceived: %d", + 0, attempts) + } + + const n = 100 + factor := (n * 7) / 10 + for i := 0; i < 500; i++ { + sa.SubmitProbeAttempt(prng.Intn(n)) + attempts, ready = sa.GetOptimalNumAttempts() + + if (sa.currentIndex < minElements && !sa.isFull) && ready { + t.Errorf("Ready when less than %d attempts made (%d).", + minElements, i) + } else if sa.currentIndex >= minElements { + if !ready { + t.Errorf("Not ready when more than %d attempts made (%d).", + minElements, i) + } else if attempts < factor-25 || attempts > factor+25 { + t.Errorf("Attempts is not close to average (%d)."+ + "\naverage: %d\nattempts: %d", i, factor, attempts) + } + } + } +}