@@ -2,49 +2,94 @@ package run
22
33import (
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
5095func 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" )
0 commit comments