Skip to content

Commit ee3349a

Browse files
committed
refactor!(run): fix intended Every/Until behavior
1 parent 5ab2725 commit ee3349a

File tree

2 files changed

+101
-55
lines changed

2 files changed

+101
-55
lines changed

run/run.go

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,94 @@ package run
22

33
import (
44
"context"
5-
"errors"
5+
"fmt"
66
"time"
7-
)
8-
9-
type Interval interface {
10-
time.Duration | RetryOptions
11-
}
12-
13-
type RetryFunc interface {
14-
func() | func() bool | func() error
15-
}
167

17-
// hack to avoid errcheck
18-
type error_ = error
8+
"go.chrisrx.dev/x/backoff"
9+
"go.chrisrx.dev/x/must"
10+
)
1911

20-
// Every runs a function periodically for the provided interval.
12+
// Every runs a function periodically for the provided interval. It runs
13+
// indefinitely or until the context is done.
2114
//
2215
// The interval can be either a [time.Duration] or, if more complex retry logic
23-
// is required, a [RetryOptions]. Given a [time.Duration], this will run the
24-
// function forever at a constant interval.
25-
//
26-
// The context passed in can be used to return early, regardless of the
27-
// interval provided. If Every returns early, the last error (if any) will be
28-
// returned.
29-
func Every[R RetryFunc, T Interval](ctx context.Context, fn R, interval T) error_ {
30-
return Retry(ctx, asRetryFunc(fn), retryOptionsFromInterval(interval)).WaitE()
16+
// is required, a [RetryOptions].
17+
func Every[T Interval](ctx context.Context, fn func(), interval T) {
18+
ro := retryOptionsFromInterval(interval)
19+
// ignore these user provided values so this runs indefinitely
20+
ro.MaxAttempts = 0
21+
ro.MaxElapsedTime = 0
22+
_ = Do(ctx, func() (bool, error) {
23+
fn()
24+
return false, nil
25+
}, ro)
3126
}
3227

3328
// Until runs a function periodically for the provided interval. This is used
34-
// for running logic until something is successful. Until has different
35-
// behaviors depending on the retry function that is passed in. If the function
36-
// returns an error, it will run until no error is returned. If given a retry
37-
// function returning a bool, then it will run until true is returned.
29+
// for running logic until something is successful or until the context is
30+
// done.
31+
//
32+
// Until has different behaviors depending on the retry function that is passed
33+
// in. If the function returns an error, it will run until no error is
34+
// encountered. If given a retry function returning a bool, then it will run
35+
// until true is returned.
3836
//
3937
// The interval can be either a [time.Duration] or, if more complex retry logic
40-
// is required, a [RetryOptions]. Given a [time.Duration], this will run the
41-
// function forever at a constant interval.
38+
// is required, a [RetryOptions].
39+
func Until[R RetryFunc, T Interval](ctx context.Context, fn R, interval T) error {
40+
return Do(ctx, asRetryFunc(fn), retryOptionsFromInterval(interval))
41+
}
42+
43+
// Unless runs a function periodically for the provided interval. This is used
44+
// for running logic until something is unsuccessful. It is the inverse of
45+
// [Until].
46+
//
47+
// Until has different behaviors depending on the retry function that is passed
48+
// in. If the function returns an error, it will run until no error is
49+
// encountered. If given a retry function returning a bool, then it will run
50+
// until true is returned.
4251
//
43-
// The context passed in can be used to return early, regardless of the
44-
// interval provided. If Until returns early, the last error (if any) will be
45-
// returned.
46-
func Until[R RetryFunc, T Interval](ctx context.Context, fn R, interval T) error_ {
47-
return Retry(ctx, asRetryFunc(fn), retryOptionsFromInterval(interval)).WaitE()
52+
// The interval can be either a [time.Duration] or, if more complex retry logic
53+
// is required, a [RetryOptions].
54+
func Unless[R RetryFunc, T Interval](ctx context.Context, fn R, interval T) error {
55+
return Do(ctx, func() (bool, error) {
56+
ok, err := asRetryFunc(fn)()
57+
return !ok, err
58+
}, retryOptionsFromInterval(interval))
59+
}
60+
61+
func Do(ctx context.Context, fn func() (bool, error), ro RetryOptions) error {
62+
if ro.MaxElapsedTime != 0 {
63+
var cancel context.CancelFunc
64+
ctx, cancel = context.WithTimeout(ctx, ro.MaxElapsedTime)
65+
defer cancel()
66+
}
67+
ticker := backoff.NewTicker(ro.Backoff())
68+
defer ticker.Stop()
69+
70+
var attempts int
71+
for {
72+
select {
73+
case <-ticker.Next():
74+
attempts++
75+
done, err := func() (_ bool, reterr error) {
76+
defer must.Catch(&reterr)
77+
return fn()
78+
}()
79+
if done {
80+
return err
81+
}
82+
if ro.MaxAttempts != 0 && attempts >= ro.MaxAttempts {
83+
return fmt.Errorf("max attempts")
84+
}
85+
case <-ctx.Done():
86+
return nil
87+
}
88+
}
89+
}
90+
91+
type Interval interface {
92+
time.Duration | RetryOptions
4893
}
4994

5095
func retryOptionsFromInterval[T Interval](interval T) RetryOptions {
@@ -60,23 +105,22 @@ func retryOptionsFromInterval[T Interval](interval T) RetryOptions {
60105
}
61106
}
62107

63-
var errContinue = errors.New("continue")
108+
type RetryFunc interface {
109+
func() bool | func() error | func() (bool, error)
110+
}
64111

65-
func asRetryFunc[T RetryFunc](fn T) func() error {
112+
func asRetryFunc[R RetryFunc](fn R) func() (bool, error) {
66113
switch fn := any(fn).(type) {
67-
case func():
68-
return func() error {
69-
fn()
70-
return errContinue
71-
}
72114
case func() bool:
73-
return func() error {
74-
if fn() {
75-
return nil
76-
}
77-
return errContinue
115+
return func() (bool, error) {
116+
return fn(), nil
78117
}
79118
case func() error:
119+
return func() (bool, error) {
120+
err := fn()
121+
return err == nil, err
122+
}
123+
case func() (bool, error):
80124
return fn
81125
default:
82126
panic("unreachable")

run/run_test.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,36 +38,37 @@ func TestUntil(t *testing.T) {
3838
assert.Equal(t, 5, n)
3939
})
4040

41-
t.Run("until func()", func(t *testing.T) {
41+
t.Run("until func() bool", func(t *testing.T) {
4242
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
4343
defer cancel()
4444

4545
var n int
46-
run.Until(ctx, func() {
46+
run.Until(ctx, func() bool {
4747
n++
48-
if n >= 5 {
49-
cancel()
50-
}
48+
return n >= 5
5149
}, 10*time.Millisecond)
5250

5351
assert.Equal(t, 5, n)
5452
})
5553

56-
t.Run("until func() bool", func(t *testing.T) {
54+
t.Run("until func() error", func(t *testing.T) {
5755
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
5856
defer cancel()
5957

6058
var n int
61-
run.Until(ctx, func() bool {
59+
run.Until(ctx, func() error {
6260
n++
63-
return n >= 5
61+
if n >= 5 {
62+
return nil
63+
}
64+
return fmt.Errorf("retry")
6465
}, 10*time.Millisecond)
6566

6667
assert.Equal(t, 5, n)
6768
})
6869

69-
t.Run("until func() error", func(t *testing.T) {
70-
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
70+
t.Run("ping", func(t *testing.T) {
71+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
7172
defer cancel()
7273

7374
var n int
@@ -76,8 +77,9 @@ func TestUntil(t *testing.T) {
7677
if n >= 5 {
7778
return nil
7879
}
80+
fmt.Printf("not yet")
7981
return fmt.Errorf("retry")
80-
}, 10*time.Millisecond)
82+
}, 1*time.Second)
8183

8284
assert.Equal(t, 5, n)
8385
})

0 commit comments

Comments
 (0)