Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion authorize/.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.0
0.7.1
4 changes: 4 additions & 0 deletions authorize/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v0.7.1 (Unreleased)

* **Note** Enhance backoff retry logic for transient errors according to [best practices](https://apidocs.pingidentity.com/pingone/platform/v1/api/#retries-best-practice-for-managing-transient-api-errors).

# v0.7.0 (2024-11-15)

* **Breaking change** `(Api[a-zA-Z]Request).Execute()` and `(*Api[a-zA-Z]Request).[a-zA-Z]Execute()` API functions now returns the `EntityArrayPagedIterator` data type to for code clients to implement paging of results. [#392](https://github.com/patrickcping/pingone-go-sdk-v2/pull/392)
Expand Down
2 changes: 1 addition & 1 deletion authorize/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The PingOne Platform API covering the PingOne Authorize service
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client.

- API version: 2023-06-29
- Package version: 0.7.0
- Package version: 0.7.1
- Build package: org.openapitools.codegen.languages.GoClientCodegen

## Installation
Expand Down
45 changes: 27 additions & 18 deletions authorize/client_ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package authorize
import (
"fmt"
"log"
"math"
"math/rand"
"net/http"
"reflect"
"regexp"
Expand All @@ -15,7 +17,7 @@ import (
type SDKInterfaceFunc func() (any, *http.Response, error)

var (
maxRetries = 5
maxRetries = 10
maximumRetryAfterBackoff = 30
)

Expand All @@ -26,10 +28,10 @@ func processResponse(f SDKInterfaceFunc, targetObject any) (*http.Response, erro
if targetObject != nil {
v := reflect.ValueOf(targetObject)
if v.Kind() != reflect.Ptr {
return nil, fmt.Errorf("Target object must be a pointer. This is always a problem with the provider, please raise an issue with the provider maintainers.")
return nil, fmt.Errorf("Target object must be a pointer. This is always a problem with the SDK, please raise an issue with the SDK maintainers.")
}
if !v.Elem().IsValid() {
return nil, fmt.Errorf("Target object is not valid. This is always a problem with the provider, please raise an issue with the provider maintainers.")
return nil, fmt.Errorf("Target object is not valid. This is always a problem with the SDK, please raise an issue with the SDK maintainers.")
}

if obj != nil {
Expand All @@ -53,11 +55,12 @@ func exponentialBackOffRetry(f SDKInterfaceFunc) (interface{}, *http.Response, e

for i := 0; i < maxRetries; i++ {
obj, resp, err = f()
retryAttempt := i + 1

backOffTime, isRetryable = testForRetryable(resp, err, backOffTime)
backOffTime, isRetryable = testForRetryable(resp, err, retryAttempt)

if isRetryable {
log.Printf("Attempt %d failed: %v, backing off by %s.", i+1, err, backOffTime.String())
log.Printf("Attempt %d failed: %v, backing off by %s.", retryAttempt, err, backOffTime.String())
time.Sleep(backOffTime)
continue
}
Expand All @@ -70,28 +73,29 @@ func exponentialBackOffRetry(f SDKInterfaceFunc) (interface{}, *http.Response, e
return obj, resp, err // output the final error
}

func testForRetryable(r *http.Response, err error, currentBackoff time.Duration) (time.Duration, bool) {
func testForRetryable(r *http.Response, err error, retryAttempt int) (time.Duration, bool) {

backoff := currentBackoff
baseDelay := time.Second
requestDelayDuration := calculateExponentialBackoff(retryAttempt, baseDelay)

if r != nil {
if r.StatusCode == 501 || r.StatusCode == 503 || r.StatusCode == 429 {
retryAfter, err := parseRetryAfterHeader(r)
if err != nil {
log.Printf("Cannot parse the expected \"Retry-After\" header: %s", err)
backoff = currentBackoff * 2
}

if retryAfter <= time.Duration(maximumRetryAfterBackoff) {
backoff += time.Duration(maximumRetryAfterBackoff)
} else {
backoff += retryAfter
if err != nil {
if retryAfter <= time.Duration(maximumRetryAfterBackoff) {
requestDelayDuration += time.Duration(maximumRetryAfterBackoff)
} else {
requestDelayDuration += retryAfter
}
}
} else {
backoff = currentBackoff * 2
}

retryAbleCodes := []int{
408,
429,
500,
501,
Expand All @@ -102,7 +106,7 @@ func testForRetryable(r *http.Response, err error, currentBackoff time.Duration)

if slices.Contains(retryAbleCodes, r.StatusCode) {
log.Printf("HTTP status code %d detected, available for retry", r.StatusCode)
return backoff, true
return requestDelayDuration, true
}
}

Expand All @@ -115,21 +119,21 @@ func testForRetryable(r *http.Response, err error, currentBackoff time.Duration)
// Test for unexpected errors
if strings.EqualFold(modelError.GetCode(), "UNEXPECTED_ERROR") {
log.Printf("Unexpected error detected, available for retry")
return backoff, true
return requestDelayDuration, true
}

// Test for inconsistent role state
m, _ := regexp.MatchString(`^Role assignment [a-z0-9\-]* cannot be deleted as it is read only`, modelError.GetMessage())

if m {
log.Printf("Inconsistent role assignment, available for retry")
return backoff, true
return requestDelayDuration, true
}
}
}
}

return backoff, false
return requestDelayDuration, false
}

func parseRetryAfterHeader(resp *http.Response) (time.Duration, error) {
Expand All @@ -153,3 +157,8 @@ func parseRetryAfterHeader(resp *http.Response) (time.Duration, error) {

return time.Until(retryAfterTime), nil
}

func calculateExponentialBackoff(attempt int, baseDelay time.Duration) time.Duration {
jitter := time.Duration(rand.Intn(101)) * time.Millisecond // Add random jitter
Comment thread Fixed
return baseDelay*time.Duration(math.Pow(2, float64(attempt))) + jitter
}
2 changes: 1 addition & 1 deletion authorize/configuration.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion credentials/.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.10.0
0.10.1
4 changes: 4 additions & 0 deletions credentials/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v0.10.1 (Unreleased)

* **Note** Enhance backoff retry logic for transient errors according to [best practices](https://apidocs.pingidentity.com/pingone/platform/v1/api/#retries-best-practice-for-managing-transient-api-errors).

# v0.10.0 (2024-11-15)

* **Breaking change** `(Api[a-zA-Z]Request).Execute()` and `(*Api[a-zA-Z]Request).[a-zA-Z]Execute()` API functions now returns the `EntityArrayPagedIterator` data type to for code clients to implement paging of results. [#392](https://github.com/patrickcping/pingone-go-sdk-v2/pull/392)
Expand Down
2 changes: 1 addition & 1 deletion credentials/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The PingOne Platform API covering the PingOne Credentials service
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client.

- API version: 2023-06-29
- Package version: 0.10.0
- Package version: 0.10.1
- Build package: org.openapitools.codegen.languages.GoClientCodegen

## Installation
Expand Down
45 changes: 27 additions & 18 deletions credentials/client_ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package credentials
import (
"fmt"
"log"
"math"
"math/rand"
"net/http"
"reflect"
"regexp"
Expand All @@ -15,7 +17,7 @@ import (
type SDKInterfaceFunc func() (any, *http.Response, error)

var (
maxRetries = 5
maxRetries = 10
maximumRetryAfterBackoff = 30
)

Expand All @@ -26,10 +28,10 @@ func processResponse(f SDKInterfaceFunc, targetObject any) (*http.Response, erro
if targetObject != nil {
v := reflect.ValueOf(targetObject)
if v.Kind() != reflect.Ptr {
return nil, fmt.Errorf("Target object must be a pointer. This is always a problem with the provider, please raise an issue with the provider maintainers.")
return nil, fmt.Errorf("Target object must be a pointer. This is always a problem with the SDK, please raise an issue with the SDK maintainers.")
}
if !v.Elem().IsValid() {
return nil, fmt.Errorf("Target object is not valid. This is always a problem with the provider, please raise an issue with the provider maintainers.")
return nil, fmt.Errorf("Target object is not valid. This is always a problem with the SDK, please raise an issue with the SDK maintainers.")
}

if obj != nil {
Expand All @@ -53,11 +55,12 @@ func exponentialBackOffRetry(f SDKInterfaceFunc) (interface{}, *http.Response, e

for i := 0; i < maxRetries; i++ {
obj, resp, err = f()
retryAttempt := i + 1

backOffTime, isRetryable = testForRetryable(resp, err, backOffTime)
backOffTime, isRetryable = testForRetryable(resp, err, retryAttempt)

if isRetryable {
log.Printf("Attempt %d failed: %v, backing off by %s.", i+1, err, backOffTime.String())
log.Printf("Attempt %d failed: %v, backing off by %s.", retryAttempt, err, backOffTime.String())
time.Sleep(backOffTime)
continue
}
Expand All @@ -70,28 +73,29 @@ func exponentialBackOffRetry(f SDKInterfaceFunc) (interface{}, *http.Response, e
return obj, resp, err // output the final error
}

func testForRetryable(r *http.Response, err error, currentBackoff time.Duration) (time.Duration, bool) {
func testForRetryable(r *http.Response, err error, retryAttempt int) (time.Duration, bool) {

backoff := currentBackoff
baseDelay := time.Second
requestDelayDuration := calculateExponentialBackoff(retryAttempt, baseDelay)

if r != nil {
if r.StatusCode == 501 || r.StatusCode == 503 || r.StatusCode == 429 {
retryAfter, err := parseRetryAfterHeader(r)
if err != nil {
log.Printf("Cannot parse the expected \"Retry-After\" header: %s", err)
backoff = currentBackoff * 2
}

if retryAfter <= time.Duration(maximumRetryAfterBackoff) {
backoff += time.Duration(maximumRetryAfterBackoff)
} else {
backoff += retryAfter
if err != nil {
if retryAfter <= time.Duration(maximumRetryAfterBackoff) {
requestDelayDuration += time.Duration(maximumRetryAfterBackoff)
} else {
requestDelayDuration += retryAfter
}
}
} else {
backoff = currentBackoff * 2
}

retryAbleCodes := []int{
408,
429,
500,
501,
Expand All @@ -102,7 +106,7 @@ func testForRetryable(r *http.Response, err error, currentBackoff time.Duration)

if slices.Contains(retryAbleCodes, r.StatusCode) {
log.Printf("HTTP status code %d detected, available for retry", r.StatusCode)
return backoff, true
return requestDelayDuration, true
}
}

Expand All @@ -115,21 +119,21 @@ func testForRetryable(r *http.Response, err error, currentBackoff time.Duration)
// Test for unexpected errors
if strings.EqualFold(modelError.GetCode(), "UNEXPECTED_ERROR") {
log.Printf("Unexpected error detected, available for retry")
return backoff, true
return requestDelayDuration, true
}

// Test for inconsistent role state
m, _ := regexp.MatchString(`^Role assignment [a-z0-9\-]* cannot be deleted as it is read only`, modelError.GetMessage())

if m {
log.Printf("Inconsistent role assignment, available for retry")
return backoff, true
return requestDelayDuration, true
}
}
}
}

return backoff, false
return requestDelayDuration, false
}

func parseRetryAfterHeader(resp *http.Response) (time.Duration, error) {
Expand All @@ -153,3 +157,8 @@ func parseRetryAfterHeader(resp *http.Response) (time.Duration, error) {

return time.Until(retryAfterTime), nil
}

func calculateExponentialBackoff(attempt int, baseDelay time.Duration) time.Duration {
jitter := time.Duration(rand.Intn(101)) * time.Millisecond // Add random jitter
Comment thread Fixed
return baseDelay*time.Duration(math.Pow(2, float64(attempt))) + jitter
}
2 changes: 1 addition & 1 deletion credentials/configuration.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion management/.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.48.0
0.48.1
4 changes: 4 additions & 0 deletions management/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v0.48.1 (Unreleased)

* **Note** Enhance backoff retry logic for transient errors according to [best practices](https://apidocs.pingidentity.com/pingone/platform/v1/api/#retries-best-practice-for-managing-transient-api-errors).

# v0.48.0 (2025-02-05)

* **Enhancement** Added the `filter` query string parameter function to the `ReadAllCustomAdminRoles(..)` API request model. [#414](https://github.com/patrickcping/pingone-go-sdk-v2/pull/414)
Expand Down
2 changes: 1 addition & 1 deletion management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The PingOne Platform API covering the base and SSO services (otherwise known as
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client.

- API version: 2023-06-29
- Package version: 0.48.0
- Package version: 0.48.1
- Build package: org.openapitools.codegen.languages.GoClientCodegen

## Installation
Expand Down
Loading