-
Notifications
You must be signed in to change notification settings - Fork 986
Add 429 throttle retry policy #26820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
andrewmathew1
wants to merge
4
commits into
Azure:main
Choose a base branch
from
andrewmathew1:andrewmathew1/cosmos-429-retry-policy
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
3481679
azcosmos: add 429 throttle retry policy
andrewmathew1 ac9ddf0
azcosmos: fix double-retry on 429 and timer leak in throttle policy
andrewmathew1 9a1d67d
Merge pull request #3 from andrewmathew1/andrewmathew1/cosmos-429-ret…
andrewmathew1 678e50a
azcosmos: address Copilot PR review on throttle retry policy
andrewmathew1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,16 @@ | |
|
|
||
| ### Features Added | ||
|
|
||
| * Added a dedicated 429 (Too Many Requests) throttling retry policy that honors the `x-ms-retry-after-ms` response header and is configurable via `ClientOptions.ThrottlingRetryOptions` (`MaxRetryAttempts`, `MaxRetryWaitTime`). This brings parity with the throttling retry behavior in the .NET, Java, and Python Cosmos SDKs. When `ClientOptions.Retry.StatusCodes` and `ClientOptions.Retry.ShouldRetry` are both unset, 429 is no longer in the azcore retry policy's default status codes (it is now handled exclusively by the throttling retry policy); the other transient status codes (408, 500, 502, 503, 504) remain. | ||
|
|
||
| ### Breaking Changes | ||
|
|
||
| ### Bugs Fixed | ||
|
|
||
| ### Other Changes | ||
|
|
||
| * Throttling retry policy: an explicit `x-ms-retry-after-ms: 0` header is now honored as "retry immediately" instead of being treated as a missing header (which would have applied the default delay). NaN/Inf values for the header are now rejected as invalid. The request body is rewound before the 429 response body is drained so a rewind failure surfaces a usable 429 response to the caller. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we probably don't need this since these changes are also in this PR for the first time right? |
||
|
|
||
| ## 1.5.0-beta.6 (2026-05-15) | ||
|
|
||
| ### Features Added | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package azcosmos | ||
|
|
||
| import ( | ||
| "math" | ||
| "net/http" | ||
| "strconv" | ||
| "time" | ||
|
|
||
| azlog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log" | ||
| "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" | ||
| azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" | ||
| "github.com/Azure/azure-sdk-for-go/sdk/internal/log" | ||
| ) | ||
|
|
||
| const ( | ||
| defaultMaxThrottleRetryAttempts = 9 | ||
| defaultMaxThrottleRetryWaitTime = 30 * time.Second | ||
| defaultThrottleRetryDelay = 5 * time.Second | ||
| ) | ||
|
|
||
| // throttleRetryPolicy retries requests that fail with HTTP 429 (Too Many Requests). | ||
| // It honors the Cosmos-specific x-ms-retry-after-ms header to determine the | ||
| // delay between attempts and caps the number of attempts and total cumulative | ||
| // retry delay. This matches the throttling retry behavior of the other Cosmos | ||
| // SDKs (.NET, Java, Python). | ||
| type throttleRetryPolicy struct { | ||
| maxRetryAttempts int | ||
| maxRetryWaitTime time.Duration | ||
| // defaultDelay is used when a 429 response is missing the | ||
| // x-ms-retry-after-ms header. Defaults to defaultThrottleRetryDelay. | ||
| defaultDelay time.Duration | ||
| } | ||
|
|
||
| // newThrottleRetryPolicy constructs a throttleRetryPolicy. For MaxRetryAttempts, | ||
| // a positive value is used as the cap, zero falls back to the default | ||
| // (defaultMaxThrottleRetryAttempts), and a negative value disables throttling | ||
| // retries entirely. For MaxRetryWaitTime, a non-positive value falls back to | ||
| // the default (defaultMaxThrottleRetryWaitTime). | ||
| func newThrottleRetryPolicy(o *ThrottlingRetryOptions) *throttleRetryPolicy { | ||
| p := &throttleRetryPolicy{ | ||
| maxRetryAttempts: defaultMaxThrottleRetryAttempts, | ||
| maxRetryWaitTime: defaultMaxThrottleRetryWaitTime, | ||
| defaultDelay: defaultThrottleRetryDelay, | ||
| } | ||
| if o != nil { | ||
| if o.MaxRetryAttempts > 0 { | ||
| p.maxRetryAttempts = o.MaxRetryAttempts | ||
| } else if o.MaxRetryAttempts < 0 { | ||
| // negative values disable throttling retries entirely | ||
| p.maxRetryAttempts = 0 | ||
| } | ||
| if o.MaxRetryWaitTime > 0 { | ||
| p.maxRetryWaitTime = o.MaxRetryWaitTime | ||
| } | ||
| } | ||
| return p | ||
| } | ||
|
|
||
| func (p *throttleRetryPolicy) Do(req *policy.Request) (*http.Response, error) { | ||
| attemptCount := 0 | ||
| cumulativeDelay := time.Duration(0) | ||
| for { | ||
| response, err := req.Next() | ||
| // Transport / non-HTTP errors are not throttling; let other policies decide. | ||
| if err != nil || response == nil || response.StatusCode != http.StatusTooManyRequests { | ||
| return response, err | ||
| } | ||
|
|
||
| if attemptCount >= p.maxRetryAttempts { | ||
| log.Writef(azlog.EventRetryPolicy, "Cosmos throttle retry exhausted attempts (%d); returning 429 to caller", p.maxRetryAttempts) | ||
| return response, nil | ||
| } | ||
|
|
||
| delay, ok := readRetryAfterMs(response) | ||
| if !ok { | ||
| // header missing or unparseable; fall back to the default delay. | ||
| // an explicit "0" header is honored (retry immediately). | ||
| delay = p.defaultDelay | ||
| } | ||
|
|
||
| if cumulativeDelay+delay > p.maxRetryWaitTime { | ||
| log.Writef(azlog.EventRetryPolicy, "Cosmos throttle retry exceeded cumulative wait time (%s); returning 429 to caller", p.maxRetryWaitTime) | ||
| return response, nil | ||
| } | ||
|
|
||
| cumulativeDelay += delay | ||
| attemptCount++ | ||
|
|
||
| // Rewind the request body before discarding the response so that, if | ||
| // the body isn't seekable, the caller still receives a usable 429 | ||
| // response for diagnostics. | ||
| if err := req.RewindBody(); err != nil { | ||
| return response, err | ||
| } | ||
|
|
||
| // drain and close the response body so the connection can be reused | ||
| azruntime.Drain(response) | ||
|
|
||
| log.Writef(azlog.EventRetryPolicy, "Cosmos throttle retry attempt %d after %s (cumulative %s)", attemptCount, delay, cumulativeDelay) | ||
|
|
||
| timer := time.NewTimer(delay) | ||
| select { | ||
| case <-timer.C: | ||
| case <-req.Raw().Context().Done(): | ||
| timer.Stop() | ||
| return response, req.Raw().Context().Err() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // readRetryAfterMs parses the Cosmos x-ms-retry-after-ms header (milliseconds). | ||
| // Returns (delay, true) on a successful parse of a non-negative finite value | ||
| // (including an explicit "0", which means "retry immediately"). Returns | ||
| // (0, false) when the header is missing, unparseable, NaN, infinite, or | ||
| // negative so that the caller can apply a default delay only in that case. | ||
| func readRetryAfterMs(resp *http.Response) (time.Duration, bool) { | ||
| if resp == nil { | ||
| return 0, false | ||
| } | ||
| v := resp.Header.Get(cosmosHeaderRetryAfterMs) | ||
| if v == "" { | ||
| return 0, false | ||
| } | ||
| ms, err := strconv.ParseFloat(v, 64) | ||
| if err != nil || math.IsNaN(ms) || math.IsInf(ms, 0) || ms < 0 { | ||
| return 0, false | ||
| } | ||
| return time.Duration(ms * float64(time.Millisecond)), true | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.