@@ -3,6 +3,7 @@ package ebo
33import (
44 "context"
55 "errors"
6+ "iter"
67 "time"
78)
89
@@ -28,69 +29,73 @@ type Attempt struct {
2829// }
2930// log.Printf("Attempt %d failed", attempt.Number)
3031// }
31- func Attempts (opts ... Option ) func ( func ( * Attempt ) bool ) {
32+ func Attempts (opts ... Option ) iter. Seq [ * Attempt ] {
3233 config := & RetryConfig {
33- InitialInterval : 500 * time . Millisecond ,
34- MaxInterval : 30 * time . Second ,
35- MaxRetries : 10 ,
36- Multiplier : 2.0 ,
37- MaxElapsedTime : 5 * time . Minute ,
38- RandomizeFactor : 0.5 ,
34+ InitialInterval : defaultInitialInterval ,
35+ MaxInterval : defaultMaxInterval ,
36+ MaxRetries : defaultMaxRetries ,
37+ Multiplier : defaultMultiplier ,
38+ MaxElapsedTime : defaultMaxElapsedTime ,
39+ RandomizeFactor : defaultRandomizeFactor ,
3940 }
40-
41+
4142 for _ , opt := range opts {
4243 opt (config )
4344 }
44-
45+
4546 return func (yield func (* Attempt ) bool ) {
4647 startTime := time .Now ()
47- attempts := 0
48- var currentDelay time.Duration
49- nextInterval := config .InitialInterval
50- var lastError error
51-
52- for {
53- attempts ++
54- elapsed := time .Since (startTime )
55-
56- // Check stopping conditions
57- if attempts > 1 { // After first attempt
58- if config .MaxRetries > 0 && attempts > config .MaxRetries {
59- return
60- }
61- if config .MaxElapsedTime > 0 && elapsed >= config .MaxElapsedTime {
62- return
63- }
48+ currentInterval := config .InitialInterval
49+ elapsed := time .Duration (0 )
50+
51+ for i := 0 ; ; i ++ {
52+ // Check max retries
53+ if config .MaxRetries > 0 && i >= config .MaxRetries {
54+ return
6455 }
65-
56+
57+ // Check max elapsed time
58+ if config .MaxElapsedTime > 0 && elapsed > config .MaxElapsedTime {
59+ return
60+ }
61+
62+ // Create attempt with current delay
6663 attempt := & Attempt {
67- Number : attempts ,
68- Delay : currentDelay ,
69- Elapsed : elapsed ,
70- LastError : lastError ,
71- Context : context .Background (),
64+ Number : i + 1 ,
65+ Delay : currentInterval ,
66+ Elapsed : elapsed ,
67+ Context : context .Background (),
7268 }
73-
74- // Wait before attempt (except first one)
75- if currentDelay > 0 {
76- time . Sleep ( currentDelay )
69+
70+ // For the first attempt, set delay to 0
71+ if i == 0 {
72+ attempt . Delay = 0
7773 }
78-
79- // Yield control to the caller
74+
75+ // Wait before yielding (except for first attempt)
76+ if i > 0 {
77+ time .Sleep (currentInterval )
78+ elapsed = time .Since (startTime )
79+ }
80+
81+ // Yield attempt
8082 if ! yield (attempt ) {
8183 return
8284 }
83-
84- // Calculate delay for next attempt
85- currentDelay = nextInterval
8685
87- // Calculate next interval with jitter
88- nextInterval = min (time .Duration (float64 (nextInterval )* config .Multiplier ), config .MaxInterval )
86+ // Update interval for next iteration
87+ if config .Multiplier > 0 {
88+ currentInterval = time .Duration (float64 (currentInterval ) * config .Multiplier )
89+ }
90+
91+ // Apply max interval cap
92+ if currentInterval > config .MaxInterval {
93+ currentInterval = config .MaxInterval
94+ }
95+
96+ // Apply jitter if configured
8997 if config .RandomizeFactor > 0 {
90- delta := config .RandomizeFactor * float64 (nextInterval )
91- minInterval := float64 (nextInterval ) - delta
92- maxInterval := float64 (nextInterval ) + delta
93- nextInterval = time .Duration (minInterval + (randomFloat () * (maxInterval - minInterval )))
98+ currentInterval = getNextInterval (currentInterval , config .RandomizeFactor )
9499 }
95100 }
96101 }
@@ -109,21 +114,84 @@ func Attempts(opts ...Option) func(func(*Attempt) bool) {
109114// return nil
110115// }
111116// }
112- func AttemptsWithContext (ctx context.Context , opts ... Option ) func (func (* Attempt ) bool ) {
113- baseIterator := Attempts (opts ... )
117+ func AttemptsWithContext (ctx context.Context , opts ... Option ) iter.Seq [* Attempt ] {
118+ config := & RetryConfig {
119+ InitialInterval : defaultInitialInterval ,
120+ MaxInterval : defaultMaxInterval ,
121+ MaxRetries : defaultMaxRetries ,
122+ Multiplier : defaultMultiplier ,
123+ MaxElapsedTime : defaultMaxElapsedTime ,
124+ RandomizeFactor : defaultRandomizeFactor ,
125+ }
126+
127+ for _ , opt := range opts {
128+ opt (config )
129+ }
114130
115131 return func (yield func (* Attempt ) bool ) {
116- baseIterator (func (attempt * Attempt ) bool {
117- // Check context cancellation
118- select {
119- case <- ctx .Done ():
120- attempt .LastError = ctx .Err ()
121- return false
122- default :
123- attempt .Context = ctx
124- return yield (attempt )
125- }
126- })
132+ startTime := time .Now ()
133+ currentInterval := config .InitialInterval
134+ elapsed := time .Duration (0 )
135+
136+ for i := 0 ; ; i ++ {
137+ // Check context
138+ if ctx .Err () != nil {
139+ return
140+ }
141+
142+ // Check max retries
143+ if config .MaxRetries > 0 && i >= config .MaxRetries {
144+ return
145+ }
146+
147+ // Check max elapsed time
148+ if config .MaxElapsedTime > 0 && elapsed > config .MaxElapsedTime {
149+ return
150+ }
151+
152+ // Create attempt with current delay
153+ attempt := & Attempt {
154+ Number : i + 1 ,
155+ Delay : currentInterval ,
156+ Elapsed : elapsed ,
157+ Context : ctx ,
158+ }
159+
160+ // For the first attempt, set delay to 0
161+ if i == 0 {
162+ attempt .Delay = 0
163+ }
164+
165+ // Wait before yielding (except for first attempt)
166+ if i > 0 {
167+ select {
168+ case <- time .After (currentInterval ):
169+ elapsed = time .Since (startTime )
170+ case <- ctx .Done ():
171+ return
172+ }
173+ }
174+
175+ // Yield attempt
176+ if ! yield (attempt ) {
177+ return
178+ }
179+
180+ // Update interval for next iteration
181+ if config .Multiplier > 0 {
182+ currentInterval = time .Duration (float64 (currentInterval ) * config .Multiplier )
183+ }
184+
185+ // Apply max interval cap
186+ if currentInterval > config .MaxInterval {
187+ currentInterval = config .MaxInterval
188+ }
189+
190+ // Apply jitter if configured
191+ if config .RandomizeFactor > 0 {
192+ currentInterval = getNextInterval (currentInterval , config .RandomizeFactor )
193+ }
194+ }
127195 }
128196}
129197
@@ -136,25 +204,27 @@ func AttemptsWithContext(ctx context.Context, opts ...Option) func(func(*Attempt
136204// return apiCall()
137205// }, ebo.Tries(5))
138206func DoWithAttempts (fn func (* Attempt ) error , opts ... Option ) error {
139- var finalErr error
207+ var lastErr error
140208
141209 for attempt := range Attempts (opts ... ) {
142- err := fn (attempt )
143- if err == nil {
210+ if err := fn (attempt ); err == nil {
144211 return nil
212+ } else {
213+ lastErr = err
214+
215+ // Check if it's a permanent error
216+ var permanent * permanentError
217+ if errors .As (err , & permanent ) {
218+ return permanent .err
219+ }
220+ attempt .LastError = err
145221 }
146-
147- // Check for permanent errors
148- var permErr * permanentError
149- if errors .As (err , & permErr ) {
150- return permErr .err
151- }
152-
153- finalErr = err
154- attempt .LastError = err
155222 }
156223
157- return finalErr
224+ if lastErr != nil {
225+ return lastErr
226+ }
227+ return errors .New ("all retry attempts failed" )
158228}
159229
160230// DoWithAttemptsContext provides context-aware iteration.
@@ -167,28 +237,29 @@ func DoWithAttempts(fn func(*Attempt) error, opts ...Option) error {
167237// return apiCall(attempt.Context)
168238// }, ebo.Tries(3))
169239func DoWithAttemptsContext (ctx context.Context , fn func (* Attempt ) error , opts ... Option ) error {
170- var finalErr error
240+ var lastErr error
171241
172242 for attempt := range AttemptsWithContext (ctx , opts ... ) {
173- err := fn (attempt )
174- if err == nil {
243+ if err := fn (attempt ); err == nil {
175244 return nil
245+ } else {
246+ lastErr = err
247+
248+ // Check if it's a permanent error
249+ var permanent * permanentError
250+ if errors .As (err , & permanent ) {
251+ return permanent .err
252+ }
253+ attempt .LastError = err
176254 }
177-
178- // Check for permanent errors
179- var permErr * permanentError
180- if errors .As (err , & permErr ) {
181- return permErr .err
182- }
183-
184- finalErr = err
185- attempt .LastError = err
186255 }
187256
188- return finalErr
189- }
190-
191- // randomFloat returns a random float64 in [0.0, 1.0)
192- func randomFloat () float64 {
193- return float64 (time .Now ().UnixNano ()% 1000000 ) / 1000000.0
194- }
257+ if ctx .Err () != nil {
258+ return ctx .Err ()
259+ }
260+
261+ if lastErr != nil {
262+ return lastErr
263+ }
264+ return errors .New ("all retry attempts failed" )
265+ }
0 commit comments