Skip to content

Commit 885bdb7

Browse files
authored
feat(go-sdk): improve retry mechanism (#506)
2 parents 29acb29 + 5836728 commit 885bdb7

20 files changed

+811
-226
lines changed

config/clients/go/CHANGELOG.md.mustache

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## [Unreleased](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v{{packageVersion}}...HEAD)
4+
5+
- feat: fix and improve retries and rate limit handling. (#176)
6+
The SDK now retries on network errors and the default retry handling has been fixed
7+
for both the calls to the OpenFGA API and the API Token Issuer for those using ClientCredentials
8+
The SDK now also respects the rate limit headers (`Retry-After`) returned by the server and will retry the request after the specified time.
9+
If the header is not sent or on network errors, it will fall back to exponential backoff.
10+
311
## v0.6.5
412

513
### [0.6.5](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.6.4...v0.6.5) (2025-02-06)

config/clients/go/config.overrides.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,20 @@
6969
"templateType": "SupportingFiles"
7070
},
7171
"oauth2/token_test.go": {},
72-
"internal/utils/randomtime.mustache": {
73-
"destinationFilename": "internal/utils/randomtime.go",
72+
"internal/utils/retryutils/retryutils.mustache": {
73+
"destinationFilename": "internal/utils/retryutils/retryutils.go",
74+
"templateType": "SupportingFiles"
75+
},
76+
"internal/utils/retryutils/retryutils_test.mustache": {
77+
"destinationFilename": "internal/utils/retryutils/retryutils_test.go",
78+
"templateType": "SupportingFiles"
79+
},
80+
"internal/utils/retryutils/retryparams.mustache": {
81+
"destinationFilename": "internal/utils/retryutils/retryparams.go",
82+
"templateType": "SupportingFiles"
83+
},
84+
"internal/utils/retryutils/retryparams_test.mustache": {
85+
"destinationFilename": "internal/utils/retryutils/retryparams_test.go",
7486
"templateType": "SupportingFiles"
7587
},
7688
"internal/utils/ulid.mustache": {

config/clients/go/template/api.mustache

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ package {{packageName}}
44
{{#operations}}
55
import (
66
"bytes"
7-
_context "context"
8-
_ioutil "io/ioutil"
9-
_nethttp "net/http"
10-
_neturl "net/url"
7+
"context"
8+
"io"
9+
"net/http"
10+
"net/url"
1111
"time"
1212
{{#imports}} "{{import}}"
1313
{{/imports}}
1414

15-
internalutils "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/internal/utils"
16-
telemetry "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/telemetry"
15+
"{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/internal/utils/retryutils"
16+
"{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/telemetry"
1717
)
1818

1919
// Linger please
2020
var (
21-
_ _context.Context
21+
_ context.Context
2222
)
2323
{{#generateInterfaces}}
2424

@@ -30,17 +30,17 @@ type {{classname}} interface {
3030
{{#notes}}
3131
* {{{unescapedNotes}}}
3232
{{/notes}}
33-
* @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().{{#pathParams}}
33+
* @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().{{#pathParams}}
3434
* @param {{paramName}}{{#description}} {{{.}}}{{/description}}{{/pathParams}}
3535
* @return {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request
3636
*/
37-
{{{nickname}}}(ctx _context.Context{{#pathParams}}, {{paramName}} {{{dataType}}}{{/pathParams}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request
37+
{{{nickname}}}(ctx context.Context{{#pathParams}}, {{paramName}} {{{dataType}}}{{/pathParams}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request
3838

3939
/*
4040
* {{nickname}}Execute executes the request{{#returnType}}
4141
* @return {{{.}}}{{/returnType}}
4242
*/
43-
{{nickname}}Execute(r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) ({{#returnType}}{{{.}}}, {{/returnType}}*_nethttp.Response, error)
43+
{{nickname}}Execute(r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) ({{#returnType}}{{{.}}}, {{/returnType}}*http.Response, error)
4444
{{/operation}}
4545
}
4646
{{/generateInterfaces}}
@@ -51,7 +51,7 @@ type {{classname}}Service service
5151
{{#operation}}
5252

5353
type {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request struct {
54-
ctx _context.Context{{#generateInterfaces}}
54+
ctx context.Context{{#generateInterfaces}}
5555
ApiService {{classname}}
5656
{{/generateInterfaces}}{{^generateInterfaces}}
5757
ApiService *{{classname}}Service
@@ -66,7 +66,7 @@ func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Reques
6666
return r
6767
}{{/isPathParam}}{{/allParams}}
6868

69-
func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) Execute() ({{#returnType}}{{{.}}}, {{/returnType}}*_nethttp.Response, error) {
69+
func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) Execute() ({{#returnType}}{{{.}}}, {{/returnType}}*http.Response, error) {
7070
return r.ApiService.{{nickname}}Execute(r)
7171
}
7272

@@ -75,11 +75,11 @@ func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Reques
7575
{{#notes}}
7676
* {{{unescapedNotes}}}
7777
{{/notes}}
78-
* @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().{{#pathParams}}
78+
* @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().{{#pathParams}}
7979
* @param {{paramName}}{{#description}} {{{.}}}{{/description}}{{/pathParams}}
8080
* @return {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request
8181
*/
82-
func (a *{{{classname}}}Service) {{{nickname}}}(ctx _context.Context{{#pathParams}}, {{paramName}} {{{dataType}}}{{/pathParams}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request {
82+
func (a *{{{classname}}}Service) {{{nickname}}}(ctx context.Context{{#pathParams}}, {{paramName}} {{{dataType}}}{{/pathParams}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request {
8383
return {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request{
8484
ApiService: a,
8585
ctx: ctx,{{#pathParams}}
@@ -91,22 +91,13 @@ func (a *{{{classname}}}Service) {{{nickname}}}(ctx _context.Context{{#pathParam
9191
* Execute executes the request{{#returnType}}
9292
* @return {{{.}}}{{/returnType}}
9393
*/
94-
func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) ({{#returnType}}{{{.}}}, {{/returnType}}*_nethttp.Response, error) {
95-
var maxRetry int
96-
var minWaitInMs int
97-
var requestStarted time.Time = time.Now()
98-
99-
if (a.RetryParams != nil) {
100-
maxRetry = a.RetryParams.MinWaitInMs
101-
minWaitInMs = a.RetryParams.MinWaitInMs
102-
} else {
103-
maxRetry = 0
104-
minWaitInMs = 0
105-
}
94+
func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) ({{#returnType}}{{{.}}}, {{/returnType}}*http.Response, error) {
95+
var requestStarted = time.Now()
10696
107-
for i := 0; i < maxRetry+1; i++ {
97+
retryParams := a.client.cfg.RetryParams
98+
for i := 0; i < retryParams.MaxRetry+1; i++ {
10899
var (
109-
localVarHTTPMethod = _nethttp.Method{{httpMethod}}
100+
localVarHTTPMethod = http.Method{{httpMethod}}
110101
localVarPostBody interface{}
111102
{{#returnType}}
112103
localVarReturnValue {{{.}}}
@@ -118,10 +109,10 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
118109
return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} is required and must be specified")
119110
}
120111

121-
localVarPath = strings.Replace(localVarPath, "{"+"{{baseName}}"+"}", _neturl.PathEscape(parameterToString(r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")), -1){{/pathParams}}
112+
localVarPath = strings.Replace(localVarPath, "{"+"{{baseName}}"+"}", url.PathEscape(parameterToString(r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")), -1){{/pathParams}}
122113

123114
localVarHeaderParams := make(map[string]string)
124-
localVarQueryParams := _neturl.Values{}
115+
localVarQueryParams := url.Values{}
125116
{{#allParams}}
126117
{{#required}}
127118
{{^isPathParam}}
@@ -256,7 +247,7 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
256247
}
257248
{{/required}}
258249
if localVarFile != nil {
259-
fbs, _ := _ioutil.ReadAll(localVarFile)
250+
fbs, _ := io.ReadAll(localVarFile)
260251
localVarFileBytes = fbs
261252
localVarFileName = localVarFile.Name()
262253
localVarFile.Close()
@@ -331,19 +322,33 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
331322

332323
localVarHTTPResponse, err := a.client.callAPI(req)
333324
if err != nil || localVarHTTPResponse == nil {
325+
if i < retryParams.MaxRetry {
326+
timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, "{{nickname}}")
327+
if timeToWait > 0 {
328+
time.Sleep(timeToWait)
329+
continue
330+
}
331+
}
334332
return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, err
335333
}
336334

337-
localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body)
335+
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
338336
localVarHTTPResponse.Body.Close()
339-
localVarHTTPResponse.Body = _ioutil.NopCloser(bytes.NewBuffer(localVarBody))
337+
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
340338
if err != nil {
339+
if i < retryParams.MaxRetry {
340+
timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, localVarHTTPResponse.Header, "{{nickname}}")
341+
if timeToWait > 0 {
342+
time.Sleep(timeToWait)
343+
continue
344+
}
345+
}
341346
return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, err
342347
}
343348

344-
if localVarHTTPResponse.StatusCode >= _nethttp.StatusMultipleChoices {
349+
if localVarHTTPResponse.StatusCode >= http.StatusMultipleChoices {
345350
346-
if localVarHTTPResponse.StatusCode == _nethttp.StatusBadRequest || localVarHTTPResponse.StatusCode == _nethttp.StatusUnprocessableEntity {
351+
if localVarHTTPResponse.StatusCode == http.StatusBadRequest || localVarHTTPResponse.StatusCode == http.StatusUnprocessableEntity {
347352
newErr := FgaApiValidationError{
348353
body: localVarBody,{{#pathParams.0}}
349354
storeId: r.storeId,{{/pathParams.0}}
@@ -369,7 +374,7 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
369374
return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, newErr
370375
}
371376

372-
if localVarHTTPResponse.StatusCode == _nethttp.StatusUnauthorized || localVarHTTPResponse.StatusCode == _nethttp.StatusForbidden {
377+
if localVarHTTPResponse.StatusCode == http.StatusUnauthorized || localVarHTTPResponse.StatusCode == http.StatusForbidden {
373378
newErr := FgaApiAuthenticationError{
374379
body: localVarBody,{{#pathParams.0}}
375380
storeId: r.storeId,{{/pathParams.0}}
@@ -384,7 +389,7 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
384389
return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, newErr
385390
}
386391

387-
if localVarHTTPResponse.StatusCode == _nethttp.StatusNotFound {
392+
if localVarHTTPResponse.StatusCode == http.StatusNotFound {
388393
newErr := FgaApiNotFoundError{
389394
body: localVarBody,{{#pathParams.0}}
390395
storeId: r.storeId,{{/pathParams.0}}
@@ -410,10 +415,13 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
410415
return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, newErr
411416
}
412417

413-
if localVarHTTPResponse.StatusCode == _nethttp.StatusTooManyRequests {
414-
if i < maxRetry {
415-
time.Sleep(time.Duration(internalutils.RandomTime(i, minWaitInMs)) * time.Millisecond)
416-
continue
418+
if localVarHTTPResponse.StatusCode == http.StatusTooManyRequests {
419+
if i < retryParams.MaxRetry {
420+
timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, localVarHTTPResponse.Header, "{{nickname}}")
421+
if timeToWait > 0 {
422+
time.Sleep(timeToWait)
423+
continue
424+
}
417425
}
418426
// maximum number of retry reached
419427
newErr := FgaApiRateLimitExceededError{
@@ -433,10 +441,13 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
433441
return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, newErr
434442
}
435443

436-
if localVarHTTPResponse.StatusCode >= _nethttp.StatusInternalServerError {
437-
if localVarHTTPResponse.StatusCode != _nethttp.StatusNotImplemented && i < maxRetry {
438-
time.Sleep(time.Duration(internalutils.RandomTime(i, minWaitInMs)) * time.Millisecond)
439-
continue
444+
if localVarHTTPResponse.StatusCode >= http.StatusInternalServerError {
445+
if localVarHTTPResponse.StatusCode != http.StatusNotImplemented && i < retryParams.MaxRetry {
446+
timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, localVarHTTPResponse.Header, "{{nickname}}")
447+
if timeToWait > 0 {
448+
time.Sleep(timeToWait)
449+
continue
450+
}
440451
}
441452
newErr := FgaApiInternalError{
442453
body: localVarBody,{{#pathParams.0}}

config/clients/go/template/api_client.mustache

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"time"
2121
"unicode/utf8"
2222

23+
"{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/internal/utils/retryutils"
2324
"{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/telemetry"
2425
)
2526

@@ -74,7 +75,7 @@ func NewAPIClient(cfg *Configuration) *APIClient {
7475
} else {
7576
cfg.Credentials.Context = context.Background()
7677
telemetry.Bind(cfg.Credentials.Context, telemetry.Get(telemetry.TelemetryFactoryParameters{Configuration: cfg.Telemetry}))
77-
var httpClient, headers = cfg.Credentials.GetHttpClientAndHeaderOverrides()
78+
var httpClient, headers = cfg.Credentials.GetHttpClientAndHeaderOverrides(retryutils.GetRetryParamsOrDefault(cfg.RetryParams))
7879
if len(headers) > 0 {
7980
for idx := range headers {
8081
cfg.AddDefaultHeader(headers[idx].Key, headers[idx].Value)

0 commit comments

Comments
 (0)