From 2154485a4c8d9498efa28104672b2ceb73abd9cb Mon Sep 17 00:00:00 2001 From: Andrey Kashcheev Date: Mon, 21 Sep 2026 13:27:52 +0200 Subject: [PATCH] Add support for Retry-After header Optional additional waiting time between retry attempts. Only version with seconds is supported Relates-To: DATASDK-107 Signed-off-by: Andrey Kashcheev --- .../include/olp/core/client/RetrySettings.h | 7 +- olp-cpp-sdk-core/src/client/OlpClient.cpp | 38 +++++- .../tests/client/OlpClientTest.cpp | 110 ++++++++++++++++++ 3 files changed, 148 insertions(+), 7 deletions(-) diff --git a/olp-cpp-sdk-core/include/olp/core/client/RetrySettings.h b/olp-cpp-sdk-core/include/olp/core/client/RetrySettings.h index 42748abb3..2fd6c5af2 100644 --- a/olp-cpp-sdk-core/include/olp/core/client/RetrySettings.h +++ b/olp-cpp-sdk-core/include/olp/core/client/RetrySettings.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2024 HERE Europe B.V. + * Copyright (C) 2021-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,8 +71,9 @@ struct CORE_API RetrySettings { * * The default value is 60. * - * @note Connection or data transfer will be interrupted after specified - * period of time ignoring `connection_timeout` and `transfer_timeout` values. + * @note Connection, data transfer, or retry attempts will be interrupted + * after specified period of time ignoring `connection_timeout` and + * `transfer_timeout` values. */ int timeout = 60; diff --git a/olp-cpp-sdk-core/src/client/OlpClient.cpp b/olp-cpp-sdk-core/src/client/OlpClient.cpp index f53a61691..c6fd22b1b 100644 --- a/olp-cpp-sdk-core/src/client/OlpClient.cpp +++ b/olp-cpp-sdk-core/src/client/OlpClient.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #ifdef OLP_SDK_NETWORK_IOS_BACKGROUND_DOWNLOAD #include @@ -182,6 +183,32 @@ bool CaseInsensitiveCompare(const std::string& str1, const std::string& str2) { }); } +std::chrono::seconds GetRetryAfterWaitTime(const HttpResponse& response) { + for (const auto& header : response.GetHeaders()) { + if (!CaseInsensitiveCompare(header.first, "Retry-After")) { + continue; + } + + if (header.second.empty()) { + return std::chrono::seconds::zero(); + } + + using Rep = std::chrono::seconds::rep; + const Rep seconds_from_header = + std::strtol(header.second.c_str(), nullptr, 10); + + if (seconds_from_header == std::numeric_limits::max() || + seconds_from_header == std::numeric_limits::min()) { + return std::chrono::seconds::zero(); + } + + // In case of other issues with conversion - return zero seconds + return std::chrono::seconds(seconds_from_header); + } + + return std::chrono::seconds::zero(); +} + RequestSettingsPtr GetRequestSettings(const RetrySettings& retry_settings) { return std::make_shared( retry_settings.initial_backdown_period, retry_settings.timeout); @@ -286,9 +313,10 @@ NetworkCallbackType GetRetryCallback( // TODO: Do not block thread but instead implement an event queue that will // trigger next retry once the time expired! - const auto actual_wait_time = - std::min(settings->current_backdown_period, - settings->max_wait_time - settings->accumulated_wait_time); + auto wait_time = settings->current_backdown_period; + wait_time += GetRetryAfterWaitTime(response); + const auto actual_wait_time = std::min( + wait_time, settings->max_wait_time - settings->accumulated_wait_time); std::this_thread::sleep_for(actual_wait_time); settings->accumulated_wait_time += actual_wait_time; @@ -843,8 +871,10 @@ HttpResponse OlpClientImpl::CallApiImpl( } // do the periodical sleep and check for cancellation status in between. + auto wait_time = backdown_period; + wait_time += GetRetryAfterWaitTime(response); auto duration_to_sleep = - std::min(backdown_period, max_wait_time - accumulated_wait_time); + std::min(wait_time, max_wait_time - accumulated_wait_time); accumulated_wait_time += duration_to_sleep; while (duration_to_sleep.count() > 0 && !context.IsCancelled()) { diff --git a/olp-cpp-sdk-core/tests/client/OlpClientTest.cpp b/olp-cpp-sdk-core/tests/client/OlpClientTest.cpp index 3b4c7cae7..04e699d82 100644 --- a/olp-cpp-sdk-core/tests/client/OlpClientTest.cpp +++ b/olp-cpp-sdk-core/tests/client/OlpClientTest.cpp @@ -451,6 +451,116 @@ TEST_P(OlpClientTest, RetryWithExponentialBackdownStrategy) { testing::Mock::VerifyAndClearExpectations(network.get()); } +TEST_P(OlpClientTest, RetryAfterHeaderDelaysRetry) { + constexpr auto kRetryAfterDelay = std::chrono::seconds(1); + auto network = network_; + auto& retry_settings = client_settings_.retry_settings; + retry_settings.max_attempts = 1; + retry_settings.initial_backdown_period = 0; + retry_settings.backdown_strategy = [](std::chrono::milliseconds, size_t) { + return std::chrono::milliseconds::zero(); + }; + + olp::client::OlpClient client(client_settings_, kEmptyBaseUrl); + std::vector timestamps; + std::vector> futures; + olp::http::RequestId request_id = 5; + int attempt = 0; + + EXPECT_CALL(*network, Send(_, _, _, _, _)) + .Times(2) + .WillRepeatedly([&](olp::http::NetworkRequest /*request*/, + olp::http::Network::Payload /*payload*/, + olp::http::Network::Callback callback, + olp::http::Network::HeaderCallback header_callback, + olp::http::Network::DataCallback /*data_callback*/) { + timestamps.push_back(std::chrono::steady_clock::now()); + const auto current_request_id = request_id++; + const bool retryable_response = attempt++ == 0; + futures.emplace_back(std::async(std::launch::async, [=]() { + std::this_thread::sleep_for(kCallbackSleepTime); + if (retryable_response) { + header_callback("retry-after", "1"); + } else { + header_callback("not-retry-after", "0"); + } + callback(olp::http::NetworkResponse() + .WithStatus(retryable_response + ? http::HttpStatusCode::TOO_MANY_REQUESTS + : http::HttpStatusCode::OK) + .WithRequestId(current_request_id)); + })); + return olp::http::SendOutcome(current_request_id); + }); + + auto call_wrapper = MakeCallWrapper(client); + const auto response = + call_wrapper->CallApi({}, "GET", {}, {}, {}, nullptr, {}); + + for (auto& future : futures) { + future.wait(); + } + + ASSERT_EQ(http::HttpStatusCode::OK, response.GetStatus()); + ASSERT_EQ(2u, timestamps.size()); + EXPECT_GE(timestamps[1] - timestamps[0], kRetryAfterDelay); + testing::Mock::VerifyAndClearExpectations(network.get()); +} + +TEST_P(OlpClientTest, InvalidRetryAfterHeaderUsesBackdownStrategy) { + constexpr auto kBackdownDelay = std::chrono::milliseconds(100); + auto network = network_; + auto& retry_settings = client_settings_.retry_settings; + retry_settings.max_attempts = 1; + retry_settings.initial_backdown_period = kBackdownDelay.count(); + retry_settings.backdown_strategy = [](std::chrono::milliseconds, size_t) { + return std::chrono::milliseconds::zero(); + }; + + olp::client::OlpClient client(client_settings_, kEmptyBaseUrl); + std::vector timestamps; + std::vector> futures; + olp::http::RequestId request_id = 5; + int attempt = 0; + + EXPECT_CALL(*network, Send(_, _, _, _, _)) + .Times(2) + .WillRepeatedly([&](olp::http::NetworkRequest /*request*/, + olp::http::Network::Payload /*payload*/, + olp::http::Network::Callback callback, + olp::http::Network::HeaderCallback header_callback, + olp::http::Network::DataCallback /*data_callback*/) { + timestamps.push_back(std::chrono::steady_clock::now()); + const auto current_request_id = request_id++; + const bool retryable_response = attempt++ == 0; + futures.emplace_back(std::async(std::launch::async, [=]() { + std::this_thread::sleep_for(kCallbackSleepTime); + if (retryable_response) { + header_callback("Retry-After", "not-a-delay"); + } + callback(olp::http::NetworkResponse() + .WithStatus(retryable_response + ? http::HttpStatusCode::TOO_MANY_REQUESTS + : http::HttpStatusCode::OK) + .WithRequestId(current_request_id)); + })); + return olp::http::SendOutcome(current_request_id); + }); + + auto call_wrapper = MakeCallWrapper(client); + const auto response = + call_wrapper->CallApi({}, "GET", {}, {}, {}, nullptr, {}); + + for (auto& future : futures) { + future.wait(); + } + + ASSERT_EQ(http::HttpStatusCode::OK, response.GetStatus()); + ASSERT_EQ(2u, timestamps.size()); + EXPECT_GE(timestamps[1] - timestamps[0], kBackdownDelay); + testing::Mock::VerifyAndClearExpectations(network.get()); +} + TEST_P(OlpClientTest, RetryTimeout) { const size_t kMaxRetries = 3; const size_t kSuccessfulAttempt = kMaxRetries + 1;