Skip to content

Commit 7087038

Browse files
Merge branch '1724-reaggregation-fix-for-dimensions-dropping' of https://github.com/nikhilbhatia08/opentelemetry-cpp into 1724-reaggregation-fix-for-dimensions-dropping
2 parents fcf36a4 + 8e4a236 commit 7087038

8 files changed

Lines changed: 318 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ Increment the:
2424
`AttributesProcessor`, matching `SyncMetricStorage`.
2525
[#1724](https://github.com/open-telemetry/opentelemetry-cpp/issues/1724)
2626

27+
* [LOGS] Fix undefined behavior in `Logger::EmitLogRecord()` when a logger is
28+
enabled (e.g. via `LoggerProvider::UpdateLoggerConfigurator()`) after
29+
`CreateLogRecord()` was called while it was still disabled. The disabled
30+
path previously returned a `NoopLogRecord`, which is not the SDK's internal
31+
`Recordable`; `EmitLogRecord()` re-checks the enabled state at emit time, so
32+
such a record could reach its unconditional `static_cast<Recordable *>` and
33+
then `MultiLogRecordProcessor::OnEmit()`'s own `static_cast`, both undefined
34+
behavior. `CreateLogRecord()` now returns an empty `MultiRecordable` while
35+
disabled instead, which is a safe target either way: every `Set*` call and
36+
`ReleaseRecordable()` on it simply loop over zero wrapped recordables, so
37+
the record is dropped without reaching a real processor. No API or ABI
38+
change.
39+
[#4624](https://github.com/open-telemetry/opentelemetry-cpp/pull/4624)
40+
2741
* [EXAMPLES] Fix random attribute selection in metrics foo example to include
2842
all key-value pairs
2943
[#4585](https://github.com/open-telemetry/opentelemetry-cpp/pull/4585)
@@ -43,6 +57,18 @@ Increment the:
4357
to compile standalone on newer standard library implementations.
4458
[#4574](https://github.com/open-telemetry/opentelemetry-cpp/pull/4574)
4559

60+
* [BUG] Send one request per curl session, rather than replacing the operation
61+
a running request still belongs to
62+
[#4396](https://github.com/open-telemetry/opentelemetry-cpp/issues/4396)
63+
64+
* [BUG] Compare the curl seek offset in `curl_off_t` before narrowing it to
65+
`size_t`, so an offset past the `size_t` range cannot wrap back inside the
66+
request body on a 32-bit build
67+
[#4630](https://github.com/open-telemetry/opentelemetry-cpp/pull/4630)
68+
69+
* [BUG] Do not queue a curl session closed by the Retry-After cap
70+
[#4632](https://github.com/open-telemetry/opentelemetry-cpp/pull/4632)
71+
4672
## [1.29.0] 2026-09-13
4773

4874
* [RELEASE] Bump main branch to 1.29.0-dev (#4259)

‎ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,25 @@ class Session : public opentelemetry::ext::http::client::Session,
186186

187187
std::shared_ptr<opentelemetry::ext::http::client::Request> CreateRequest() noexcept override
188188
{
189+
if (send_started_.load(std::memory_order_acquire))
190+
{
191+
// The request that has been sent stays where it is. libcurl does not copy the body or the
192+
// header list, so the transfer reads them for as long as it runs.
193+
return http_request_;
194+
}
195+
189196
http_request_.reset(new Request());
190197
return http_request_;
191198
}
192199

200+
/**
201+
* Send the request this session carries.
202+
*
203+
* A session carries one request. A second call reports CreateFailed to its own handler and
204+
* sends nothing: the easy handle of the first request holds this session and the operation
205+
* that owns it, and replacing that operation takes away what libcurl and the client are still
206+
* reading.
207+
*/
193208
void SendRequest(
194209
std::shared_ptr<opentelemetry::ext::http::client::EventHandler> callback) noexcept override;
195210

@@ -235,6 +250,9 @@ class Session : public opentelemetry::ext::http::client::Session,
235250
uint64_t session_id_ = 0UL;
236251
HttpClient &http_client_;
237252
std::atomic<bool> is_session_active_{false};
253+
254+
// Raised by the first SendRequest and never lowered. A session carries one request.
255+
std::atomic<bool> send_started_{false};
238256
};
239257

240258
class HttpClientSync : public opentelemetry::ext::http::client::HttpClientSync

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,9 @@ class HttpOperation
298298
* be called when got a CURLMSG_DONE.
299299
*
300300
* @param code CURLcode
301+
* @return true if the request was re-armed for a retry, false if the operation was cleaned up
301302
*/
302-
void PerformCurlMessage(CURLcode code);
303+
bool PerformCurlMessage(CURLcode code);
303304

304305
inline CURL *GetCurlEasyHandle() noexcept { return curl_resource_.easy_handle; }
305306

‎ext/src/http/client/curl/http_client_curl.cc‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@ static int deflateInPlace(z_stream *strm, unsigned char *buf, uint32_t len, uint
141141
void Session::SendRequest(
142142
std::shared_ptr<opentelemetry::ext::http::client::EventHandler> callback) noexcept
143143
{
144+
if (send_started_.exchange(true, std::memory_order_acq_rel))
145+
{
146+
// The first request is not finished with this session. Its easy handle names the session in
147+
// CURLOPT_PRIVATE and names its operation in every callback it was given, and the message
148+
// loop resolves that name to whichever operation the session owns, so a second operation
149+
// would be handed the first one's completion. Worse from a handler that sends again from
150+
// OnResponse, where the operation being replaced is the one running that handler.
151+
if (callback)
152+
{
153+
callback->OnEvent(opentelemetry::ext::http::client::SessionState::CreateFailed,
154+
"a session carries one request");
155+
}
156+
return;
157+
}
158+
144159
is_session_active_.store(true, std::memory_order_release);
145160
const auto &url = host_ + http_request_->uri_;
146161
auto callback_ptr = callback.get();
@@ -534,9 +549,7 @@ bool HttpClient::MaybeSpawnBackgroundThread()
534549
{
535550
// Session can not be destroyed when calling PerformCurlMessage
536551
auto hold_session = session->shared_from_this();
537-
operation->PerformCurlMessage(result);
538-
539-
if (operation->IsRetryable())
552+
if (operation->PerformCurlMessage(result))
540553
{
541554
self->pending_to_retry_sessions_.push_back(hold_session);
542555
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ int HttpOperation::SeekCallback(void *userp, curl_off_t offset, int origin)
347347
// move of the read cursor. Anything else is refused rather than approximated, because reporting
348348
// success without repositioning would resume the upload from the wrong offset and send a
349349
// truncated or misaligned body.
350-
if (origin != SEEK_SET || offset < 0 || static_cast<size_t>(offset) > self->request_body_.size())
350+
if (origin != SEEK_SET || offset < 0 ||
351+
offset > static_cast<curl_off_t>(self->request_body_.size()))
351352
{
352353
return CURL_SEEKFUNC_CANTSEEK;
353354
}
@@ -1561,7 +1562,7 @@ void HttpOperation::Abort()
15611562
}
15621563
}
15631564

1564-
void HttpOperation::PerformCurlMessage(CURLcode code)
1565+
bool HttpOperation::PerformCurlMessage(CURLcode code)
15651566
{
15661567
++retry_attempts_;
15671568
last_attempt_time_ = std::chrono::system_clock::now();
@@ -1667,7 +1668,10 @@ void HttpOperation::PerformCurlMessage(CURLcode code)
16671668
{
16681669
// Cleanup and unbind easy handle from multi handle, and finish callback
16691670
Cleanup();
1671+
return false;
16701672
}
1673+
1674+
return true;
16711675
}
16721676

16731677
} // namespace curl

‎ext/test/http/curl_http_test.cc‎

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
#include <atomic>
1818
#include <chrono>
1919
#include <condition_variable>
20+
#include <cstdint>
2021
#include <cstdio>
2122
#include <cstring>
23+
#include <limits>
2224
#include <map>
2325
#include <memory>
2426
#include <mutex>
@@ -120,6 +122,37 @@ class CustomEventHandler : public http_client::EventHandler
120122
// inside the Response event, which is the one moment both arms of the completion callback are
121123
// eligible: DispatchEvent notifies the handler before it stores the new state, and the callback
122124
// runs after both, so it sees an aborted operation that also has a response.
125+
class ReentrantSendHandler : public CustomEventHandler
126+
{
127+
public:
128+
void OnResponse(http_client::Response & /* response */) noexcept override
129+
{
130+
got_response_.store(true, std::memory_order_release);
131+
132+
if (session_ != nullptr && !resent_.exchange(true, std::memory_order_acq_rel))
133+
{
134+
auto again = session_->CreateRequest();
135+
again->SetUri("get/");
136+
session_->SendRequest(self_.lock());
137+
}
138+
}
139+
140+
void OnEvent(http_client::SessionState state, nostd::string_view /* reason */) noexcept override
141+
{
142+
if (state == http_client::SessionState::CreateFailed)
143+
{
144+
create_failed_.fetch_add(1, std::memory_order_release);
145+
}
146+
}
147+
148+
http_client::Session *session_ = nullptr;
149+
std::weak_ptr<ReentrantSendHandler> self_;
150+
std::atomic<int> create_failed_{0};
151+
152+
private:
153+
std::atomic<bool> resent_{false};
154+
};
155+
123156
class TerminalCountingHandler : public CustomEventHandler
124157
{
125158
public:
@@ -262,6 +295,7 @@ class BasicCurlHttpTests : public ::testing::Test, public HTTP_SERVER_NS::HttpRe
262295
server_.addHandler("/get/", *this);
263296
server_.addHandler("/post/", *this);
264297
server_.addHandler("/retry/", *this);
298+
server_.addHandler("/retry-after/", *this);
265299
server_.addHandler("/close/", *this);
266300
server_.start();
267301
is_running_ = true;
@@ -302,6 +336,14 @@ class BasicCurlHttpTests : public ::testing::Test, public HTTP_SERVER_NS::HttpRe
302336
response.headers["Content-Type"] = "text/plain";
303337
response_status = 429;
304338
}
339+
else if (request.uri == "/retry-after/")
340+
{
341+
std::unique_lock<std::mutex> lk1(mtx_requests);
342+
received_requests_.push_back(request);
343+
response.headers["Content-Type"] = "text/plain";
344+
response.headers["Retry-After"] = "30";
345+
response_status = 429;
346+
}
305347
else if (request.uri == "/close/")
306348
{
307349
// -1 is the documented way for a handler to ask the server to terminate the
@@ -506,6 +548,12 @@ TEST_F(BasicCurlHttpTests, SeekCallbackRepositionsTheRequestBody)
506548
EXPECT_EQ(CURL_SEEKFUNC_CANTSEEK, Peer::Seek(operation, -1, SEEK_SET));
507549
EXPECT_EQ(CURL_SEEKFUNC_CANTSEEK, Peer::Seek(operation, 0, SEEK_CUR));
508550
EXPECT_EQ(CURL_SEEKFUNC_CANTSEEK, Peer::Seek(operation, 0, SEEK_END));
551+
552+
// An offset past the range of size_t must stay out of range. Narrowing it to size_t first
553+
// wrapped it back into the body on a build where size_t is 32 bits.
554+
EXPECT_EQ(CURL_SEEKFUNC_CANTSEEK,
555+
Peer::Seek(operation, static_cast<curl_off_t>(std::numeric_limits<uint32_t>::max()) + 4,
556+
SEEK_SET));
509557
EXPECT_EQ(3u, Peer::ReadCursor(operation));
510558

511559
EXPECT_EQ(CURL_SEEKFUNC_CANTSEEK, Peer::SeekNullUserData(0, SEEK_SET));
@@ -638,6 +686,90 @@ TEST_F(BasicCurlHttpTests, ExponentialBackoffRetry)
638686
ASSERT_EQ(CURLE_OK, operation.Send());
639687
ASSERT_FALSE(operation.IsRetryable());
640688
}
689+
690+
// A Retry-After beyond max_backoff closes the session. The IO loop used to queue it anyway, where
691+
// it held back later retries and the background thread until the server's time.
692+
TEST_F(BasicCurlHttpTests, RetryAfterBeyondMaxBackoffIsNotQueued)
693+
{
694+
received_requests_.clear();
695+
curl::HttpClient http_client;
696+
const http_client::RetryPolicy retry_policy = {2, std::chrono::duration<float>{0.1f},
697+
std::chrono::duration<float>{1.0f}, 1.0f};
698+
699+
auto capped_session = http_client.CreateSession("http://127.0.0.1:19000");
700+
auto capped_request = capped_session->CreateRequest();
701+
capped_request->SetMethod(http_client::Method::Post);
702+
capped_request->SetUri("retry-after/");
703+
capped_request->SetRetryPolicy(retry_policy);
704+
auto capped_handler = std::make_shared<RetryEventHandler>();
705+
capped_session->SendRequest(capped_handler);
706+
capped_session->FinishSession();
707+
ASSERT_TRUE(capped_handler->got_response_.load(std::memory_order_acquire));
708+
709+
auto session = http_client.CreateSession("http://127.0.0.1:19000");
710+
auto request = session->CreateRequest();
711+
request->SetMethod(http_client::Method::Post);
712+
request->SetUri("retry/");
713+
request->SetRetryPolicy(retry_policy);
714+
auto handler = std::make_shared<RetryEventHandler>();
715+
auto started_at = std::chrono::steady_clock::now();
716+
session->SendRequest(handler);
717+
session->FinishSession();
718+
const auto retried_in = std::chrono::steady_clock::now() - started_at;
719+
ASSERT_TRUE(handler->got_response_.load(std::memory_order_acquire));
720+
721+
started_at = std::chrono::steady_clock::now();
722+
http_client.WaitBackgroundThreadExit();
723+
const auto joined_in = std::chrono::steady_clock::now() - started_at;
724+
725+
// The server asks for 30 s; the policy backs off for about 0.1 s.
726+
EXPECT_TRUE(retried_in < std::chrono::seconds{10})
727+
<< "retry ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(retried_in).count();
728+
EXPECT_TRUE(joined_in < std::chrono::seconds{10})
729+
<< "join ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(joined_in).count();
730+
731+
std::unique_lock<std::mutex> lock_requests(mtx_requests);
732+
const auto hits = [this](const char *uri) {
733+
return std::count_if(
734+
received_requests_.begin(), received_requests_.end(),
735+
[uri](const HTTP_SERVER_NS::HttpRequest &received) { return received.uri == uri; });
736+
};
737+
EXPECT_EQ(1, hits("/retry-after/"));
738+
EXPECT_EQ(2, hits("/retry/"));
739+
}
740+
741+
// The shutdown half of #4631: the closed session used to hold the join until the server's time.
742+
TEST_F(BasicCurlHttpTests, RetryAfterBeyondMaxBackoffDoesNotDelayShutdown)
743+
{
744+
received_requests_.clear();
745+
curl::HttpClient http_client;
746+
const http_client::RetryPolicy retry_policy = {2, std::chrono::duration<float>{0.1f},
747+
std::chrono::duration<float>{1.0f}, 1.0f};
748+
749+
auto session = http_client.CreateSession("http://127.0.0.1:19000");
750+
auto request = session->CreateRequest();
751+
request->SetMethod(http_client::Method::Post);
752+
request->SetUri("retry-after/");
753+
request->SetRetryPolicy(retry_policy);
754+
auto handler = std::make_shared<RetryEventHandler>();
755+
session->SendRequest(handler);
756+
session->FinishSession();
757+
ASSERT_TRUE(handler->got_response_.load(std::memory_order_acquire));
758+
759+
const auto started_at = std::chrono::steady_clock::now();
760+
http_client.WaitBackgroundThreadExit();
761+
const auto joined_in = std::chrono::steady_clock::now() - started_at;
762+
763+
// The server asks for 30 s.
764+
EXPECT_TRUE(joined_in < std::chrono::seconds{10})
765+
<< "join ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(joined_in).count();
766+
767+
std::unique_lock<std::mutex> lock_requests(mtx_requests);
768+
EXPECT_EQ(1, std::count_if(received_requests_.begin(), received_requests_.end(),
769+
[](const HTTP_SERVER_NS::HttpRequest &received) {
770+
return received.uri == "/retry-after/";
771+
}));
772+
}
641773
#endif // ENABLE_OTLP_RETRY_PREVIEW
642774

643775
// A cancel that arrives once the server has answered used to deliver Cancelled and the response,
@@ -742,6 +874,63 @@ TEST_F(BasicCurlHttpTests, ResetMultiHandleWithASessionDoesNotDeadlock)
742874
client->FinishAllSessions();
743875
}
744876

877+
// The reproduction from #4396. A handler that sends again from OnResponse replaces the operation
878+
// whose Cleanup is running that handler, and Cleanup reads that operation again on the way out.
879+
TEST_F(BasicCurlHttpTests, ASecondRequestFromInsideTheResponseIsRefused)
880+
{
881+
received_requests_.clear();
882+
auto session_manager = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
883+
ASSERT_TRUE(session_manager != nullptr);
884+
885+
auto session = session_manager->CreateSession("http://127.0.0.1:19000");
886+
auto request = session->CreateRequest();
887+
request->SetUri("get/");
888+
889+
auto handler = std::make_shared<ReentrantSendHandler>();
890+
handler->session_ = session.get();
891+
handler->self_ = handler;
892+
893+
session->SendRequest(handler);
894+
ASSERT_TRUE(waitForRequests(30, 1));
895+
session->FinishSession();
896+
897+
EXPECT_TRUE(handler->got_response_.load(std::memory_order_acquire));
898+
EXPECT_EQ(1, handler->create_failed_.load(std::memory_order_acquire));
899+
900+
session_manager->FinishAllSessions();
901+
}
902+
903+
// The same thing without the re-entrancy, and the request the first send is reading from is not
904+
// replaced under it either.
905+
TEST_F(BasicCurlHttpTests, ASessionSendsOneRequest)
906+
{
907+
received_requests_.clear();
908+
auto session_manager = std::make_shared<http_client::curl::HttpCurlClientFactory>()->Create();
909+
ASSERT_TRUE(session_manager != nullptr);
910+
911+
auto session = session_manager->CreateSession("http://127.0.0.1:19000");
912+
auto request = session->CreateRequest();
913+
request->SetUri("get/");
914+
915+
auto first = std::make_shared<ReentrantSendHandler>();
916+
session->SendRequest(first);
917+
ASSERT_TRUE(waitForRequests(30, 1));
918+
session->FinishSession();
919+
920+
EXPECT_TRUE(first->got_response_.load(std::memory_order_acquire));
921+
EXPECT_EQ(0, first->create_failed_.load(std::memory_order_acquire));
922+
923+
auto again = session->CreateRequest();
924+
EXPECT_EQ(request.get(), again.get());
925+
926+
auto second = std::make_shared<ReentrantSendHandler>();
927+
session->SendRequest(second);
928+
EXPECT_EQ(1, second->create_failed_.load(std::memory_order_acquire));
929+
EXPECT_FALSE(second->got_response_.load(std::memory_order_acquire));
930+
931+
session_manager->FinishAllSessions();
932+
}
933+
745934
// The caller-thread side of the same cancel. The server handler takes mtx_requests before it
746935
// answers, so holding it keeps a response from racing the cancel and the abort lands while the
747936
// IO thread is still driving the easy handle. That pairing is what #4369 caught.

0 commit comments

Comments
 (0)