Retry throttled calls on a provided HTTP client - #2139
nicolas-grekas wants to merge 1 commit into
Conversation
23d666d to
2239bea
Compare
|
Inlined, thanks. CI was mine too: On muting the given client, I tried it in the Symfony PR and pulled it back out. It does work, including through a decorator, so a retryable client under a and the multiplication it avoids is real, 16 attempts for one throttled request rather than 4. What stopped me is that The second line is the awkward one: the clone happens even when a retryable client consumes the option, because So it is a real trade rather than a clear win, and it is your call which side of it async-aws wants. Happy to add it if you prefer the lower attempt count; the |
|
Applied, you were right and my objection was not. I had argued the mute detaches the caller from the client it passed, because I had generalised from the last line, which is a test double. One existing test did need a fix:
|
ca5089b to
7d181b2
Compare
|
The lowest job caught a real one: The two test failures had the same root: stubbing |
…'s HTTP client (nicolas-grekas) This PR was merged into the 8.2 branch. Discussion ---------- [KeyManagement] Let the AWS factory take the application's HTTP client | Q | A | ------------- | --- | Branch? | 8.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT Follows #66177, which did this for the Azure, Google Cloud and Vault factories and left the AWS one out. async-aws talks through the client it is given as its third constructor argument, the way the SES, SQS, SNS and DynamoDb bridges already pass it: ```php return new AwsKms(new KmsClient(Configuration::create($options), null, $this->client)); ``` So a timeout, certificates and the profiler configured on the application's client reach KMS too. async-aws retries throttled calls on the client it builds itself and takes a given one as is, so passing a client means its own retry policy applies instead of `AwsRetryStrategy`. That is the same trade the four bridges above already make, and it is better fixed in async-aws than worked around in each of them: async-aws/aws#2139 moves the retry wrapper so it applies to a given client too, with the error factory and the logger of the client it belongs to. Commits ------- 1f9d089 [KeyManagement] Let the AWS factory take the application's HTTP client
| $this->logger | ||
| ); | ||
| } elseif (method_exists($httpClient, 'withOptions')) { | ||
| // withOptions() landed in symfony/http-client-contracts 2.4. |
There was a problem hiding this comment.
Which version of symfony/http-client corresponds to contracts 2.4 ? Maybe we can bump the min version of the dependency if this is only for Symfony 4.4 which is long EOL
There was a problem hiding this comment.
Answering my question: it was added in symfony/http-client 5.3
|
|
||
| // Throttled calls are worth retrying whoever built the client: a caller that provides one | ||
| // to configure a timeout, a proxy or the profiler should not lose the AWS retry policy. | ||
| if (class_exists(RetryableHttpClient::class)) { |
There was a problem hiding this comment.
For reference, this was added in symfony/http-client 5.2, so this condition could be removed if we drop support for 4.4
The retry wrapper was only applied to the client built internally, so a caller passing one to set a timeout, a proxy, certificates or to get the requests into a profiler silently lost the AWS retry policy along the way. Wrapping a given client too keeps that policy, with the error factory and the logger of the client it belongs to. A client that already retries is muted first, so its attempts do not multiply with the ones made here. Bumping symfony/http-client to 5.3 drops the class_exists() guard around RetryableHttpClient, which has been there since 5.2.
7d181b2 to
6ee753d
Compare
|
Bumped
That last row is why I kept the * @method static withOptions(array $options) Returns a new instance of the client with new default optionsso an implementation compiled against 2.x is not obliged to have it. Symfony's own clients all do from 5.3, and the check costs nothing, so it seemed better than requiring contracts http-client 5.3 already requires contracts |
AbstractApiwraps its HTTP client in aRetryableHttpClientwith anAwsRetryStrategy, but only when it builds that client itself:So a caller that passes a client to set a timeout, a proxy, certificates, or to get the requests into a profiler, silently loses the AWS retry policy. That is not obvious from the outside: the constructor argument reads like "use this transport", not "and give up retrying".
It matters because the two policies are not interchangeable. Symfony's
GenericRetryStrategymaps0,500,504,507and510toIDEMPOTENT_METHODS, which does not include POST, and every AWS API call is a POST. It also never looks at a 400 body, where AWS reports throttling. Measured on the same throttled request, all POST:AwsRetryStrategyGenericRetryStrategyThrottlingExceptionProvisionedThroughputExceededExceptionValidationExceptionThis moves the wrapper out of the
isset()branch so it applies to a given client too, keeping the error factory and the logger of the client it belongs to.Symfony's SES, SQS, SNS, DynamoDb and KMS bridges all pass their application's client, so they are all in this case today. The alternative is for each of them to rebuild the wrapper, which duplicates the policy in five places and cannot reach the per-client error factory.
Worth noting it is a behaviour change: a caller who passes a client now gets up to three extra attempts on throttling and 5xx. A caller whose client already retries gets both layers.