Skip to content

Commit 45512f1

Browse files
Merge pull request #2025 from benluddy/small-jitters
Bug 1932182: Support jittering relatively small resync intervals.
2 parents c8e2189 + 2413caf commit 45512f1

File tree

2 files changed

+118
-30
lines changed

2 files changed

+118
-30
lines changed

pkg/lib/queueinformer/jitter.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,45 @@
11
package queueinformer
22

33
import (
4+
"math"
45
"math/rand"
56
"time"
67
)
78

89
const DefaultResyncPeriod = 15 * time.Minute
910

11+
type float64er interface {
12+
// Float64 returns a float64 in range [0.0, 1.0).
13+
Float64() float64
14+
}
15+
16+
type realFloat64er struct{}
17+
18+
func (realFloat64er) Float64() float64 {
19+
return rand.Float64()
20+
}
21+
1022
// ResyncWithJitter takes a resync interval and adds jitter within a percent difference.
1123
// factor is a value between 0 and 1 indicating the amount of jitter
1224
// a factor of 0.2 and a period of 10m will have a range of 8 to 12 minutes (20%)
1325
func ResyncWithJitter(resyncPeriod time.Duration, factor float64) func() time.Duration {
26+
return resyncWithJitter(resyncPeriod, factor, realFloat64er{})
27+
}
28+
29+
func resyncWithJitter(period time.Duration, factor float64, rand float64er) func() time.Duration {
1430
return func() time.Duration {
15-
if factor < 0.0 || factor > 1.0 {
16-
return resyncPeriod
17-
}
18-
if resyncPeriod < 0.0 {
31+
if period < 0.0 {
1932
return DefaultResyncPeriod
2033
}
21-
22-
// if we would wrap around, return resyncPeriod
23-
if time.Duration((1+factor)*resyncPeriod.Minutes())*time.Minute < 0.0 {
24-
return resyncPeriod
34+
if period > math.MaxInt64/2 { // 1281023h53m38.427387903s
35+
// avoid overflowing time.Duration
36+
return period
37+
}
38+
if factor < 0.0 || factor > 1.0 {
39+
return period
2540
}
2641

27-
min := resyncPeriod.Minutes() * (1 - factor)
28-
max := resyncPeriod.Minutes() * (1 + factor)
29-
30-
return time.Duration(min)*time.Minute + time.Duration(rand.Float64()*(max-min))*time.Minute
42+
// The effective scale will be in [1-factor, 1+factor) because rand.Float64() is in [0.0, 1.0).
43+
return time.Duration((1 - factor + 2*rand.Float64()*factor) * float64(period))
3144
}
3245
}

pkg/lib/queueinformer/jitter_test.go

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package queueinformer
22

33
import (
4+
"math"
45
"math/rand"
56
"reflect"
67
"testing"
@@ -10,60 +11,134 @@ import (
1011
"github.com/stretchr/testify/require"
1112
)
1213

14+
type fakeFloat64er float64
15+
16+
func (f fakeFloat64er) Float64() float64 {
17+
return float64(f)
18+
}
19+
1320
func TestResyncWithJitter(t *testing.T) {
1421
type args struct {
1522
resyncPeriod time.Duration
1623
factor float64
24+
r float64
1725
}
1826
tests := []struct {
19-
name string
20-
args args
21-
wantMin time.Duration
22-
wantMax time.Duration
27+
name string
28+
args args
29+
want time.Duration
2330
}{
2431
{
25-
name: "TypicalInput/Minutes",
32+
name: "TypicalInput/15Minutes/Min",
33+
args: args{
34+
resyncPeriod: 15 * time.Minute,
35+
factor: 0.2,
36+
r: 0,
37+
},
38+
want: 12 * time.Minute,
39+
},
40+
{
41+
name: "TypicalInput/15Minutes/Mid",
42+
args: args{
43+
resyncPeriod: 15 * time.Minute,
44+
factor: 0.2,
45+
r: 0.5,
46+
},
47+
want: 15 * time.Minute,
48+
},
49+
{
50+
name: "TypicalInput/15Minutes/Max",
2651
args: args{
2752
resyncPeriod: 15 * time.Minute,
2853
factor: 0.2,
54+
r: 1,
2955
},
30-
wantMin: 12 * time.Minute,
31-
wantMax: 18 * time.Minute,
56+
want: 18 * time.Minute,
3257
},
3358
{
34-
name: "TypicalInput/Hours",
59+
name: "TypicalInput/10Hours/Min",
3560
args: args{
3661
resyncPeriod: 10 * time.Hour,
3762
factor: 0.1,
63+
r: 0,
3864
},
39-
wantMin: 9 * time.Hour,
40-
wantMax: 11 * time.Hour,
65+
want: 9 * time.Hour,
66+
},
67+
{
68+
name: "TypicalInput/10Hours/Mid",
69+
args: args{
70+
resyncPeriod: 10 * time.Hour,
71+
factor: 0.1,
72+
r: 0.5,
73+
},
74+
want: 10 * time.Hour,
75+
},
76+
{
77+
name: "TypicalInput/10Hours/Max",
78+
args: args{
79+
resyncPeriod: 10 * time.Hour,
80+
factor: 0.1,
81+
r: 1,
82+
},
83+
want: 11 * time.Hour,
4184
},
4285
{
4386
name: "BadInput/BadFactor",
4487
args: args{
4588
resyncPeriod: 10 * time.Hour,
4689
factor: -0.1,
4790
},
48-
wantMin: 10 * time.Hour,
49-
wantMax: 10 * time.Hour,
91+
want: 10 * time.Hour,
5092
},
5193
{
5294
name: "BadInput/BadResync",
5395
args: args{
5496
resyncPeriod: -10 * time.Hour,
5597
factor: 0.1,
5698
},
57-
wantMin: DefaultResyncPeriod,
58-
wantMax: DefaultResyncPeriod,
99+
want: DefaultResyncPeriod,
100+
},
101+
{
102+
name: "BadInput/Big",
103+
args: args{
104+
resyncPeriod: time.Duration(math.MaxInt64),
105+
factor: 1,
106+
r: 1,
107+
},
108+
want: time.Duration(math.MaxInt64),
109+
},
110+
{
111+
name: "SmallInput/Min",
112+
args: args{
113+
resyncPeriod: 10 * time.Second,
114+
factor: 0.5,
115+
r: 0,
116+
},
117+
want: 5 * time.Second,
118+
},
119+
{
120+
name: "SmallInput/Mid",
121+
args: args{
122+
resyncPeriod: 10 * time.Second,
123+
factor: 0.5,
124+
r: 0.5,
125+
},
126+
want: 10 * time.Second,
127+
},
128+
{
129+
name: "SmallInput/Max",
130+
args: args{
131+
resyncPeriod: 10 * time.Second,
132+
factor: 0.5,
133+
r: 1,
134+
},
135+
want: 15 * time.Second,
59136
},
60137
}
61138
for _, tt := range tests {
62139
t.Run(tt.name, func(t *testing.T) {
63-
got := ResyncWithJitter(tt.args.resyncPeriod, tt.args.factor)
64-
require.True(t, got() >= tt.wantMin)
65-
require.True(t, got() <= tt.wantMax)
66-
require.True(t, got() != got() || tt.wantMax == tt.wantMin)
140+
got := resyncWithJitter(tt.args.resyncPeriod, tt.args.factor, fakeFloat64er(tt.args.r))()
141+
require.Equal(t, tt.want, got)
67142
})
68143
}
69144
}

0 commit comments

Comments
 (0)