Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ 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)
* [CODE HEALTH] Move api/test and sdk/test classes into anonymous namespace
[#4217](https://github.com/open-telemetry/opentelemetry-cpp/pull/4217)
* docs: update supported development platforms
[#4260](https://github.com/open-telemetry/opentelemetry-cpp/pull/4260)

Expand Down
106 changes: 106 additions & 0 deletions api/include/opentelemetry/common/timestamp.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@

#pragma once

#include <cctype>
#include <chrono>
#include <cstdint>
#include <ctime>
#include <iomanip>
#include <limits>
#include <sstream>
#include <string>

#include "opentelemetry/common/string_util.h"
#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/version.h"

OPENTELEMETRY_BEGIN_NAMESPACE
Expand Down Expand Up @@ -202,5 +210,103 @@ class DurationUtil
}
};

/**
* @brief Parsing helpers for RFC 7231 value formats (HTTP-date, section
* 7.1.1.1, and delay-seconds, section 7.1.3) that appear in headers such
* as Retry-After, Date, Last-Modified, and Expires.
*/
class HttpUtil
{
public:
static bool ParseDelaySeconds(nostd::string_view value, std::chrono::seconds &delay) noexcept
{
value = StringUtil::Trim(value);
if (value.empty())
{
return false;
}

std::chrono::seconds::rep result = 0;
for (const char c : value)
{
if (!std::isdigit(static_cast<unsigned char>(c)))
{
return false;
}
auto digit = c - '0';
if (result > ((std::numeric_limits<decltype(result)>::max)() - digit) / 10)
{
return false;
}
result = result * 10 + digit;
}

delay = std::chrono::seconds(result);
return true;
}

static bool ParseHttpDate(nostd::string_view value, std::chrono::system_clock::time_point &date)
{
value = StringUtil::Trim(value);
std::string str(value.data(), value.size());
std::tm tm = {};
std::istringstream ss(str);

ss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S");
if (!ss.fail())
{
date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm));
return true;
}

ss.clear();
ss.str(str);
tm = {};
ss >> std::get_time(&tm, "%A, %d-%b-%y %H:%M:%S");
if (!ss.fail())
{
std::time_t now_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
int current_year = 1970 + static_cast<int>(now_t / 31556952);
int full_year = 1900 + tm.tm_year;
if (full_year - current_year > 50)
{
full_year -= 100;
}
tm.tm_year = full_year - 1900;
date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm));
return true;
}

ss.clear();
ss.str(str);
tm = {};
ss >> std::get_time(&tm, "%a %b %d %H:%M:%S %Y");
if (!ss.fail())
{
date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm));
return true;
}

return false;
}

private:
static std::time_t PortableTimegm(std::tm *tm) noexcept
{
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 + (153 * (month - 3) + 2) / 5 + day -
719469;
return static_cast<std::time_t>(days) * 86400 + tm->tm_hour * 3600 + tm->tm_min * 60 +
tm->tm_sec;
}
};

} // namespace common
OPENTELEMETRY_END_NAMESPACE
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
83 changes: 81 additions & 2 deletions ext/src/http/client/curl/http_operation_curl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
# include <array>
#endif // ENABLE_OTLP_RETRY_PREVIEW

#include <stdint.h>
#include <algorithm>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cmath>
#include <cstring>
Expand All @@ -23,6 +25,7 @@
#include <utility>
#include <vector>

#include "opentelemetry/common/timestamp.h"
#include "opentelemetry/ext/http/client/curl/http_client_curl.h"
#include "opentelemetry/ext/http/client/curl/http_operation_curl.h"
#include "opentelemetry/ext/http/client/http_client.h"
Expand All @@ -35,6 +38,55 @@
# define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z))
#endif

namespace
{

bool FindRetryAfterValue(const std::vector<uint8_t> &raw_headers, std::string &value)
{
if (raw_headers.empty())
{
return false;
}

const char *data = reinterpret_cast<const char *>(raw_headers.data());
const char *end = data + raw_headers.size();
const char *line = data;

while (line < end)
{
const char *line_end = line;
while (line_end < end && *line_end != '\n')
{
++line_end;
}

static const char kRetryAfterHeader[] = "retry-after:";
static const size_t kRetryAfterLen = sizeof(kRetryAfterHeader) - 1;

size_t line_len = static_cast<size_t>(line_end - line);
if (line_len > kRetryAfterLen)
{
bool match = true;
for (size_t i = 0; i < kRetryAfterLen && match; ++i)
{
match = (std::tolower(static_cast<unsigned char>(line[i])) == kRetryAfterHeader[i]);
}

if (match)
{
value = std::string(line + kRetryAfterLen, line_end);
return true;
}
}

line = (line_end < end) ? line_end + 1 : end;
}

return false;
}

} // namespace

OPENTELEMETRY_BEGIN_NAMESPACE
namespace ext
{
Expand Down Expand Up @@ -560,6 +612,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 +1520,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 +1571,27 @@ void HttpOperation::PerformCurlMessage(CURLcode code)

if (IsRetryable())
{

std::string retry_after;
if (FindRetryAfterValue(response_headers_, retry_after))
{
std::chrono::seconds delay;

if (opentelemetry::common::HttpUtil::ParseDelaySeconds(retry_after, delay))
{
retry_after_time_point_ = std::chrono::system_clock::now() + delay;
}
else
{
std::chrono::system_clock::time_point date;
if (opentelemetry::common::HttpUtil::ParseHttpDate(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