Skip to content

Commit 07ab789

Browse files
committed
Update documentation and RetryOptions
A more idiomatic approach for Go would be to use time.Duration instead of int representation of Milliseconds.
1 parent 2a286c6 commit 07ab789

File tree

4 files changed

+108
-98
lines changed

4 files changed

+108
-98
lines changed

README.md

Lines changed: 82 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -229,40 +229,44 @@ type TokenManagerOptions struct {
229229
// Default: 0.7 (refresh at 70% of token lifetime)
230230
ExpirationRefreshRatio float64
231231

232-
// Optional: Minimum time before expiration to refresh (ms)
233-
// Default: 10000 (10 seconds)
234-
LowerRefreshBounds int64
232+
// Optional: Minimum time before expiration to trigger refresh
233+
// Default: 0 (no lower bound, refresh based on ExpirationRefreshRatio)
234+
LowerRefreshBound time.Duration
235+
236+
// Optional: Custom response parser
237+
IdentityProviderResponseParser shared.IdentityProviderResponseParser
235238

236239
// Optional: Configuration for retry behavior
237240
RetryOptions RetryOptions
238241

239-
// Optional: Custom response parser
240-
IdentityProviderResponseParser IdentityProviderResponseParser
242+
// Optional: Timeout for token requests
243+
RequestTimeout time.Duration
241244
}
242245
```
243246

244247
#### 3. RetryOptions
245248
Options for retry behavior:
246249
```go
247250
type RetryOptions struct {
251+
// Optional: Function to determine if an error is retryable
252+
// Default: Checks for network errors and timeouts
253+
IsRetryable func(err error) bool
254+
248255
// Optional: Maximum number of retry attempts
249256
// Default: 3
250257
MaxAttempts int
251258

252-
// Optional: Initial delay between retries (ms)
253-
// Default: 1000 (1 second)
254-
InitialDelayMs int64
259+
// Optional: Initial delay between retries
260+
// Default: 1 second
261+
InitialDelay time.Duration
255262

256-
// Optional: Maximum delay between retries (ms)
257-
// Default: 30000 (30 seconds)
258-
MaxDelayMs int64
263+
// Optional: Maximum delay between retries
264+
// Default: 10 seconds
265+
MaxDelay time.Duration
259266

260267
// Optional: Multiplier for exponential backoff
261268
// Default: 2.0
262269
BackoffMultiplier float64
263-
264-
// Optional: Custom retry predicate
265-
IsRetryable func(error) bool
266270
}
267271
```
268272

@@ -364,8 +368,8 @@ options := entraid.CredentialsProviderOptions{
364368
LowerRefreshBounds: 10000,
365369
RetryOptions: manager.RetryOptions{
366370
MaxAttempts: 3,
367-
InitialDelayMs: 1000,
368-
MaxDelayMs: 30000,
371+
InitialDelay: 1000 * time.Millisecond,
372+
MaxDelay: 30000 * time.Millisecond,
369373
BackoffMultiplier: 2.0,
370374
IsRetryable: func(err error) bool {
371375
return strings.Contains(err.Error(), "network error") ||
@@ -475,6 +479,7 @@ import (
475479
"fmt"
476480
"log"
477481
"os"
482+
"strings"
478483
"time"
479484

480485
"github.com/redis-developer/go-redis-entraid/entraid"
@@ -516,7 +521,18 @@ func main() {
516521
tokenManager, err := manager.NewTokenManager(customProvider, manager.TokenManagerOptions{
517522
// Configure token refresh behavior
518523
ExpirationRefreshRatio: 0.7,
519-
LowerRefreshBounds: 10000,
524+
LowerRefreshBound: time.Second * 10,
525+
RetryOptions: manager.RetryOptions{
526+
MaxAttempts: 3,
527+
InitialDelay: time.Second,
528+
MaxDelay: time.Second * 10,
529+
BackoffMultiplier: 2.0,
530+
IsRetryable: func(err error) bool {
531+
return strings.Contains(err.Error(), "network error") ||
532+
strings.Contains(err.Error(), "timeout")
533+
},
534+
},
535+
RequestTimeout: time.Second * 30,
520536
})
521537
if err != nil {
522538
log.Fatalf("Failed to create token manager: %v", err)
@@ -561,6 +577,7 @@ Key points about this implementation:
561577
- Uses our `TokenManager` for automatic token refresh
562578
- Benefits from our retry mechanisms
563579
- Handles token caching and lifecycle
580+
- Configurable refresh timing and retry behavior
564581

565582
3. **Streaming Credentials**:
566583
- Uses our `StreamingCredentialsProvider` for Redis integration
@@ -631,7 +648,53 @@ func TestRedisConnection(t *testing.T) {
631648
## FAQ
632649

633650
### Q: How do I handle token expiration?
634-
A: The library handles token expiration automatically. Tokens are refreshed when they reach 70% of their lifetime (configurable via `ExpirationRefreshRatio`). You can customize this behavior using `TokenManagerOptions`.
651+
A: The library handles token expiration automatically. Tokens are refreshed when they reach 70% of their lifetime (configurable via `ExpirationRefreshRatio`). You can also set a minimum time before expiration to trigger refresh using `LowerRefreshBound`. The token manager will automatically handle token refresh and caching.
652+
653+
### Q: How do I handle connection failures?
654+
A: The library includes built-in retry mechanisms in the TokenManager. You can configure retry behavior using `RetryOptions`:
655+
```go
656+
RetryOptions: manager.RetryOptions{
657+
MaxAttempts: 3,
658+
InitialDelay: time.Second,
659+
MaxDelay: time.Second * 10,
660+
BackoffMultiplier: 2.0,
661+
IsRetryable: func(err error) bool {
662+
return strings.Contains(err.Error(), "network error") ||
663+
strings.Contains(err.Error(), "timeout")
664+
},
665+
}
666+
```
667+
668+
### Q: What happens if token refresh fails?
669+
A: The library will retry according to the configured `RetryOptions`. If all retries fail, the error will be propagated to the client. You can customize the retry behavior by:
670+
1. Setting the maximum number of attempts
671+
2. Configuring the initial and maximum delay between retries using `time.Duration` values
672+
3. Setting the backoff multiplier for exponential backoff
673+
4. Providing a custom function to determine which errors are retryable
674+
675+
### Q: How do I implement custom authentication?
676+
A: You can create a custom identity provider by implementing the `IdentityProvider` interface:
677+
```go
678+
type IdentityProvider interface {
679+
// RequestToken requests a token from the identity provider.
680+
// It returns the token, the expiration time, and an error if any.
681+
RequestToken() (IdentityProviderResponse, error)
682+
}
683+
```
684+
685+
The `IdentityProviderResponse` interface provides methods to access the authentication result:
686+
```go
687+
type IdentityProviderResponse interface {
688+
// Type returns the type of the auth result
689+
Type() string
690+
AuthResult() public.AuthResult
691+
AccessToken() azcore.AccessToken
692+
RawToken() string
693+
}
694+
```
695+
696+
### Q: Can I customize how token responses are parsed?
697+
A: Yes, you can provide a custom `IdentityProviderResponseParser` in the `TokenManagerOptions`. This allows you to handle custom token formats or implement special parsing logic.
635698

636699
### Q: What's the difference between managed identity types?
637700
A: There are three main types of managed identities in Azure:
@@ -664,57 +727,4 @@ A: There are three main types of managed identities in Azure:
664727
The choice between these types depends on your specific use case:
665728
- Use System Assigned for single-resource applications
666729
- Use User Assigned for shared identity scenarios
667-
- Use Default Azure Identity for development and testing
668-
669-
### Q: How do I handle connection failures?
670-
A: The library includes built-in retry mechanisms in the TokenManager. You can configure retry behavior using `RetryOptions`:
671-
```go
672-
RetryOptions: manager.RetryOptions{
673-
MaxAttempts: 3,
674-
InitialDelayMs: 1000,
675-
MaxDelayMs: 30000,
676-
BackoffMultiplier: 2.0,
677-
}
678-
```
679-
680-
### Q: Does this work with Redis Cluster?
681-
A: Yes, the library works with both standalone Redis and Redis Cluster. Use the appropriate Redis client constructor:
682-
```go
683-
// For standalone Redis
684-
client := redis.NewClient(&redis.Options{
685-
Addr: "your-endpoint:6380",
686-
StreamingCredentialsProvider: provider,
687-
})
688-
689-
// For Redis Cluster
690-
client := redis.NewClusterClient(&redis.ClusterOptions{
691-
Addrs: []string{"your-endpoint:6380"},
692-
StreamingCredentialsProvider: provider,
693-
})
694-
```
695-
696-
### Q: How do I implement custom authentication?
697-
A: You can create a custom identity provider by implementing the `IdentityProvider` interface:
698-
```go
699-
// IdentityProviderResponse is an interface that defines the methods for an identity provider authentication result.
700-
// It is used to get the type of the authentication result, the authentication result itself (can be AuthResult or AccessToken),
701-
type IdentityProviderResponse interface {
702-
// Type returns the type of the auth result
703-
Type() string
704-
AuthResult() public.AuthResult
705-
AccessToken() azcore.AccessToken
706-
RawToken() string
707-
}
708-
709-
// IdentityProvider is an interface that defines the methods for an identity provider.
710-
// It is used to request a token for authentication.
711-
// The identity provider is responsible for providing the raw authentication token.
712-
type IdentityProvider interface {
713-
// RequestToken requests a token from the identity provider.
714-
// It returns the token, the expiration time, and an error if any.
715-
RequestToken() (IdentityProviderResponse, error)
716-
}
717-
```
718-
719-
### Q: What happens if token refresh fails?
720-
A: The library will retry according to the configured `RetryOptions`. If all retries fail, the error will be propagated to the client.
730+
- Use Default Azure Identity for development and testing

manager/defaults.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import (
1515
const (
1616
DefaultExpirationRefreshRatio = 0.7
1717
DefaultRetryOptionsMaxAttempts = 3
18-
DefaultRetryOptionsInitialDelayMs = 1000
1918
DefaultRetryOptionsBackoffMultiplier = 2.0
20-
DefaultRetryOptionsMaxDelayMs = 10000
19+
DefaultRetryOptionsInitialDelay = 1000 * time.Millisecond
20+
DefaultRetryOptionsMaxDelay = 10000 * time.Millisecond
2121
)
2222

2323
// defaultIsRetryable is a function that checks if the error is retriable.
@@ -57,14 +57,14 @@ func defaultRetryOptionsOr(retryOptions RetryOptions) RetryOptions {
5757
if retryOptions.MaxAttempts <= 0 {
5858
retryOptions.MaxAttempts = DefaultRetryOptionsMaxAttempts
5959
}
60-
if retryOptions.InitialDelayMs == 0 {
61-
retryOptions.InitialDelayMs = DefaultRetryOptionsInitialDelayMs
60+
if retryOptions.InitialDelay == 0 {
61+
retryOptions.InitialDelay = DefaultRetryOptionsInitialDelay
6262
}
6363
if retryOptions.BackoffMultiplier == 0 {
6464
retryOptions.BackoffMultiplier = DefaultRetryOptionsBackoffMultiplier
6565
}
66-
if retryOptions.MaxDelayMs == 0 {
67-
retryOptions.MaxDelayMs = DefaultRetryOptionsMaxDelayMs
66+
if retryOptions.MaxDelay == 0 {
67+
retryOptions.MaxDelay = DefaultRetryOptionsMaxDelay
6868
}
6969
return retryOptions
7070
}

manager/token_manager.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ type RetryOptions struct {
5858
//
5959
// default: 3
6060
MaxAttempts int
61-
// InitialDelayMs is the initial delay in milliseconds before retrying the token request.
61+
// InitialDelay is the initial delay before retrying the token request.
6262
//
63-
// default: 1000 ms
64-
InitialDelayMs int
65-
// MaxDelayMs is the maximum delay in milliseconds between retry attempts.
63+
// default: 1 second
64+
InitialDelay time.Duration
65+
// MaxDelay is the maximum delay between retry attempts.
6666
//
67-
// default: 10000 ms
68-
MaxDelayMs int
67+
// default: 10 seconds
68+
MaxDelay time.Duration
6969
// BackoffMultiplier is the multiplier for the backoff delay.
7070
// default: 2.0
7171
BackoffMultiplier float64
@@ -265,8 +265,8 @@ func (e *entraidTokenManager) Start(listener TokenListener) (StopFunc, error) {
265265
e.listener = listener
266266

267267
go func(listener TokenListener, closed <-chan struct{}) {
268-
maxDelay := time.Duration(e.retryOptions.MaxDelayMs) * time.Millisecond
269-
initialDelay := time.Duration(e.retryOptions.InitialDelayMs) * time.Millisecond
268+
maxDelay := e.retryOptions.MaxDelay
269+
initialDelay := e.retryOptions.InitialDelay
270270

271271
for {
272272
timeToRenewal := e.durationToRenewal()

manager/token_manager_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ func TestTokenManagerWithOptions(t *testing.T) {
8888
assert.NotNil(t, tm.retryOptions.IsRetryable)
8989
assertFuncNameMatches(t, tm.retryOptions.IsRetryable, defaultIsRetryable)
9090
assert.Equal(t, DefaultRetryOptionsMaxAttempts, tm.retryOptions.MaxAttempts)
91-
assert.Equal(t, DefaultRetryOptionsInitialDelayMs, tm.retryOptions.InitialDelayMs)
92-
assert.Equal(t, DefaultRetryOptionsMaxDelayMs, tm.retryOptions.MaxDelayMs)
91+
assert.Equal(t, DefaultRetryOptionsInitialDelay, tm.retryOptions.InitialDelay)
92+
assert.Equal(t, DefaultRetryOptionsMaxDelay, tm.retryOptions.MaxDelay)
9393
assert.Equal(t, DefaultRetryOptionsBackoffMultiplier, tm.retryOptions.BackoffMultiplier)
9494
})
9595
}
@@ -865,7 +865,7 @@ func TestEntraidTokenManager_Streaming(t *testing.T) {
865865
<-time.After(10 * time.Millisecond)
866866
assert.NoError(t, cancel())
867867

868-
assert.InDelta(t, stop.Sub(start), time.Duration(tm.retryOptions.InitialDelayMs)*time.Millisecond, float64(200*time.Millisecond))
868+
assert.InDelta(t, stop.Sub(start), tm.retryOptions.InitialDelay, float64(200*time.Millisecond))
869869

870870
idp.AssertNumberOfCalls(t, "RequestToken", 2)
871871
listener.AssertNumberOfCalls(t, "OnNext", 2)
@@ -880,7 +880,7 @@ func TestEntraidTokenManager_Streaming(t *testing.T) {
880880
TokenManagerOptions{
881881
LowerRefreshBound: time.Hour,
882882
RetryOptions: RetryOptions{
883-
InitialDelayMs: 5000, // 5 seconds
883+
InitialDelay: 5 * time.Second,
884884
},
885885
},
886886
)
@@ -916,7 +916,7 @@ func TestEntraidTokenManager_Streaming(t *testing.T) {
916916
assert.Equal(t, time.Duration(0), toRenewal)
917917
assert.True(t, expiresIn > toRenewal)
918918

919-
<-time.After(time.Duration(tm.retryOptions.InitialDelayMs/2) * time.Millisecond)
919+
<-time.After(time.Duration(tm.retryOptions.InitialDelay / 2))
920920
assert.NoError(t, cancel())
921921
assert.Nil(t, tm.listener)
922922
assert.Panics(t, func() {
@@ -1090,14 +1090,14 @@ func TestEntraidTokenManager_Streaming(t *testing.T) {
10901090
idp := &mockIdentityProvider{}
10911091
listener := &mockTokenListener{}
10921092
maxAttempts := 3
1093-
maxDelayMs := 500
1094-
initialDelayMs := 100
1093+
maxDelay := 500 * time.Millisecond
1094+
initialDelay := 100 * time.Millisecond
10951095
tokenManager, err := NewTokenManager(idp,
10961096
TokenManagerOptions{
10971097
RetryOptions: RetryOptions{
10981098
MaxAttempts: maxAttempts,
1099-
MaxDelayMs: maxDelayMs,
1100-
InitialDelayMs: initialDelayMs,
1099+
MaxDelay: maxDelay,
1100+
InitialDelay: initialDelay,
11011101
BackoffMultiplier: 10,
11021102
},
11031103
},
@@ -1158,15 +1158,15 @@ func TestEntraidTokenManager_Streaming(t *testing.T) {
11581158
idp.On("RequestToken", mock.Anything).Return(nil, returnErr)
11591159

11601160
select {
1161-
case <-time.After(toRenewal + time.Duration(maxAttempts*maxDelayMs)*time.Millisecond):
1161+
case <-time.After(toRenewal + time.Duration(maxAttempts)*maxDelay):
11621162
assert.Fail(t, "Timeout - max retries not reached")
11631163
case <-maxAttemptsReached:
11641164
}
11651165

11661166
// initialRenewal window, maxAttempts - 1 * max delay + the initial one which was lower than max delay
11671167
allDelaysShouldBe := toRenewal
1168-
allDelaysShouldBe += time.Duration(initialDelayMs) * time.Millisecond
1169-
allDelaysShouldBe += time.Duration(maxAttempts-1) * time.Duration(maxDelayMs) * time.Millisecond
1168+
allDelaysShouldBe += initialDelay
1169+
allDelaysShouldBe += time.Duration(maxAttempts-1) * maxDelay
11701170

11711171
assert.InEpsilon(t, elapsed, allDelaysShouldBe, float64(10*time.Millisecond))
11721172

0 commit comments

Comments
 (0)