Skip to content

Commit f1d9552

Browse files
authored
Update the RetryPolicy and ShouldRetry customization logic to allow loosening the retry condition. (Azure#5656)
* Update the RetryPolicy and ShouldRetry customization logic to allow loosen the retry condition. * Add CL entry and update doc comment. * Move WasLastAttempt check out, and add more tests. * Address PR feedback, remove use of void and unused params.
1 parent b7a751d commit f1d9552

4 files changed

Lines changed: 184 additions & 31 deletions

File tree

sdk/core/azure-core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
### Other Changes
1414

1515
- [[#5622]](https://github.com/Azure/azure-sdk-for-cpp/pull/5622) Documentation fix for building the SDK with specific OpenSSL version. (A community contribution, courtesy of _[ByteYue](https://github.com/ByteYue)_)
16+
- [[#5515]](https://github.com/Azure/azure-sdk-for-cpp/issues/5515) Add a `ShouldRetry` virtual method to the retry policy to enable customization of service-specific retry logic.
1617

1718
### Acknowledgments
1819

sdk/core/azure-core/inc/azure/core/http/policies/policy.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ namespace Azure { namespace Core { namespace Http { namespace Policies {
420420
* request. Custom implementations of this method that override the retry behavior, should
421421
* handle that error case, if that needs to be customized.
422422
*
423-
* @remark Unless overriden, the default implementation is to always return true. The
423+
* @remark Unless overriden, the default implementation is to always return `false`. The
424424
* non-retriable errors, including those specified in the RetryOptions, remain evaluated
425425
* before calling ShouldRetry.
426426
*

sdk/core/azure-core/src/http/retry_policy.cpp

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ int32_t RetryPolicy::GetRetryCount(Context const& context)
120120

121121
bool RetryPolicy::ShouldRetry(std::unique_ptr<RawResponse> const&, RetryOptions const&) const
122122
{
123-
return true;
123+
return false;
124124
}
125125

126126
std::unique_ptr<RawResponse> RetryPolicy::Send(
@@ -145,19 +145,27 @@ std::unique_ptr<RawResponse> RetryPolicy::Send(
145145
{
146146
auto response = nextPolicy.Send(request, retryContext);
147147

148-
// If we are out of retry attempts, if a response is non-retriable (or simply 200 OK, i.e
149-
// doesn't need to be retried), then ShouldRetryOnResponse returns false.
150-
if (!ShouldRetryOnResponse(*response.get(), m_retryOptions, attempt, retryAfter))
148+
// Are we out of retry attempts?
149+
// Checking this first, before checking the response so that the extension point of
150+
// ShouldRetry doesn't have the responsibility of checking the number of retries (again).
151+
if (WasLastAttempt(m_retryOptions, attempt))
151152
{
152-
// If this is the second attempt and StartTry was called, we need to stop it. Otherwise
153-
// trying to perform same request would use last retry query/headers
154153
return response;
155154
}
156155

157-
// Service SDKs can inject custom logic to define whether the request should be retried,
158-
// based on the response. The default is true.
159-
if (!ShouldRetry(response, m_retryOptions))
156+
// If a response is non-retriable (or simply 200 OK, i.e doesn't need to be retried), then
157+
// ShouldRetryOnResponse returns false. Service SDKs can inject custom logic to define whether
158+
// the request should be retried, based on the response. The default of `ShouldRetry` is
159+
// false.
160+
// Because of boolean short-circuit evaluation, if ShouldRetryOnResponse returns true, the
161+
// overriden ShouldRetry is not called. This is expected, since overriding ShouldRetry enables
162+
// loosening the retry conditions (retrying where otherwise the request wouldn't be), but not
163+
// strengthening it.
164+
if (!ShouldRetryOnResponse(*response.get(), m_retryOptions, attempt, retryAfter)
165+
&& !ShouldRetry(response, m_retryOptions))
160166
{
167+
// If this is the second attempt and StartTry was called, we need to stop it. Otherwise
168+
// trying to perform same request would use last retry query/headers
161169
return response;
162170
}
163171
}
@@ -227,12 +235,6 @@ bool RetryPolicy::ShouldRetryOnResponse(
227235
using Azure::Core::Diagnostics::Logger;
228236
using Azure::Core::Diagnostics::_internal::Log;
229237

230-
// Are we out of retry attempts?
231-
if (WasLastAttempt(retryOptions, attempt))
232-
{
233-
return false;
234-
}
235-
236238
// Should we retry on the given response retry code?
237239
{
238240
auto const& statusCodes = retryOptions.StatusCodes;

sdk/core/azure-core/test/ut/retry_policy_test.cpp

Lines changed: 165 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,21 +125,91 @@ class RetryPolicyTest final : public RetryPolicy {
125125
}
126126
};
127127

128-
class RetryPolicyTest_CustomRetry_False final : public RetryPolicy {
128+
class RetryPolicyTest_CustomRetry_True final : public RetryPolicy {
129129
public:
130-
RetryPolicyTest_CustomRetry_False(RetryOptions const& retryOptions) : RetryPolicy(retryOptions) {}
130+
RetryPolicyTest_CustomRetry_True(RetryOptions const& retryOptions) : RetryPolicy(retryOptions) {}
131131

132132
std::unique_ptr<HttpPolicy> Clone() const override
133133
{
134-
return std::make_unique<RetryPolicyTest_CustomRetry_False>(*this);
134+
return std::make_unique<RetryPolicyTest_CustomRetry_True>(*this);
135135
}
136136

137137
protected:
138-
bool ShouldRetry(std::unique_ptr<RawResponse> const& response, RetryOptions const& retryOptions)
139-
const override
138+
bool ShouldRetry(std::unique_ptr<RawResponse> const&, RetryOptions const&) const override
140139
{
141-
(void)response;
142-
(void)retryOptions;
140+
return true;
141+
}
142+
};
143+
144+
class RetryPolicyTest_CustomRetry_Throw final : public RetryPolicy {
145+
public:
146+
RetryPolicyTest_CustomRetry_Throw(RetryOptions const& retryOptions) : RetryPolicy(retryOptions) {}
147+
148+
std::unique_ptr<HttpPolicy> Clone() const override
149+
{
150+
return std::make_unique<RetryPolicyTest_CustomRetry_Throw>(*this);
151+
}
152+
153+
protected:
154+
bool ShouldRetry(std::unique_ptr<RawResponse> const&, RetryOptions const&) const override
155+
{
156+
throw TransportException("Short-circuit evaluation means this should never be called.");
157+
}
158+
};
159+
160+
class RetryPolicyTest_CustomRetry_CopySource final : public RetryPolicy {
161+
public:
162+
RetryPolicyTest_CustomRetry_CopySource(RetryOptions const& retryOptions)
163+
: RetryPolicy(retryOptions)
164+
{
165+
}
166+
167+
std::unique_ptr<HttpPolicy> Clone() const override
168+
{
169+
return std::make_unique<RetryPolicyTest_CustomRetry_CopySource>(*this);
170+
}
171+
172+
protected:
173+
bool ShouldRetry(std::unique_ptr<RawResponse> const& response, RetryOptions const&) const override
174+
{
175+
if (response == nullptr)
176+
{
177+
return true;
178+
}
179+
180+
if (static_cast<std::underlying_type_t<Azure::Core::Http::HttpStatusCode>>(
181+
response->GetStatusCode())
182+
>= 400)
183+
{
184+
const auto& headers = response->GetHeaders();
185+
auto ite = headers.find("x-ms-error-code");
186+
if (ite != headers.end())
187+
{
188+
const std::string error = ite->second;
189+
190+
if (error == "InternalError" || error == "OperationTimedOut" || error == "ServerBusy")
191+
{
192+
return true;
193+
}
194+
}
195+
}
196+
197+
if (static_cast<std::underlying_type_t<Azure::Core::Http::HttpStatusCode>>(
198+
response->GetStatusCode())
199+
>= 400)
200+
{
201+
const auto& headers = response->GetHeaders();
202+
auto ite = headers.find("x-ms-copy-source-error-code");
203+
if (ite != headers.end())
204+
{
205+
const std::string error = ite->second;
206+
207+
if (error == "InternalError" || error == "OperationTimedOut" || error == "ServerBusy")
208+
{
209+
return true;
210+
}
211+
}
212+
}
143213
return false;
144214
}
145215
};
@@ -150,10 +220,11 @@ TEST(RetryPolicy, ShouldRetry)
150220
using namespace std::chrono_literals;
151221
RetryOptions const retryOptions{3, 10ms, 60s, {HttpStatusCode::Ok}};
152222

153-
// The default ShouldRetry implementation is to always return true,
154-
// which means we will retry up to the retry count in the options.
223+
// The default ShouldRetry implementation is to always return false,
224+
// which means we will retry up to the retry count in the options, unless the status code isn't
225+
// one that is retriable.
155226
{
156-
Azure::Core::Context context = Azure::Core::Context::ApplicationContext;
227+
Azure::Core::Context context = Azure::Core::Context();
157228
auto policy = RetryPolicy(retryOptions);
158229

159230
RawResponse const* responsePtrSent = nullptr;
@@ -163,7 +234,6 @@ TEST(RetryPolicy, ShouldRetry)
163234
policies.emplace_back(std::make_unique<RetryPolicy>(policy));
164235
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
165236
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Ok, "Test");
166-
167237
responsePtrSent = response.get();
168238
retryCounter++;
169239
return response;
@@ -178,20 +248,46 @@ TEST(RetryPolicy, ShouldRetry)
178248
EXPECT_EQ(retryCounter, 3);
179249
}
180250

181-
// Overriding ShouldRetry to return false will mean we won't retry.
251+
// If the status code is retriable based on the options, ShouldRetry doesn't get called.
252+
// Testing short-circuit evaluation
182253
{
183254
Azure::Core::Context context = Azure::Core::Context();
184-
auto policy = RetryPolicyTest_CustomRetry_False(retryOptions);
255+
auto policy = RetryPolicyTest_CustomRetry_Throw(retryOptions);
185256

186257
RawResponse const* responsePtrSent = nullptr;
187258
int32_t retryCounter = -1;
188259

189260
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> policies;
190-
policies.emplace_back(std::make_unique<RetryPolicyTest_CustomRetry_False>(policy));
191-
261+
policies.emplace_back(std::make_unique<RetryPolicyTest_CustomRetry_Throw>(policy));
192262
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
193263
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Ok, "Test");
264+
responsePtrSent = response.get();
265+
retryCounter++;
266+
return response;
267+
}));
268+
269+
Azure::Core::Http::_internal::HttpPipeline pipeline(policies);
270+
271+
Request request(HttpMethod::Get, Azure::Core::Url("https://www.microsoft.com"));
272+
pipeline.Send(request, context);
273+
274+
EXPECT_NE(responsePtrSent, nullptr);
275+
EXPECT_EQ(retryCounter, 3);
276+
}
194277

278+
// If the status code isn't retriable based on the options, only then does ShouldRetry get called.
279+
// Since the default of ShouldRetry is false, we don't expect any retries.
280+
{
281+
Azure::Core::Context context = Azure::Core::Context();
282+
auto policy = RetryPolicy(retryOptions);
283+
284+
RawResponse const* responsePtrSent = nullptr;
285+
int32_t retryCounter = -1;
286+
287+
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> policies;
288+
policies.emplace_back(std::make_unique<RetryPolicy>(policy));
289+
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
290+
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Accepted, "Test");
195291
responsePtrSent = response.get();
196292
retryCounter++;
197293
return response;
@@ -205,6 +301,60 @@ TEST(RetryPolicy, ShouldRetry)
205301
EXPECT_NE(responsePtrSent, nullptr);
206302
EXPECT_EQ(retryCounter, 0);
207303
}
304+
305+
// Overriding ShouldRetry to return true will mean we will retry, when the status code isn't
306+
// retriable based on the options.
307+
{
308+
Azure::Core::Context context = Azure::Core::Context();
309+
auto policy = RetryPolicyTest_CustomRetry_True(retryOptions);
310+
311+
RawResponse const* responsePtrSent = nullptr;
312+
int32_t retryCounter = -1;
313+
314+
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> policies;
315+
policies.emplace_back(std::make_unique<RetryPolicyTest_CustomRetry_True>(policy));
316+
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
317+
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Accepted, "Test");
318+
responsePtrSent = response.get();
319+
retryCounter++;
320+
return response;
321+
}));
322+
323+
Azure::Core::Http::_internal::HttpPipeline pipeline(policies);
324+
325+
Request request(HttpMethod::Get, Azure::Core::Url("https://www.microsoft.com"));
326+
pipeline.Send(request, context);
327+
328+
EXPECT_NE(responsePtrSent, nullptr);
329+
EXPECT_EQ(retryCounter, 3);
330+
}
331+
332+
// Copy Status Code scenario (non-retriable HTTP status code)
333+
{
334+
Azure::Core::Context context = Azure::Core::Context();
335+
auto policy = RetryPolicyTest_CustomRetry_CopySource(RetryOptions());
336+
337+
RawResponse const* responsePtrSent = nullptr;
338+
int32_t retryCounter = -1;
339+
340+
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> policies;
341+
policies.emplace_back(std::make_unique<RetryPolicyTest_CustomRetry_CopySource>(policy));
342+
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
343+
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Conflict, "Test");
344+
response->SetHeader("x-ms-copy-source-error-code", "OperationTimedOut");
345+
responsePtrSent = response.get();
346+
retryCounter++;
347+
return response;
348+
}));
349+
350+
Azure::Core::Http::_internal::HttpPipeline pipeline(policies);
351+
352+
Request request(HttpMethod::Get, Azure::Core::Url("https://www.microsoft.com"));
353+
pipeline.Send(request, context);
354+
355+
EXPECT_NE(responsePtrSent, nullptr);
356+
EXPECT_EQ(retryCounter, 3);
357+
}
208358
}
209359

210360
TEST(RetryPolicy, ShouldRetryOnResponse)

0 commit comments

Comments
 (0)