Files
teleport/lib/utils/interval/multi_test.go
T

285 lines
6.6 KiB
Go

// Copyright 2022 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interval
import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestMultiIntervalReset verifies the basic behavior of the multi interval reset functionality.
// Since time based tests tend to be flaky, this test passes if it has a >50% success
// rate (i.e. >50% of resets seemed to have actually extended the timer successfully).
func TestMultiIntervalReset(t *testing.T) {
const iterations = 1_000
const duration = time.Millisecond * 666
t.Parallel()
var success, failure atomic.Uint64
var wg sync.WaitGroup
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resetTimer := time.NewTimer(duration / 3)
defer resetTimer.Stop()
interval := NewMulti[string](SubInterval[string]{
Key: "key",
Duration: duration,
})
defer interval.Stop()
start := time.Now()
for i := 0; i < 6; i++ {
select {
case <-interval.Next():
failure.Add(1)
return
case <-resetTimer.C:
interval.Reset("key")
resetTimer.Reset(duration / 3)
}
}
<-interval.Next()
elapsed := time.Since(start)
// we expect this test to produce elapsed times of
// 3*duration if it is working properly. we accept a
// margin or error of +/- 1 duration in order to
// minimize flakiness.
if elapsed > duration*2 && elapsed < duration*4 {
success.Add(1)
} else {
failure.Add(1)
}
}()
}
wg.Wait()
require.Greater(t, success.Load(), failure.Load())
}
// TestMultiIntervalBasics verifies basic expected multiinterval behavior. Due to the general
// flakiness of time-based tests, this test is designed to only care about which sub-intervals
// tick *more* relative to one another, and not about the specific relative frequency of observed
// ticks.
func TestMultiIntervalBasics(t *testing.T) {
t.Parallel()
interval := NewMulti[string](
SubInterval[string]{
Key: "fast",
Duration: time.Millisecond * 8,
},
SubInterval[string]{
Key: "slow",
Duration: time.Millisecond * 16,
},
SubInterval[string]{
Key: "once",
Duration: time.Hour,
FirstDuration: time.Millisecond,
},
)
defer interval.Stop()
var fast, slow, once int
var prevT time.Time
for i := 0; i < 60; i++ {
tick := <-interval.Next()
require.True(t, !tick.Time.IsZero())
require.True(t, tick.Time.After(prevT) || tick.Time == prevT)
prevT = tick.Time
switch tick.Key {
case "fast":
fast++
case "slow":
slow++
case "once":
once++
}
}
require.Equal(t, 1, once)
require.Greater(t, slow, once)
require.Greater(t, fast, slow)
}
// TestMultiIntervalPush verifies the expected behavior of MultiInterval.Push, both in terms of
// its ability to add new sub-intervals, and to overwrite existing sub-intervals.
func TestMultiIntervalPush(t *testing.T) {
t.Parallel()
interval := NewMulti[string](
SubInterval[string]{
Key: "foo",
Duration: time.Millisecond * 6,
},
)
defer interval.Stop()
// verify that single-interval is working
for i := 0; i < 3; i++ {
tick := <-interval.Next()
require.Equal(t, "foo", tick.Key)
}
// push a new slower sub-interval
interval.Push(SubInterval[string]{
Key: "bar",
Duration: time.Millisecond * 12,
})
// aggregate rates of both sub-intervals
var foo, bar int
for i := 0; i < 60; i++ {
tick := <-interval.Next()
switch tick.Key {
case "foo":
foo++
case "bar":
bar++
}
}
// verify that both sub-intervals are firing, and that
// foo is firing more frequently.
require.NotZero(t, foo)
require.NotZero(t, bar)
require.Greater(t, foo, bar)
// overwrite the old sub-intervals, inverting their respective
// tick rates.
interval.Push(SubInterval[string]{
Key: "foo",
Duration: time.Millisecond * 12,
})
interval.Push(SubInterval[string]{
Key: "bar",
Duration: time.Millisecond * 6,
})
// aggregate new rates for both sub-intervals
foo = 0
bar = 0
for i := 0; i < 60; i++ {
tick := <-interval.Next()
switch tick.Key {
case "foo":
foo++
case "bar":
bar++
}
}
// verify that both sub-intervals are firing, and that
// foo their relative rates have flipped, with bar now
// firing more frequently.
require.NotZero(t, foo)
require.NotZero(t, bar)
require.Greater(t, bar, foo)
}
// TestMultiIntervalFireNow verifies the expected behavior of MultiInterval.FireNow.
func TestMultiIntervalFireNow(t *testing.T) {
t.Parallel()
// set up one sub-interval that fires frequently, and another that will never
// fire during this test unless we trigger with FireNow.
interval := NewMulti[string](
SubInterval[string]{
Key: "slow",
Duration: time.Hour,
},
SubInterval[string]{
Key: "fast",
Duration: time.Millisecond * 10,
},
)
defer interval.Stop()
// verify that only the 'fast' interval is firing
for i := 0; i < 10; i++ {
tick := <-interval.Next()
require.Equal(t, "fast", tick.Key)
}
// trigger the slow interval
interval.FireNow("slow")
// make sure that we observe slow interval firing
var seenSlow bool
for i := 0; i < 60; i++ {
tick := <-interval.Next()
if tick.Key == "slow" {
seenSlow = true
break
}
}
require.True(t, seenSlow)
}
// TestPendingTicks tests the expected behavior of the pendingTicks helper.
func TestPendingTicks(t *testing.T) {
// "backlog" of fake ticks with lots of duplicates
tks := []string{
"foo",
"bar",
"foo",
"foo",
"bin",
"bar",
"bar",
"baz",
"foo",
}
var pending pendingTicks[string]
// insert out fake ticks
for _, tk := range tks {
pending.add(time.Now(), tk)
}
// we expect to have not stored any duplicate keys (important for
// preventing runaway memory growth due to neglected interval).
require.Len(t, pending.keys, 4)
// expected order of ticks to be emitted
expect := []string{
"foo",
"bar",
"bin",
"baz",
}
// verify that we see the ticks in the order we expect
for _, exp := range expect {
tick, ok := pending.next()
require.True(t, ok)
require.Equal(t, exp, tick.Key)
pending.remove(exp)
}
// verify that no more ticks are available
_, ok := pending.next()
require.False(t, ok)
}