Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Increment the:

## [Unreleased]

* [OTLP/HTTP] Honor `Retry-After` response header when retrying exports,
supporting both delay-seconds and HTTP-date formats per RFC 7231 §7.1.3.
[#4172](https://github.com/open-telemetry/opentelemetry-cpp/issues/4172)

* [SDK] Add `TracerProvider::UpdateTracerConfigurator()` and example
[#4065](https://github.com/open-telemetry/opentelemetry-cpp/issues/4065)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ class HttpOperation
const RetryPolicy retry_policy_;
decltype(RetryPolicy::max_attempts) retry_attempts_;
std::chrono::system_clock::time_point last_attempt_time_;
std::chrono::system_clock::time_point retry_after_time_point_{};

// Processed response headers and body
// See CURLINFO_RESPONSE_CODE, type is long
Expand Down
99 changes: 97 additions & 2 deletions ext/src/http/client/curl/http_operation_curl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@

#include <algorithm>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cmath>
#include <cstring>
#include <ctime>
#include <functional>
#include <future>
#include <iomanip>
#include <memory>
#include <random>
#include <sstream>
Expand All @@ -35,6 +38,69 @@
# define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z))
#endif

namespace
{
std::time_t PortableTimegm(std::tm *tm)
{
int year = tm->tm_year + 1900;
int month = tm->tm_mon + 1;

if (month <= 2)
{
year -= 1;
month += 12;
}

int day = tm->tm_mday;
int days = 365 * year + year / 4 - year / 100 + year / 400 + 367 * month / 12 - 30 + day - 719530;

return static_cast<std::time_t>(days) * 86400 + tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec;
}

bool ParseRetryAfterDelay(std::string value, std::chrono::seconds &delay)
{
value.erase(0, value.find_first_not_of(" \t\r\n"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use common::StringUtil::Trim to trim string.

value.erase(value.find_last_not_of(" \t\r\n") + 1);

if (value.empty())
{
return false;
}

if (std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c); }))
{
try

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please use the codes just like GetTimeoutFromString in sdk/src/common/env_variables.cc to parse duration? The common codes can be moved into api/include/opentelemetry/common/timestamp.h .

And exception can be disabled by -fno-exception or /EH . We should use OPENTELEMETRY_HAVE_EXCEPTIONS to check if exception is enabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The try/catch around std::stoull is very likely what's triggering the Bazel noexcept CI failure.

I'll replace it with a manual digit-by-digit parsing loop that performs explicit overflow checks, following the same approach used by GetTimeoutFromString in sdk/src/common/env_variables.cc. Where appropriate, I'll keep the exception-related handling guarded with OPENTELEMETRY_HAVE_EXCEPTIONS.

I'll also move the shared parsing logic into api/include/opentelemetry/common/timestamp.h as you suggested so it can be reused across different call sites.

{
delay = std::chrono::seconds(std::stoull(value));
return true;
}
catch (...)
{
return false;
}
}
return false;
}

bool ParseRetryAfterDate(std::string value, std::chrono::system_clock::time_point &date)
{
value.erase(0, value.find_first_not_of(" \t\r\n"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use common::StringUtil::Trim to trim string.

value.erase(value.find_last_not_of(" \t\r\n") + 1);

std::tm tm = {};
std::istringstream ss(value);

ss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1 seems support much more formats and timezone setting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. I went back and checked §7.1.1.1 again, and you're right. The RFC defines three valid HTTP-date formats:

  • IMF-fixdate: Sun, 06 Nov 1994 08:49:37 GMT
  • RFC 850: Sunday, 06-Nov-94 08:49:37 GMT
  • asctime: Sun Nov 6 08:49:37 1994

Right now I'm only handling the first one, so I'll add support for the other two as well since the RFC requires recipients to accept all three.

I'll also implement the RFC 850 two-digit year handling. If the parsed year ends up looking more than 50 years in the future, I'll map it back to the most recent matching year in the past, as required by the spec.

For the timezone part, I don't think any extra handling is needed. All three formats are defined as UTC the first two explicitly use GMT, and the asctime format is also specified to be interpreted as UTC. So treating the parsed std::tm as UTC with PortableTimegm should be the right thing to do here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — all valid timezones in HTTP-date are UTC/GMT, so timezone handling isn't needed. Only the datetime format variations matter.

if (!ss.fail())
{
std::time_t epoch = PortableTimegm(&tm);
Comment thread
owent marked this conversation as resolved.
date = std::chrono::system_clock::from_time_t(epoch);
return true;
}
return false;
}
} // namespace

OPENTELEMETRY_BEGIN_NAMESPACE
namespace ext
{
Expand Down Expand Up @@ -560,6 +626,11 @@ bool HttpOperation::IsRetryable()

std::chrono::system_clock::time_point HttpOperation::NextRetryTime()
{
if (retry_after_time_point_ != std::chrono::system_clock::time_point{})
{
return retry_after_time_point_;
}

static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<float> dis(0.8f, 1.2f);
Expand Down Expand Up @@ -1463,8 +1534,9 @@ void HttpOperation::Abort()
void HttpOperation::PerformCurlMessage(CURLcode code)
{
++retry_attempts_;
last_attempt_time_ = std::chrono::system_clock::now();
last_curl_result_ = code;
last_attempt_time_ = std::chrono::system_clock::now();
last_curl_result_ = code;
retry_after_time_point_ = std::chrono::system_clock::time_point{};

if (code != CURLE_OK)
{
Expand Down Expand Up @@ -1513,6 +1585,29 @@ void HttpOperation::PerformCurlMessage(CURLcode code)

if (IsRetryable())
{
auto headers = GetResponseHeaders();
Comment thread
owent marked this conversation as resolved.
auto it = headers.find("Retry-After");

if (it != headers.end())
{
std::string retry_after = it->second;
std::chrono::seconds delay;

if (ParseRetryAfterDelay(retry_after, delay))
{
retry_after_time_point_ = std::chrono::system_clock::now() + delay;
}
else
{
std::chrono::system_clock::time_point date;
if (ParseRetryAfterDate(retry_after, date))
{
auto now = std::chrono::system_clock::now();
retry_after_time_point_ = (date > now) ? date : now;
}
}
}

// Clear any response data received in previous attempt
ReleaseResponse();
// Rewind request data so that read callback can re-transfer the payload
Expand Down
Loading