-
Notifications
You must be signed in to change notification settings - Fork 103
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add multiplicative ticker, use in runners
- Loading branch information
1 parent
ffc708b
commit 5c2a107
Showing
4 changed files
with
186 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package backoff | ||
|
||
import ( | ||
"time" | ||
) | ||
|
||
// NewMultiplicativeTicker returns a ticker where each interval = baseDuration * ticks until maxDuration is reached. | ||
func NewMultiplicativeTicker(baseDuration, maxDuration time.Duration) *ticker { | ||
return newTicker(newMultiplicativeCounter(baseDuration, maxDuration)) | ||
} | ||
|
||
type ticker struct { | ||
C chan time.Time | ||
baseTicker *time.Ticker | ||
stoppedChan chan struct{} | ||
stopped bool | ||
durationCounter durationCounter | ||
} | ||
|
||
func newTicker(durationCounter *durationCounter) *ticker { | ||
thisTicker := &ticker{ | ||
C: make(chan time.Time), | ||
stoppedChan: make(chan struct{}), | ||
durationCounter: *durationCounter, | ||
} | ||
|
||
thisTicker.baseTicker = time.NewTicker(thisTicker.durationCounter.next()) | ||
|
||
go func() { | ||
for { | ||
select { | ||
case t := <-thisTicker.baseTicker.C: | ||
thisTicker.baseTicker.Reset(thisTicker.durationCounter.next()) | ||
thisTicker.C <- t | ||
case <-thisTicker.stoppedChan: | ||
thisTicker.baseTicker.Stop() | ||
return | ||
} | ||
} | ||
}() | ||
|
||
return thisTicker | ||
} | ||
|
||
func (t *ticker) Stop() { | ||
if t.stopped { | ||
return | ||
} | ||
|
||
t.stopped = true | ||
close(t.stoppedChan) | ||
} | ||
|
||
type durationCounter struct { | ||
count int | ||
baseInterval, maxInterval time.Duration | ||
calcNext func(count int, baseDuration time.Duration) time.Duration | ||
} | ||
|
||
func (dc *durationCounter) next() time.Duration { | ||
dc.count++ | ||
interval := dc.calcNext(dc.count, dc.baseInterval) | ||
if interval > dc.maxInterval { | ||
return dc.maxInterval | ||
} | ||
return interval | ||
} | ||
|
||
func newMultiplicativeCounter(baseDuration, maxDuration time.Duration) *durationCounter { | ||
return &durationCounter{ | ||
baseInterval: baseDuration, | ||
maxInterval: maxDuration, | ||
calcNext: func(count int, baseInterval time.Duration) time.Duration { | ||
return baseInterval * time.Duration(count) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package backoff | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestMultiplicativeCounter(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := []struct { | ||
name string | ||
baseInterval time.Duration | ||
maxInterval time.Duration | ||
expected []time.Duration | ||
}{ | ||
{ | ||
name: "seconds", | ||
baseInterval: time.Second, | ||
maxInterval: 5 * time.Second, | ||
expected: []time.Duration{ | ||
time.Second, // 1s | ||
2 * time.Second, // 2s | ||
3 * time.Second, // 3s | ||
4 * time.Second, // 4s | ||
5 * time.Second, // 5s (max interval) | ||
5 * time.Second, // capped at max interval | ||
}, | ||
}, | ||
{ | ||
name: "minutes", | ||
baseInterval: time.Minute, | ||
maxInterval: 3 * time.Minute, | ||
expected: []time.Duration{ | ||
time.Minute, // 1m | ||
2 * time.Minute, // 2m | ||
3 * time.Minute, // 3m (max interval) | ||
3 * time.Minute, // capped at max interval | ||
3 * time.Minute, // capped at max interval | ||
}, | ||
}, | ||
{ | ||
name: "combo", | ||
baseInterval: (1 * time.Minute) + (30 * time.Second), | ||
maxInterval: 5 * time.Minute, | ||
expected: []time.Duration{ | ||
(1 * time.Minute) + (30 * time.Second), // 1m30s | ||
2 * ((1 * time.Minute) + (30 * time.Second)), // 3m | ||
3 * ((1 * time.Minute) + (30 * time.Second)), // 4m30s | ||
5 * time.Minute, // 5m | ||
5 * time.Minute, // 5m | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
ec := newMultiplicativeCounter(tt.baseInterval, tt.maxInterval) | ||
for _, expected := range tt.expected { | ||
require.Equal(t, expected, ec.next()) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// TestMultiplicativeTicker tests the NewMultiplicativeTicker and its behavior. | ||
func TestMultiplicativeTicker(t *testing.T) { | ||
Check failure on line 71 in pkg/backoff/ticker_test.go GitHub Actions / lint (macos-latest)
Check failure on line 71 in pkg/backoff/ticker_test.go GitHub Actions / lint (macos-latest)
Check failure on line 71 in pkg/backoff/ticker_test.go GitHub Actions / lint (windows-latest)
Check failure on line 71 in pkg/backoff/ticker_test.go GitHub Actions / lint (windows-latest)
Check failure on line 71 in pkg/backoff/ticker_test.go GitHub Actions / lint (ubuntu-latest)
|
||
baseTime := 100 * time.Millisecond | ||
maxTime := 500 * time.Millisecond | ||
|
||
tk := NewMultiplicativeTicker(baseTime, maxTime) | ||
defer tk.Stop() | ||
|
||
expectedDurations := []time.Duration{ | ||
100 * time.Millisecond, | ||
200 * time.Millisecond, | ||
300 * time.Millisecond, | ||
400 * time.Millisecond, | ||
500 * time.Millisecond, // maxTime limit | ||
500 * time.Millisecond, // maxTime limit | ||
} | ||
|
||
buffer := 25 * time.Millisecond | ||
|
||
for _, expected := range expectedDurations { | ||
start := time.Now() | ||
|
||
select { | ||
case <-tk.C: | ||
require.WithinDuration(t, start, time.Now(), expected+buffer) | ||
case <-time.After(maxTime + buffer): | ||
t.Fatalf("ticker did not send event in expected time: %v", expected) | ||
} | ||
} | ||
|
||
// stop the ticker | ||
tk.Stop() | ||
|
||
// call stop again to make sure no panic (same as stdlib ticker) | ||
tk.Stop() | ||
} |