Skip to content

Commit 0307a43

Browse files
committed
Improve jittering and testing
1 parent 2e9bb20 commit 0307a43

File tree

13 files changed

+373
-161
lines changed

13 files changed

+373
-161
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Set up Go
1919
uses: actions/setup-go@v4
2020
with:
21-
go-version: "1.22"
21+
go-version: "1.24"
2222

2323
- name: Install dependencies
2424
run: |

CHANGELOG.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
# Changelog
22

3+
### v0.4.0
4+
5+
- Update to Go 1.24
6+
- Change jittering to use interval, offset, and timeouts
7+
- Change tests to verify package
8+
39
### v0.3.0
410

5-
- Migrated to Go 1.20
11+
- Update to Go 1.20
612
- Simplified `Throttle` implementation
7-
- Migrated to Go 1.18
813

914
### v0.2.0 (2022-03-04)
1015

11-
- Added Throttle
16+
- Add Throttle
1217

1318
### v0.1.1 (2021-09-07)
1419

15-
- Fixed documentation
20+
- Fixe documentation
1621

1722
### v0.1.0 (2021-09-07)
1823

19-
- Migrated Wait code from together library
24+
- Migrate Wait code from together library

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
BSD 3-Clause License
22

3-
Copyright (c) 2019-2024, Frank Mueller / Tideland / Oldenburg / Germany
3+
Copyright (c) 2019-2025, Frank Mueller / Tideland / Oldenburg / Germany
44
All rights reserved.
55

66
Redistribution and use in source and binary forms, with or without

doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Tideland Go Wait
22
//
3-
// Copyright (C) 2019-2024 Frank Mueller / Tideland / Oldenburg / Germany
3+
// Copyright (C) 2019-2025 Frank Mueller / Tideland / Oldenburg / Germany
44
//
55
// All rights reserved. Use of this source code is governed
66
// by the new BSD license.

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
module tideland.dev/go/wait
22

3-
go 1.22
3+
go 1.24
44

55
require (
66
golang.org/x/time v0.5.0
7-
tideland.dev/go/audit v0.7.0
7+
tideland.dev/go/asserts v0.0.0
88
)

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
22
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
3-
tideland.dev/go/audit v0.7.0 h1:lr4LkNu7i5qLJuqQ6lUfnt0J09anZNfrdXdB1I9JlTs=
4-
tideland.dev/go/audit v0.7.0/go.mod h1:Jua+IB3KgAC7fbuZ1YHT7gKhwpiTOcn3Q7AOCQsrro8=

jitter.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Tideland Go Wait
2+
//
3+
// Copyright (C) 2019-2025 Frank Mueller / Tideland / Oldenburg / Germany
4+
//
5+
// All rights reserved. Use of this source code is governed
6+
// by the new BSD license.
7+
8+
package wait // import "tideland.dev/go/wait"
9+
10+
//--------------------
11+
// IMPORTS
12+
//--------------------
13+
14+
import (
15+
"context"
16+
"crypto/rand"
17+
"math"
18+
"math/big"
19+
"time"
20+
)
21+
22+
// MakeJitteringTicker returns a ticker signalling in jittering intervals. This
23+
// avoids converging on periadoc behavior during condition check. The returned
24+
// interval jitters inside the given interval and starts with the given offset.
25+
// The ticker stops after reaching the timeout.
26+
func MakeJitteringTicker(interval, offset, timeout time.Duration) TickerFunc {
27+
start := time.Now()
28+
deadline := start.Add(timeout)
29+
30+
// Sanitize
31+
if interval < time.Millisecond {
32+
interval = time.Millisecond
33+
}
34+
if offset < 0 {
35+
offset = 0
36+
}
37+
if interval > time.Duration(math.MaxInt64)-offset {
38+
interval = time.Duration(math.MaxInt64) - offset
39+
}
40+
41+
next := start
42+
43+
changer := func(_ time.Duration) (time.Duration, bool) {
44+
now := time.Now()
45+
if now.After(deadline) {
46+
return 0, false
47+
}
48+
49+
// Generate jitter in range [0, interval)
50+
jitterRange := interval
51+
if deadline.Sub(now) < offset+jitterRange {
52+
jitterRange = deadline.Sub(now) - offset
53+
if jitterRange < 1 {
54+
return 0, false
55+
}
56+
}
57+
58+
bigInt, err := rand.Int(rand.Reader, big.NewInt(jitterRange.Nanoseconds()))
59+
if err != nil {
60+
return 0, false
61+
}
62+
63+
jitter := time.Duration(bigInt.Int64())
64+
wait := offset + jitter
65+
66+
next = next.Add(wait)
67+
delay := max(time.Until(next), 0)
68+
return delay, true
69+
}
70+
71+
return MakeGenericIntervalTicker(changer)
72+
}
73+
74+
// WithJitter is convenience for Poll() with MakeJitteringTicker().
75+
func WithJitter(
76+
ctx context.Context,
77+
interval, offset, timeout time.Duration,
78+
condition ConditionFunc,
79+
) error {
80+
return Poll(
81+
ctx,
82+
MakeJitteringTicker(interval, offset, timeout),
83+
condition,
84+
)
85+
}
86+
87+
// EOF

jitter_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Tideland Go Wait - Unit Tests
2+
//
3+
// Copyright (C) 2019-2025 Frank Mueller / Tideland / Oldenburg / Germany
4+
//
5+
// All rights reserved. Use of this source code is governed
6+
// by the new BSD license.
7+
8+
package wait_test // import "tideland.dev/go/wait"
9+
10+
//--------------------
11+
// IMPORTS
12+
//--------------------
13+
14+
import (
15+
"context"
16+
"testing"
17+
"time"
18+
19+
"tideland.dev/go/asserts/verify"
20+
21+
"tideland.dev/go/wait"
22+
)
23+
24+
//--------------------
25+
// TESTS
26+
//--------------------
27+
28+
// TestPollWithJitter tests the polling of conditions in a maximum
29+
// number of intervals.
30+
func TestPollWithJitter(t *testing.T) {
31+
timestamps := []time.Time{}
32+
err := wait.Poll(
33+
context.Background(),
34+
wait.MakeJitteringTicker(
35+
50*time.Millisecond,
36+
10*time.Millisecond,
37+
1250*time.Millisecond,
38+
),
39+
func() (bool, error) {
40+
timestamps = append(timestamps, time.Now())
41+
if len(timestamps) == 10 {
42+
return true, nil
43+
}
44+
return false, nil
45+
},
46+
)
47+
verify.NoError(t, err)
48+
verify.Length(t, timestamps, 10)
49+
50+
t.Logf("Timestamps for first test: %v", timestamps)
51+
for i := range 9 {
52+
diff := timestamps[i+1].Sub(timestamps[i])
53+
t.Logf("Diff %d: %v", i, diff)
54+
// According to implementation, jitter is within [offset, offset+interval]
55+
verify.InRange(t, 10*time.Millisecond, 60*time.Millisecond, diff)
56+
}
57+
58+
timestamps = []time.Time{}
59+
err = wait.WithJitter(
60+
context.Background(),
61+
50*time.Millisecond,
62+
10*time.Millisecond,
63+
1250*time.Millisecond, func() (bool, error) {
64+
timestamps = append(timestamps, time.Now())
65+
if len(timestamps) == 10 {
66+
return true, nil
67+
}
68+
return false, nil
69+
})
70+
verify.NoError(t, err)
71+
verify.Length(t, timestamps, 10)
72+
73+
t.Logf("Timestamps for second test: %v", timestamps)
74+
for i := 1; i < 10; i++ {
75+
diff := timestamps[i].Sub(timestamps[i-1])
76+
t.Logf("Diff %d: %v", i, diff)
77+
// According to implementation, jitter is within [offset, offset+interval]
78+
verify.InRange(t, 10*time.Millisecond, 60*time.Millisecond, diff)
79+
}
80+
81+
timestamps = []time.Time{}
82+
err = wait.Poll(
83+
context.Background(),
84+
wait.MakeJitteringTicker(
85+
50*time.Millisecond,
86+
10*time.Millisecond,
87+
1250*time.Millisecond),
88+
func() (bool, error) {
89+
timestamps = append(timestamps, time.Now())
90+
return false, nil
91+
},
92+
)
93+
verify.ErrorContains(t, "exceeded", err)
94+
verify.InRange(t, len(timestamps), 10, 25)
95+
96+
timestamps = []time.Time{}
97+
err = wait.Poll(
98+
context.Background(),
99+
wait.MakeJitteringTicker(
100+
50*time.Millisecond,
101+
10*time.Millisecond,
102+
1000*time.Millisecond),
103+
func() (bool, error) {
104+
timestamps = append(timestamps, time.Now())
105+
return false, nil
106+
},
107+
)
108+
verify.ErrorContains(t, "exceeded", err)
109+
// This ticker has a non-zero offset, so it should run at least once before timing out
110+
verify.True(t, len(timestamps) > 0)
111+
112+
timestamps = []time.Time{}
113+
ctx, cancel := context.WithTimeout(context.Background(), 350*time.Millisecond)
114+
defer cancel()
115+
err = wait.Poll(
116+
ctx,
117+
wait.MakeJitteringTicker(
118+
50*time.Millisecond,
119+
10*time.Millisecond,
120+
1000*time.Millisecond),
121+
func() (bool, error) {
122+
timestamps = append(timestamps, time.Now())
123+
return false, nil
124+
},
125+
)
126+
verify.ErrorContains(t, "exceeded", err)
127+
// Context cancellation should still allow some ticks to happen
128+
verify.True(t, len(timestamps) > 0)
129+
}
130+
131+
// EOF

throttle.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Tideland Go Wait
22
//
3-
// Copyright (C) 2019-2023 Frank Mueller / Tideland / Oldenburg / Germany
3+
// Copyright (C) 2019-2025 Frank Mueller / Tideland / Oldenburg / Germany
44
//
55
// All rights reserved. Use of this source code is governed
66
// by the new BSD license.

0 commit comments

Comments
 (0)