Skip to content

Commit befb538

Browse files
committed
Add support for setting per-attempt-timeout
- At the moment, only "complete" requests can be retried (that result in an HTTP status).
1 parent 401eada commit befb538

File tree

7 files changed

+195
-0
lines changed

7 files changed

+195
-0
lines changed

authentication/authentication_option.go

+20
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,26 @@ func WithRetries(maxRetries int, statuses []int) Option {
8383
}
8484
}
8585

86+
// RetryStrategy defines the retry rules that should be followed by the SDK when making requests.
87+
type RetryStrategy struct {
88+
MaxRetries int
89+
Statuses []int
90+
91+
// PerAttemptTimeout can optionally be set to timeout individual API requests.
92+
PerAttemptTimeout time.Duration
93+
}
94+
95+
// WithRetryStrategy configures the management client to only retry under the conditions provided.
96+
func WithRetryStrategy(retryStrategy RetryStrategy) Option {
97+
return func(a *Authentication) {
98+
a.retryStrategy = client.RetryOptions{
99+
MaxRetries: retryStrategy.MaxRetries,
100+
Statuses: retryStrategy.Statuses,
101+
PerAttemptTimeout: retryStrategy.PerAttemptTimeout,
102+
}
103+
}
104+
}
105+
86106
// WithNoRetries configures the management client to only retry under the conditions provided.
87107
func WithNoRetries() Option {
88108
return func(a *Authentication) {

authentication/authentication_test.go

+85
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/url"
1212
"os"
1313
"runtime"
14+
"sync/atomic"
1415
"testing"
1516
"time"
1617

@@ -450,6 +451,90 @@ func TestRetries(t *testing.T) {
450451
assert.ErrorIs(t, err, context.Canceled)
451452
assert.Equal(t, 1, i) // 1 request should have been made before the context times out
452453
})
454+
455+
t.Run("Retry strategy", func(t *testing.T) {
456+
i := 0
457+
ctx, cancel := context.WithCancel(context.Background())
458+
459+
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
460+
i++
461+
cancel()
462+
w.WriteHeader(http.StatusBadGateway)
463+
})
464+
465+
s := httptest.NewTLSServer(h)
466+
defer s.Close()
467+
468+
a, err := New(
469+
context.Background(),
470+
s.URL,
471+
WithIDTokenSigningAlg("HS256"),
472+
WithClient(s.Client()),
473+
WithRetryStrategy(RetryStrategy{
474+
MaxRetries: 3,
475+
Statuses: []int{http.StatusBadGateway},
476+
PerAttemptTimeout: time.Second,
477+
}),
478+
)
479+
assert.NoError(t, err)
480+
481+
_, err = a.UserInfo(ctx, "123")
482+
assert.ErrorIs(t, err, context.Canceled)
483+
assert.Equal(t, 1, i) // 1 request should have been made before the context times out
484+
})
485+
486+
t.Run("Retry per request timeout", func(t *testing.T) {
487+
var i atomic.Int64
488+
ctx, cancel := context.WithCancel(context.Background())
489+
defer cancel()
490+
491+
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
492+
ctx := r.Context()
493+
494+
c := i.Add(1)
495+
t.Log(c)
496+
if c == 2 {
497+
cancel()
498+
}
499+
500+
timer := time.NewTimer(10 * time.Second)
501+
select {
502+
case <-timer.C:
503+
t.Log("completed")
504+
w.WriteHeader(http.StatusOK)
505+
return
506+
case <-ctx.Done():
507+
t.Log("cancelled")
508+
w.WriteHeader(499)
509+
return
510+
}
511+
})
512+
513+
s := httptest.NewTLSServer(h)
514+
defer s.Close()
515+
516+
m, err := New(
517+
context.Background(),
518+
s.URL,
519+
WithIDTokenSigningAlg("HS256"),
520+
WithClient(s.Client()),
521+
WithRetryStrategy(RetryStrategy{
522+
MaxRetries: 10,
523+
Statuses: []int{
524+
http.StatusInternalServerError,
525+
http.StatusBadGateway,
526+
http.StatusServiceUnavailable,
527+
http.StatusGatewayTimeout,
528+
},
529+
PerAttemptTimeout: 5 * time.Millisecond,
530+
}),
531+
)
532+
assert.NoError(t, err)
533+
534+
_, err = m.UserInfo(ctx, "123")
535+
assert.ErrorIs(t, err, context.Canceled)
536+
assert.Equal(t, int64(2), i.Load()) // 1 request should have been made before the context times out
537+
})
453538
}
454539

455540
func TestWithClockTolerance(t *testing.T) {

internal/client/client.go

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ var DefaultAuth0ClientInfo = &Auth0ClientInfo{
5757
type RetryOptions struct {
5858
MaxRetries int
5959
Statuses []int
60+
61+
// PerAttemptTimeout can optionally be set to timeout individual API requests.
62+
PerAttemptTimeout time.Duration
6063
}
6164

6265
// IsEmpty checks whether the provided Auth0ClientInfo data is nil or has no data to allow
@@ -111,6 +114,7 @@ func RetriesTransport(base http.RoundTripper, r RetryOptions) http.RoundTripper
111114
),
112115
backoffDelay(),
113116
)
117+
tr.PerAttemptTimeout = r.PerAttemptTimeout
114118

115119
return tr
116120
}

management/management.gen.go

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

management/management.gen_test.go

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

management/management_option.go

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package management
33
import (
44
"context"
55
"net/http"
6+
"time"
67

78
"github.com/auth0/go-auth0/internal/client"
89
)
@@ -123,6 +124,26 @@ func WithRetries(maxRetries int, statuses []int) Option {
123124
}
124125
}
125126

127+
// RetryStrategy defines the retry rules that should be followed by the SDK when making requests.
128+
type RetryStrategy struct {
129+
MaxRetries int
130+
Statuses []int
131+
132+
// PerAttemptTimeout can optionally be set to timeout individual API requests.
133+
PerAttemptTimeout time.Duration
134+
}
135+
136+
// WithRetryStrategy configures the management client to only retry under the conditions provided.
137+
func WithRetryStrategy(retryStrategy RetryStrategy) Option {
138+
return func(m *Management) {
139+
m.retryStrategy = client.RetryOptions{
140+
MaxRetries: retryStrategy.MaxRetries,
141+
Statuses: retryStrategy.Statuses,
142+
PerAttemptTimeout: retryStrategy.PerAttemptTimeout,
143+
}
144+
}
145+
}
146+
126147
// WithNoRetries configures the management client to only retry under the conditions provided.
127148
func WithNoRetries() Option {
128149
return func(m *Management) {

management/management_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http/httptest"
1111
"os"
1212
"runtime"
13+
"sync/atomic"
1314
"testing"
1415
"time"
1516

@@ -529,6 +530,57 @@ func TestRetries(t *testing.T) {
529530
assert.ErrorIs(t, err, context.Canceled)
530531
assert.Equal(t, 1, i) // 1 request should have been made before the context times out
531532
})
533+
534+
t.Run("Retry per request timeout", func(t *testing.T) {
535+
var i atomic.Int64
536+
ctx, cancel := context.WithCancel(context.Background())
537+
defer cancel()
538+
539+
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
540+
ctx := r.Context()
541+
542+
c := i.Add(1)
543+
t.Log(c)
544+
if c == 2 {
545+
cancel()
546+
}
547+
548+
timer := time.NewTimer(10 * time.Second)
549+
select {
550+
case <-timer.C:
551+
t.Log("completed")
552+
w.WriteHeader(http.StatusOK)
553+
return
554+
case <-ctx.Done():
555+
t.Log("cancelled")
556+
w.WriteHeader(499)
557+
return
558+
}
559+
})
560+
561+
s := httptest.NewServer(h)
562+
defer s.Close()
563+
564+
m, err := New(
565+
s.URL,
566+
WithInsecure(),
567+
WithRetryStrategy(RetryStrategy{
568+
MaxRetries: 10,
569+
Statuses: []int{
570+
http.StatusInternalServerError,
571+
http.StatusBadGateway,
572+
http.StatusServiceUnavailable,
573+
http.StatusGatewayTimeout,
574+
},
575+
PerAttemptTimeout: 5 * time.Millisecond,
576+
}),
577+
)
578+
assert.NoError(t, err)
579+
580+
_, err = m.User.Read(ctx, "123")
581+
assert.ErrorIs(t, err, context.Canceled)
582+
assert.Equal(t, int64(2), i.Load()) // 1 request should have been made before the context times out
583+
})
532584
}
533585

534586
func TestApiCallContextTimeout(t *testing.T) {

0 commit comments

Comments
 (0)