Skip to content

Commit 6c9b9bf

Browse files
committed
[OTLP/HTTP] Honor Retry-After header when retrying exports Fixes #4172
Signed-off-by: DCchoudhury15 <divyanshuchoudhury3@gmail.com>
1 parent 65aae9c commit 6c9b9bf

3 files changed

Lines changed: 191 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Increment the:
1515

1616
## [Unreleased]
1717

18+
* [OTLP/HTTP] Honor `Retry-After` response header when retrying exports,
19+
supporting both delay-seconds and HTTP-date formats per RFC 7231 §7.1.3.
20+
[#4172](https://github.com/open-telemetry/opentelemetry-cpp/issues/4172)
21+
1822
* [SDK] Add `TracerProvider::UpdateTracerConfigurator()` and example
1923
[#4065](https://github.com/open-telemetry/opentelemetry-cpp/issues/4065)
2024

ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ class HttpOperation
348348
const RetryPolicy retry_policy_;
349349
decltype(RetryPolicy::max_attempts) retry_attempts_;
350350
std::chrono::system_clock::time_point last_attempt_time_;
351+
std::chrono::system_clock::time_point retry_after_time_point_{};
351352

352353
// Processed response headers and body
353354
// See CURLINFO_RESPONSE_CODE, type is long

ext/src/http/client/curl/http_operation_curl.cc

Lines changed: 186 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@
88
# include <array>
99
#endif // ENABLE_OTLP_RETRY_PREVIEW
1010

11+
#include <stdint.h>
1112
#include <algorithm>
1213
#include <atomic>
14+
#include <cctype>
1315
#include <chrono>
1416
#include <cmath>
1517
#include <cstring>
18+
#include <ctime>
1619
#include <functional>
1720
#include <future>
21+
#include <iomanip>
22+
#include <limits>
1823
#include <memory>
1924
#include <random>
2025
#include <sstream>
@@ -23,6 +28,7 @@
2328
#include <utility>
2429
#include <vector>
2530

31+
#include "opentelemetry/common/string_util.h"
2632
#include "opentelemetry/ext/http/client/curl/http_client_curl.h"
2733
#include "opentelemetry/ext/http/client/curl/http_operation_curl.h"
2834
#include "opentelemetry/ext/http/client/http_client.h"
@@ -35,6 +41,157 @@
3541
# define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z))
3642
#endif
3743

44+
namespace
45+
{
46+
47+
std::time_t PortableTimegm(std::tm *tm)
48+
{
49+
int year = tm->tm_year + 1900;
50+
int month = tm->tm_mon + 1;
51+
52+
if (month <= 2)
53+
{
54+
year -= 1;
55+
month += 12;
56+
}
57+
58+
int day = tm->tm_mday;
59+
int days = 365 * year + year / 4 - year / 100 + year / 400 + 367 * month / 12 - 30 + day - 719530;
60+
61+
return static_cast<std::time_t>(days) * 86400 + tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec;
62+
}
63+
64+
bool ParseRetryAfterDelay(opentelemetry::nostd::string_view value,
65+
std::chrono::seconds &delay) noexcept
66+
{
67+
value = opentelemetry::common::StringUtil::Trim(value);
68+
69+
if (value.empty())
70+
{
71+
return false;
72+
}
73+
74+
std::chrono::seconds::rep result = 0;
75+
76+
for (const char c : value)
77+
{
78+
if (!std::isdigit(static_cast<unsigned char>(c)))
79+
{
80+
return false;
81+
}
82+
83+
auto digit = c - '0';
84+
85+
if (result > (std::numeric_limits<std::chrono::seconds::rep>::max() - digit) / 10)
86+
{
87+
return false;
88+
}
89+
90+
result = result * 10 + digit;
91+
}
92+
93+
if (result == 0)
94+
{
95+
return false;
96+
}
97+
98+
delay = std::chrono::seconds(result);
99+
return true;
100+
}
101+
102+
bool ParseRetryAfterDate(opentelemetry::nostd::string_view value,
103+
std::chrono::system_clock::time_point &date)
104+
{
105+
value = opentelemetry::common::StringUtil::Trim(value);
106+
107+
std::string str(value.data(), value.size());
108+
std::tm tm = {};
109+
std::istringstream ss(str);
110+
111+
ss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S");
112+
if (!ss.fail())
113+
{
114+
date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm));
115+
return true;
116+
}
117+
118+
ss.clear();
119+
ss.str(str);
120+
tm = {};
121+
ss >> std::get_time(&tm, "%A, %d-%b-%y %H:%M:%S");
122+
if (!ss.fail())
123+
{
124+
std::time_t now_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
125+
int current_year = 1970 + static_cast<int>(now_t / 31556952);
126+
int full_year = 1900 + tm.tm_year;
127+
if (full_year - current_year > 50)
128+
{
129+
full_year -= 100;
130+
}
131+
tm.tm_year = full_year - 1900;
132+
date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm));
133+
return true;
134+
}
135+
136+
ss.clear();
137+
ss.str(str);
138+
tm = {};
139+
ss >> std::get_time(&tm, "%a %b %d %H:%M:%S %Y");
140+
if (!ss.fail())
141+
{
142+
date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm));
143+
return true;
144+
}
145+
146+
return false;
147+
}
148+
149+
bool FindRetryAfterValue(const std::vector<uint8_t> &raw_headers, std::string &value)
150+
{
151+
if (raw_headers.empty())
152+
{
153+
return false;
154+
}
155+
156+
const char *data = reinterpret_cast<const char *>(raw_headers.data());
157+
const char *end = data + raw_headers.size();
158+
const char *line = data;
159+
160+
while (line < end)
161+
{
162+
const char *line_end = line;
163+
while (line_end < end && *line_end != '\n')
164+
{
165+
++line_end;
166+
}
167+
168+
static const char kRetryAfterHeader[] = "retry-after:";
169+
static const size_t kRetryAfterLen = sizeof(kRetryAfterHeader) - 1;
170+
171+
size_t line_len = static_cast<size_t>(line_end - line);
172+
if (line_len > kRetryAfterLen)
173+
{
174+
bool match = true;
175+
for (size_t i = 0; i < kRetryAfterLen && match; ++i)
176+
{
177+
match = (std::tolower(static_cast<unsigned char>(line[i])) == kRetryAfterHeader[i]);
178+
}
179+
180+
if (match)
181+
{
182+
value = std::string(line + kRetryAfterLen, line_end);
183+
return true;
184+
}
185+
}
186+
187+
line = (line_end < end) ? line_end + 1 : end;
188+
}
189+
190+
return false;
191+
}
192+
193+
} // namespace
194+
38195
OPENTELEMETRY_BEGIN_NAMESPACE
39196
namespace ext
40197
{
@@ -560,6 +717,11 @@ bool HttpOperation::IsRetryable()
560717

561718
std::chrono::system_clock::time_point HttpOperation::NextRetryTime()
562719
{
720+
if (retry_after_time_point_ != std::chrono::system_clock::time_point{})
721+
{
722+
return retry_after_time_point_;
723+
}
724+
563725
static std::random_device rd;
564726
static std::mt19937 gen(rd());
565727
static std::uniform_real_distribution<float> dis(0.8f, 1.2f);
@@ -1463,8 +1625,9 @@ void HttpOperation::Abort()
14631625
void HttpOperation::PerformCurlMessage(CURLcode code)
14641626
{
14651627
++retry_attempts_;
1466-
last_attempt_time_ = std::chrono::system_clock::now();
1467-
last_curl_result_ = code;
1628+
last_attempt_time_ = std::chrono::system_clock::now();
1629+
last_curl_result_ = code;
1630+
retry_after_time_point_ = std::chrono::system_clock::time_point{};
14681631

14691632
if (code != CURLE_OK)
14701633
{
@@ -1513,6 +1676,27 @@ void HttpOperation::PerformCurlMessage(CURLcode code)
15131676

15141677
if (IsRetryable())
15151678
{
1679+
1680+
std::string retry_after;
1681+
if (FindRetryAfterValue(response_headers_, retry_after))
1682+
{
1683+
std::chrono::seconds delay;
1684+
1685+
if (ParseRetryAfterDelay(retry_after, delay))
1686+
{
1687+
retry_after_time_point_ = std::chrono::system_clock::now() + delay;
1688+
}
1689+
else
1690+
{
1691+
std::chrono::system_clock::time_point date;
1692+
if (ParseRetryAfterDate(retry_after, date))
1693+
{
1694+
auto now = std::chrono::system_clock::now();
1695+
retry_after_time_point_ = (date > now) ? date : now;
1696+
}
1697+
}
1698+
}
1699+
15161700
// Clear any response data received in previous attempt
15171701
ReleaseResponse();
15181702
// Rewind request data so that read callback can re-transfer the payload

0 commit comments

Comments
 (0)