Skip to content

Commit

Permalink
Update the RetryPolicy and ShouldRetry customization logic to allow l…
Browse files Browse the repository at this point in the history
…oosening 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.
  • Loading branch information
ahsonkhan authored May 30, 2024
1 parent b7a751d commit f1d9552
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 31 deletions.
1 change: 1 addition & 0 deletions sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### Other Changes

- [[#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)_)
- [[#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.

### Acknowledgments

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ namespace Azure { namespace Core { namespace Http { namespace Policies {
* request. Custom implementations of this method that override the retry behavior, should
* handle that error case, if that needs to be customized.
*
* @remark Unless overriden, the default implementation is to always return true. The
* @remark Unless overriden, the default implementation is to always return `false`. The
* non-retriable errors, including those specified in the RetryOptions, remain evaluated
* before calling ShouldRetry.
*
Expand Down
32 changes: 17 additions & 15 deletions sdk/core/azure-core/src/http/retry_policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ int32_t RetryPolicy::GetRetryCount(Context const& context)

bool RetryPolicy::ShouldRetry(std::unique_ptr<RawResponse> const&, RetryOptions const&) const
{
return true;
return false;
}

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

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

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

// Are we out of retry attempts?
if (WasLastAttempt(retryOptions, attempt))
{
return false;
}

// Should we retry on the given response retry code?
{
auto const& statusCodes = retryOptions.StatusCodes;
Expand Down
180 changes: 165 additions & 15 deletions sdk/core/azure-core/test/ut/retry_policy_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,91 @@ class RetryPolicyTest final : public RetryPolicy {
}
};

class RetryPolicyTest_CustomRetry_False final : public RetryPolicy {
class RetryPolicyTest_CustomRetry_True final : public RetryPolicy {
public:
RetryPolicyTest_CustomRetry_False(RetryOptions const& retryOptions) : RetryPolicy(retryOptions) {}
RetryPolicyTest_CustomRetry_True(RetryOptions const& retryOptions) : RetryPolicy(retryOptions) {}

std::unique_ptr<HttpPolicy> Clone() const override
{
return std::make_unique<RetryPolicyTest_CustomRetry_False>(*this);
return std::make_unique<RetryPolicyTest_CustomRetry_True>(*this);
}

protected:
bool ShouldRetry(std::unique_ptr<RawResponse> const& response, RetryOptions const& retryOptions)
const override
bool ShouldRetry(std::unique_ptr<RawResponse> const&, RetryOptions const&) const override
{
(void)response;
(void)retryOptions;
return true;
}
};

class RetryPolicyTest_CustomRetry_Throw final : public RetryPolicy {
public:
RetryPolicyTest_CustomRetry_Throw(RetryOptions const& retryOptions) : RetryPolicy(retryOptions) {}

std::unique_ptr<HttpPolicy> Clone() const override
{
return std::make_unique<RetryPolicyTest_CustomRetry_Throw>(*this);
}

protected:
bool ShouldRetry(std::unique_ptr<RawResponse> const&, RetryOptions const&) const override
{
throw TransportException("Short-circuit evaluation means this should never be called.");
}
};

class RetryPolicyTest_CustomRetry_CopySource final : public RetryPolicy {
public:
RetryPolicyTest_CustomRetry_CopySource(RetryOptions const& retryOptions)
: RetryPolicy(retryOptions)
{
}

std::unique_ptr<HttpPolicy> Clone() const override
{
return std::make_unique<RetryPolicyTest_CustomRetry_CopySource>(*this);
}

protected:
bool ShouldRetry(std::unique_ptr<RawResponse> const& response, RetryOptions const&) const override
{
if (response == nullptr)
{
return true;
}

if (static_cast<std::underlying_type_t<Azure::Core::Http::HttpStatusCode>>(
response->GetStatusCode())
>= 400)
{
const auto& headers = response->GetHeaders();
auto ite = headers.find("x-ms-error-code");
if (ite != headers.end())
{
const std::string error = ite->second;

if (error == "InternalError" || error == "OperationTimedOut" || error == "ServerBusy")
{
return true;
}
}
}

if (static_cast<std::underlying_type_t<Azure::Core::Http::HttpStatusCode>>(
response->GetStatusCode())
>= 400)
{
const auto& headers = response->GetHeaders();
auto ite = headers.find("x-ms-copy-source-error-code");
if (ite != headers.end())
{
const std::string error = ite->second;

if (error == "InternalError" || error == "OperationTimedOut" || error == "ServerBusy")
{
return true;
}
}
}
return false;
}
};
Expand All @@ -150,10 +220,11 @@ TEST(RetryPolicy, ShouldRetry)
using namespace std::chrono_literals;
RetryOptions const retryOptions{3, 10ms, 60s, {HttpStatusCode::Ok}};

// The default ShouldRetry implementation is to always return true,
// which means we will retry up to the retry count in the options.
// The default ShouldRetry implementation is to always return false,
// which means we will retry up to the retry count in the options, unless the status code isn't
// one that is retriable.
{
Azure::Core::Context context = Azure::Core::Context::ApplicationContext;
Azure::Core::Context context = Azure::Core::Context();
auto policy = RetryPolicy(retryOptions);

RawResponse const* responsePtrSent = nullptr;
Expand All @@ -163,7 +234,6 @@ TEST(RetryPolicy, ShouldRetry)
policies.emplace_back(std::make_unique<RetryPolicy>(policy));
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Ok, "Test");

responsePtrSent = response.get();
retryCounter++;
return response;
Expand All @@ -178,20 +248,46 @@ TEST(RetryPolicy, ShouldRetry)
EXPECT_EQ(retryCounter, 3);
}

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

RawResponse const* responsePtrSent = nullptr;
int32_t retryCounter = -1;

std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> policies;
policies.emplace_back(std::make_unique<RetryPolicyTest_CustomRetry_False>(policy));

policies.emplace_back(std::make_unique<RetryPolicyTest_CustomRetry_Throw>(policy));
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Ok, "Test");
responsePtrSent = response.get();
retryCounter++;
return response;
}));

Azure::Core::Http::_internal::HttpPipeline pipeline(policies);

Request request(HttpMethod::Get, Azure::Core::Url("https://www.microsoft.com"));
pipeline.Send(request, context);

EXPECT_NE(responsePtrSent, nullptr);
EXPECT_EQ(retryCounter, 3);
}

// If the status code isn't retriable based on the options, only then does ShouldRetry get called.
// Since the default of ShouldRetry is false, we don't expect any retries.
{
Azure::Core::Context context = Azure::Core::Context();
auto policy = RetryPolicy(retryOptions);

RawResponse const* responsePtrSent = nullptr;
int32_t retryCounter = -1;

std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> policies;
policies.emplace_back(std::make_unique<RetryPolicy>(policy));
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Accepted, "Test");
responsePtrSent = response.get();
retryCounter++;
return response;
Expand All @@ -205,6 +301,60 @@ TEST(RetryPolicy, ShouldRetry)
EXPECT_NE(responsePtrSent, nullptr);
EXPECT_EQ(retryCounter, 0);
}

// Overriding ShouldRetry to return true will mean we will retry, when the status code isn't
// retriable based on the options.
{
Azure::Core::Context context = Azure::Core::Context();
auto policy = RetryPolicyTest_CustomRetry_True(retryOptions);

RawResponse const* responsePtrSent = nullptr;
int32_t retryCounter = -1;

std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> policies;
policies.emplace_back(std::make_unique<RetryPolicyTest_CustomRetry_True>(policy));
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Accepted, "Test");
responsePtrSent = response.get();
retryCounter++;
return response;
}));

Azure::Core::Http::_internal::HttpPipeline pipeline(policies);

Request request(HttpMethod::Get, Azure::Core::Url("https://www.microsoft.com"));
pipeline.Send(request, context);

EXPECT_NE(responsePtrSent, nullptr);
EXPECT_EQ(retryCounter, 3);
}

// Copy Status Code scenario (non-retriable HTTP status code)
{
Azure::Core::Context context = Azure::Core::Context();
auto policy = RetryPolicyTest_CustomRetry_CopySource(RetryOptions());

RawResponse const* responsePtrSent = nullptr;
int32_t retryCounter = -1;

std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> policies;
policies.emplace_back(std::make_unique<RetryPolicyTest_CustomRetry_CopySource>(policy));
policies.emplace_back(std::make_unique<TestTransportPolicy>([&]() {
auto response = std::make_unique<RawResponse>(1, 1, HttpStatusCode::Conflict, "Test");
response->SetHeader("x-ms-copy-source-error-code", "OperationTimedOut");
responsePtrSent = response.get();
retryCounter++;
return response;
}));

Azure::Core::Http::_internal::HttpPipeline pipeline(policies);

Request request(HttpMethod::Get, Azure::Core::Url("https://www.microsoft.com"));
pipeline.Send(request, context);

EXPECT_NE(responsePtrSent, nullptr);
EXPECT_EQ(retryCounter, 3);
}
}

TEST(RetryPolicy, ShouldRetryOnResponse)
Expand Down

0 comments on commit f1d9552

Please sign in to comment.