Skip to content

Make configured retry backoff authoritative - #4528

Open
jaxalo wants to merge 6 commits into
jaxalo/retry-jitterfrom
jaxalo/retry-authoritative-backoff
Open

Make configured retry backoff authoritative#4528
jaxalo wants to merge 6 commits into
jaxalo/retry-jitterfrom
jaxalo/retry-authoritative-backoff

Conversation

@jaxalo

@jaxalo jaxalo commented Sep 1, 2026

Copy link
Copy Markdown

What does this PR do?

This stacked PR builds on #4517 and makes configured retry backoff authoritative.

Notable changes

  • Previously, a valid X-Ratelimit-Reset value completely replaced configured backoff for a 429 response, even when it requested a shorter delay. This PR uses max(configured exponential backoff, X-Ratelimit-Reset), so the server may extend but never shorten the configured delay.
  • Previously, HTTPClient.Timeout could shorten configured retry delays even though it is the timeout for an individual HTTP request. It no longer caps backoff.

Small changes

  • Previously, negative reset values were accepted as negative durations and could cause an immediate retry. Values that fit in int64 seconds could also overflow when converted to time.Duration. Both now fall back to configured backoff.
  • Previously, a pre-retry delay that could not fit in the remaining HTTPRetryTimeout budget waited until the deadline expired. Because the previous request has already returned, waiting is useless when the delay alone would exhaust the remaining budget; the client now returns that response immediately without starting another attempt.
  • Discarded intermediate retry response bodies are now closed before the next attempt.
  • The retry limit check is defensively enforced with retryCount >= MaxRetries; five retries still allow six total attempts.

Preserved behavior

  • Missing or malformed reset headers fall back to configured backoff.
  • 5xx responses use configured backoff and ignore rate-limit reset headers.
  • An HTTP retry that has already started may finish after the retry deadline.
  • Standard Retry-After handling and transport-error retries remain out of scope.

Jitter is introduced by the stacked PR #4517. This PR applies it after selecting the authoritative delay.

Additional Notes

Validated with:

  • go test ./api/datadog
  • go test ./api/datadog -run "^TestRetryJitter$" -count=20
  • go test ./api/datadog -run "^TestRetryDeadlineAllowsInFlightAttemptToFinish$" -count=10
  • cd tests && go test ./api
  • generated client/template parity check
  • git diff --check

Review checklist

  • This PR does not modify cassette-based tests.
  • This PR does not rely on API client schema changes.
    • The CI should be fully passing.

@jaxalo
jaxalo requested review from a team as code owners September 1, 2026 06:40
Comment on lines -250 to -252
// retry duration shouldn't exceed the configured timeout period (skip cap when Timeout==0, which means no timeout)
if c.Cfg.HTTPClient.Timeout > 0 {
retryVal = math.Min(float64(c.Cfg.HTTPClient.Timeout/time.Second), retryVal)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one was a bit false in the implem because it was capped by "c.Cfg.HTTPClient.Timeout" which is the timeout of the httpRequest not the "configured timeout period"

I replaced it with

if deadline, ok := ctx.Deadline(); ok && *retryDuration >= time.Until(deadline) {
return resp, requestErr
}

Comment thread api/datadog/client.go
retryDuration := time.Duration(retryVal) * time.Second

if isRateLimited {
retryDuration = max(retryDuration, parseRateLimitReset(response.Header.Get(rateLimitResetHeader)))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one was a bit false in the implem because it was capped by "c.Cfg.HTTPClient.Timeout" which is the timeout of the httpRequest not the "configured timeout period"

I replaced it with

if deadline, ok := ctx.Deadline(); ok && *retryDuration >= time.Until(deadline) {
return resp, requestErr
}

Comment thread api/datadog/client.go
Comment on lines -248 to -253
if err != nil || response.StatusCode == 429 || response.StatusCode >= 500 {
// Calculate the retry val (base * multiplier^retryCount)
retryVal := c.Cfg.RetryConfiguration.BackOffBase * math.Pow(c.Cfg.RetryConfiguration.BackOffMultiplier, float64(retryCount))
// retry duration shouldn't exceed the configured timeout period (skip cap when Timeout==0, which means no timeout)
if c.Cfg.HTTPClient.Timeout > 0 {
retryVal = math.Min(float64(c.Cfg.HTTPClient.Timeout/time.Second), retryVal)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I inlined the expression, because the triple nested if was too convoluted to my taste

@jaxalo jaxalo added the changelog/Changed Changed features results into a major version bump label Sep 1, 2026
Comment thread api/datadog/client.go
Comment on lines +226 to +228
if resp.Body != nil {
resp.Body.Close()
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

freing up ressource

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/Changed Changed features results into a major version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant