@@ -288,6 +288,26 @@ class RetryEventHandler : public CustomEventHandler
288288 }
289289};
290290
291+ #ifdef ENABLE_OTLP_RETRY_PREVIEW
292+ class RetryDeadlineEventHandler : public RetryEventHandler
293+ {
294+ public:
295+ void OnEvent (http_client::SessionState state, nostd::string_view reason) noexcept override
296+ {
297+ CustomEventHandler::OnEvent (state, reason);
298+ if (state == http_client::SessionState::Response && operation_ != nullptr )
299+ {
300+ ++response_event_count_;
301+ retry_time_during_response_ = operation_->NextRetryTime ();
302+ }
303+ }
304+
305+ curl::HttpOperation *operation_{nullptr };
306+ int response_event_count_{0 };
307+ std::chrono::system_clock::time_point retry_time_during_response_{};
308+ };
309+ #endif // ENABLE_OTLP_RETRY_PREVIEW
310+
291311// curl_global_init_mem is the only way to reach the failure returns of curl_slist_append and
292312// curl_multi_init. It must be called before libcurl is initialised: afterwards it returns
293313// CURLE_OK and changes nothing. The switches are thread local, so arming one from a test cannot
@@ -662,7 +682,6 @@ class ReportedStateHandler : public CustomEventHandler
662682 std::string reason_;
663683};
664684} // namespace
665-
666685class BasicCurlHttpTests : public ::testing::Test, public HTTP_SERVER_NS ::HttpRequestCallback
667686{
668687protected:
@@ -719,6 +738,7 @@ class BasicCurlHttpTests : public ::testing::Test, public HTTP_SERVER_NS::HttpRe
719738 server_.addHandler (" /post/" , *this );
720739 server_.addHandler (" /retry/" , *this );
721740 server_.addHandler (" /retry-after/" , *this );
741+ server_.addHandler (" /retry-after-invalid/" , *this );
722742 server_.addHandler (" /close/" , *this );
723743 server_.start ();
724744 is_running_ = true ;
@@ -752,12 +772,21 @@ class BasicCurlHttpTests : public ::testing::Test, public HTTP_SERVER_NS::HttpRe
752772 response.body = " {'k1':'v1', 'k2':'v2', 'k3':'v3'}" ;
753773 response_status = 200 ;
754774 }
755- else if (request.uri == " /retry/" )
775+ else if (request.uri == " /retry/" || request.uri == " /retry-after/" ||
776+ request.uri == " /retry-after-invalid/" )
756777 {
757778 std::unique_lock<std::mutex> lk1 (mtx_requests);
758779 received_requests_.push_back (request);
759780 response.headers [" Content-Type" ] = " text/plain" ;
760- response_status = 429 ;
781+ if (request.uri == " /retry-after/" )
782+ {
783+ response.headers [" Retry-After" ] = " 2" ;
784+ }
785+ else if (request.uri == " /retry-after-invalid/" )
786+ {
787+ response.headers [" Retry-After" ] = " invalid" ;
788+ }
789+ response_status = 429 ;
761790 }
762791 else if (request.uri == " /retry-after/" )
763792 {
@@ -1160,6 +1189,89 @@ TEST_F(BasicCurlHttpTests, ExponentialBackoffRetry)
11601189 ASSERT_FALSE (operation.IsRetryable ());
11611190}
11621191
1192+ TEST_F (BasicCurlHttpTests, RetryDeadlineIsStableWithinAttempt)
1193+ {
1194+ RetryEventHandler handler;
1195+ http_client::HttpSslOptions no_ssl;
1196+ http_client::Body body;
1197+ http_client::Headers headers;
1198+ http_client::Compression compression = http_client::Compression::kNone ;
1199+ http_client::RetryPolicy retry_policy = {4 , std::chrono::duration<float >{1 .0f },
1200+ std::chrono::duration<float >{5 .0f }, 2 .0f };
1201+
1202+ curl::HttpOperation operation (http_client::Method::Post, " http://127.0.0.1:19000/retry/" , no_ssl,
1203+ &handler, headers, body, compression, false ,
1204+ curl::kDefaultHttpConnTimeout , false , false , retry_policy);
1205+
1206+ ASSERT_EQ (CURLE_OK , operation.Send ());
1207+ ASSERT_TRUE (operation.IsRetryable ());
1208+
1209+ const auto retry_time = operation.NextRetryTime ();
1210+ for (int i = 0 ; i < 8 ; ++i)
1211+ {
1212+ EXPECT_EQ (retry_time, operation.NextRetryTime ());
1213+ }
1214+ }
1215+
1216+ TEST_F (BasicCurlHttpTests, RetryDeadlineIsReadyDuringResponseEvent)
1217+ {
1218+ RetryDeadlineEventHandler handler;
1219+ http_client::HttpSslOptions no_ssl;
1220+ http_client::Body body;
1221+ http_client::Headers headers;
1222+ http_client::Compression compression = http_client::Compression::kNone ;
1223+ http_client::RetryPolicy retry_policy = {4 , std::chrono::duration<float >{1 .0f },
1224+ std::chrono::duration<float >{5 .0f }, 2 .0f };
1225+
1226+ curl::HttpOperation operation (http_client::Method::Post, " http://127.0.0.1:19000/retry-after/" ,
1227+ no_ssl, &handler, headers, body, compression, false ,
1228+ curl::kDefaultHttpConnTimeout , false , false , retry_policy);
1229+ handler.operation_ = &operation;
1230+
1231+ const auto before_send = std::chrono::system_clock::now ();
1232+ ASSERT_EQ (CURLE_OK , operation.Send ());
1233+ const auto after_send = std::chrono::system_clock::now ();
1234+
1235+ ASSERT_TRUE (operation.IsRetryable ());
1236+ ASSERT_EQ (1 , handler.response_event_count_ );
1237+ EXPECT_EQ (handler.retry_time_during_response_ , operation.NextRetryTime ());
1238+ EXPECT_GE (handler.retry_time_during_response_ .time_since_epoch ().count (),
1239+ (before_send + std::chrono::seconds{2 }).time_since_epoch ().count ());
1240+ EXPECT_LE (handler.retry_time_during_response_ .time_since_epoch ().count (),
1241+ (after_send + std::chrono::seconds{2 }).time_since_epoch ().count ());
1242+ }
1243+
1244+ TEST_F (BasicCurlHttpTests, InvalidRetryAfterUsesStableBackoff)
1245+ {
1246+ RetryDeadlineEventHandler handler;
1247+ http_client::HttpSslOptions no_ssl;
1248+ http_client::Body body;
1249+ http_client::Headers headers;
1250+ http_client::Compression compression = http_client::Compression::kNone ;
1251+ http_client::RetryPolicy retry_policy = {4 , std::chrono::duration<float >{1 .0f },
1252+ std::chrono::duration<float >{5 .0f }, 2 .0f };
1253+
1254+ curl::HttpOperation operation (
1255+ http_client::Method::Post, " http://127.0.0.1:19000/retry-after-invalid/" , no_ssl, &handler,
1256+ headers, body, compression, false , curl::kDefaultHttpConnTimeout , false , false , retry_policy);
1257+ handler.operation_ = &operation;
1258+
1259+ const auto before_send = std::chrono::system_clock::now ();
1260+ ASSERT_EQ (CURLE_OK , operation.Send ());
1261+ const auto after_send = std::chrono::system_clock::now ();
1262+
1263+ ASSERT_TRUE (operation.IsRetryable ());
1264+ ASSERT_EQ (1 , handler.response_event_count_ );
1265+ for (int i = 0 ; i < 8 ; ++i)
1266+ {
1267+ EXPECT_EQ (handler.retry_time_during_response_ , operation.NextRetryTime ());
1268+ }
1269+ EXPECT_GE (handler.retry_time_during_response_ .time_since_epoch ().count (),
1270+ (before_send + std::chrono::milliseconds{750 }).time_since_epoch ().count ());
1271+ EXPECT_LE (handler.retry_time_during_response_ .time_since_epoch ().count (),
1272+ (after_send + std::chrono::milliseconds{1250 }).time_since_epoch ().count ());
1273+ }
1274+
11631275// A Retry-After beyond max_backoff closes the session. The IO loop used to queue it anyway, where
11641276// it held back later retries and the background thread until the server's time.
11651277TEST_F (BasicCurlHttpTests, RetryAfterBeyondMaxBackoffIsNotQueued)
0 commit comments