Skip to content

Commit bd0c772

Browse files
committed
Stop the flush tests leaking the handler
The fake session kept the handler so one test could complete a session after the waiter parked, but a handler owns its session, so that closed a reference cycle and every case leaked one handler and one session. The script now receives the shared_ptr and the one test that needs the handler holds it itself, so nothing the fakes own points back at them. Found by LeakSanitizer in the Bazel asan job, which builds with ENABLE_ASYNC_EXPORT and therefore runs these cases.
1 parent 61bffc3 commit bd0c772

3 files changed

Lines changed: 79 additions & 41 deletions

File tree

‎exporters/elasticsearch/include/opentelemetry/exporters/elasticsearch/es_log_record_exporter.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ class ElasticsearchLogRecordExporter final : public opentelemetry::sdk::logs::Lo
153153
std::atomic<std::size_t> finished_session_counter_{0};
154154
std::condition_variable force_flush_cv;
155155
std::mutex force_flush_cv_m;
156-
std::recursive_mutex force_flush_m;
156+
// Timed so that a second ForceFlush() caller is bounded by its own deadline rather than
157+
// by however long the first one waits.
158+
std::recursive_timed_mutex force_flush_m;
157159
};
158160
nostd::shared_ptr<SynchronizationData> synchronization_data_;
159161
#endif

‎exporters/elasticsearch/src/es_log_record_exporter.cc‎

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,35 @@ bool ElasticsearchLogRecordExporter::ForceFlush(std::chrono::microseconds timeou
505505
OPENTELEMETRY_MAYBE_UNUSED) noexcept
506506
{
507507
#ifdef ENABLE_ASYNC_EXPORT
508+
// ASAN will report chrono: runtime error: signed integer overflow: A + B cannot be represented
509+
// in type 'long int' here. So we reset timeout to meet signed long int limit here. Zero is
510+
// what that returns for a timeout there is no point waiting against, which is also how a
511+
// caller asks for no deadline.
512+
timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout(
513+
timeout, std::chrono::microseconds::zero());
514+
515+
// One deadline for the whole call, taken before anything can block, so that time spent waiting
516+
// for another ForceFlush() to finish counts against the caller's budget too.
517+
const bool indefinite = timeout <= std::chrono::microseconds::zero();
518+
const std::chrono::steady_clock::time_point deadline =
519+
indefinite ? (std::chrono::steady_clock::time_point::max)()
520+
: std::chrono::steady_clock::now() +
521+
std::chrono::duration_cast<std::chrono::steady_clock::duration>(timeout);
522+
508523
// Serialises concurrent ForceFlush() calls.
509-
std::lock_guard<std::recursive_mutex> lock_guard{synchronization_data_->force_flush_m};
524+
std::unique_lock<std::recursive_timed_mutex> flush_guard{synchronization_data_->force_flush_m,
525+
std::defer_lock};
526+
if (indefinite)
527+
{
528+
flush_guard.lock();
529+
}
530+
else if (!flush_guard.try_lock_until(deadline))
531+
{
532+
// Ran out of time before this call could even start. Everything outstanding may still have
533+
// finished while waiting, so answer from the counters rather than assuming failure.
534+
return synchronization_data_->finished_session_counter_.load(std::memory_order_acquire) >=
535+
synchronization_data_->session_counter_.load(std::memory_order_acquire);
536+
}
510537

511538
// The flush covers the sessions that were already running when it was asked for.
512539
const std::size_t running_counter =
@@ -516,29 +543,16 @@ bool ElasticsearchLogRecordExporter::ForceFlush(std::chrono::microseconds timeou
516543
running_counter;
517544
};
518545

519-
// ASAN will report chrono: runtime error: signed integer overflow: A + B cannot be represented
520-
// in type 'long int' here. So we reset timeout to meet signed long int limit here. Zero is
521-
// what that returns for a timeout there is no point waiting against, which is also how a
522-
// caller asks for no deadline.
523-
timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout(
524-
timeout, std::chrono::microseconds::zero());
525-
526546
std::unique_lock<std::mutex> lock(synchronization_data_->force_flush_cv_m);
527547

528-
if (timeout <= std::chrono::microseconds::zero())
548+
if (indefinite)
529549
{
530550
// wait() only returns once the predicate holds, so the flush has completed.
531551
synchronization_data_->force_flush_cv.wait(lock, flushed);
532552
return true;
533553
}
534554

535-
// One deadline for the call, so a wakeup that is not a completion resumes against what is left
536-
// rather than starting the wait again. wait_until() returns the predicate, so a flush that ran
537-
// out of time cannot report success.
538-
const std::chrono::steady_clock::time_point deadline =
539-
std::chrono::steady_clock::now() +
540-
std::chrono::duration_cast<std::chrono::steady_clock::duration>(timeout);
541-
555+
// wait_until() returns the predicate, so a flush that ran out of time cannot report success.
542556
return synchronization_data_->force_flush_cv.wait_until(lock, deadline, flushed);
543557
#else
544558
return true;

‎exporters/elasticsearch/test/es_log_record_exporter_test.cc‎

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <string>
2626
#include <thread>
2727
#include <utility>
28+
#include <vector>
2829
#include "nlohmann/json.hpp"
2930

3031
namespace sdklogs = opentelemetry::sdk::logs;
@@ -195,7 +196,7 @@ class FakeRequest : public http_client::Request
195196
void SetRetryPolicy(const http_client::RetryPolicy &) noexcept override {}
196197
};
197198

198-
using EventScript = std::function<void(http_client::EventHandler &)>;
199+
using EventScript = std::function<void(const std::shared_ptr<http_client::EventHandler> &)>;
199200

200201
class FakeSession : public http_client::Session
201202
{
@@ -207,12 +208,8 @@ class FakeSession : public http_client::Session
207208
}
208209
void SendRequest(std::shared_ptr<http_client::EventHandler> handler) noexcept override
209210
{
210-
last_handler_ = handler;
211-
script_(*handler);
211+
script_(handler);
212212
}
213-
214-
// Kept so a test can deliver a completion after the flush has started waiting.
215-
std::shared_ptr<http_client::EventHandler> last_handler_;
216213
bool IsSessionActive() noexcept override { return false; }
217214
bool CancelSession() noexcept override { return true; }
218215
bool FinishSession() noexcept override { return true; }
@@ -227,11 +224,8 @@ class FakeHttpClient : public http_client::HttpClient
227224
explicit FakeHttpClient(EventScript script) : script_(std::move(script)) {}
228225
std::shared_ptr<http_client::Session> CreateSession(nostd::string_view) noexcept override
229226
{
230-
last_session_ = std::make_shared<FakeSession>(script_);
231-
return last_session_;
227+
return std::make_shared<FakeSession>(script_);
232228
}
233-
234-
std::shared_ptr<FakeSession> last_session_;
235229
bool CancelAllSessions() noexcept override { return true; }
236230
bool FinishAllSessions() noexcept override { return true; }
237231
void SetMaxSessionsPerConnection(std::size_t) noexcept override {}
@@ -289,7 +283,7 @@ void ExportOnce(logs_exporter::ElasticsearchLogRecordExporter &exporter)
289283
// so when the caller's deadline runs out rather than when the response timeout does.
290284
TEST(ElasticsearchForceFlushTests, ReportsFailureWhenTheFlushDoesNotComplete)
291285
{
292-
auto fixture = MakeExporter([](http_client::EventHandler &) {});
286+
auto fixture = MakeExporter([](const std::shared_ptr<http_client::EventHandler> &) {});
293287
ExportOnce(*fixture.exporter);
294288

295289
const auto start = std::chrono::steady_clock::now();
@@ -304,7 +298,7 @@ TEST(ElasticsearchForceFlushTests, ReportsFailureWhenTheFlushDoesNotComplete)
304298
// Nothing outstanding, so there is nothing to wait for.
305299
TEST(ElasticsearchForceFlushTests, ReturnsImmediatelyWithNothingInFlight)
306300
{
307-
auto fixture = MakeExporter([](http_client::EventHandler &) {});
301+
auto fixture = MakeExporter([](const std::shared_ptr<http_client::EventHandler> &) {});
308302

309303
const auto start = std::chrono::steady_clock::now();
310304
EXPECT_TRUE(fixture.exporter->ForceFlush(std::chrono::milliseconds{20}));
@@ -318,9 +312,9 @@ TEST(ElasticsearchForceFlushTests, ReturnsImmediatelyWithNothingInFlight)
318312
// completion is already published when the predicate is first evaluated.
319313
TEST(ElasticsearchForceFlushTests, SucceedsWhenTheSessionFinishedBeforeTheWait)
320314
{
321-
auto fixture = MakeExporter([](http_client::EventHandler &handler) {
315+
auto fixture = MakeExporter([](const std::shared_ptr<http_client::EventHandler> &handler) {
322316
FakeResponse response(200, kAcceptedBody);
323-
handler.OnResponse(response);
317+
handler->OnResponse(response);
324318
});
325319
ExportOnce(*fixture.exporter);
326320

@@ -332,13 +326,14 @@ TEST(ElasticsearchForceFlushTests, SucceedsWhenTheSessionFinishedBeforeTheWait)
332326
TEST(ElasticsearchForceFlushTests, PartialCompletionIsNotSuccess)
333327
{
334328
bool respond = true;
335-
auto fixture = MakeExporter([&respond](http_client::EventHandler &handler) {
336-
if (respond)
337-
{
338-
FakeResponse response(200, kAcceptedBody);
339-
handler.OnResponse(response);
340-
}
341-
});
329+
auto fixture =
330+
MakeExporter([&respond](const std::shared_ptr<http_client::EventHandler> &handler) {
331+
if (respond)
332+
{
333+
FakeResponse response(200, kAcceptedBody);
334+
handler->OnResponse(response);
335+
}
336+
});
342337
ExportOnce(*fixture.exporter);
343338
respond = false;
344339
ExportOnce(*fixture.exporter);
@@ -350,10 +345,14 @@ TEST(ElasticsearchForceFlushTests, PartialCompletionIsNotSuccess)
350345
// the half the inline scripts above cannot reach.
351346
TEST(ElasticsearchForceFlushTests, ACompletionAfterTheWaiterParksWakesIt)
352347
{
353-
auto fixture = MakeExporter([](http_client::EventHandler &) {});
348+
// Held by the test, not by the fakes: a handler owns its session, so a session that also owned
349+
// its handler would be a reference cycle and leak.
350+
std::shared_ptr<http_client::EventHandler> captured;
351+
auto fixture =
352+
MakeExporter([&captured](const std::shared_ptr<http_client::EventHandler> &handler) {
353+
captured = handler;
354+
});
354355
ExportOnce(*fixture.exporter);
355-
ASSERT_NE(fixture.client->last_session_, nullptr);
356-
const auto captured = fixture.client->last_session_->last_handler_;
357356
ASSERT_NE(captured, nullptr);
358357

359358
std::thread responder([&captured] {
@@ -366,4 +365,27 @@ TEST(ElasticsearchForceFlushTests, ACompletionAfterTheWaiterParksWakesIt)
366365
responder.join();
367366
EXPECT_TRUE(flushed);
368367
}
368+
369+
// A second caller gets its own deadline. Serialising the calls is fine, making the second one
370+
// wait out the first one's is not, since its timeout would mean nothing.
371+
TEST(ElasticsearchForceFlushTests, AConcurrentFlushKeepsItsOwnDeadline)
372+
{
373+
std::vector<std::shared_ptr<http_client::EventHandler>> kept;
374+
auto fixture = MakeExporter([&kept](const std::shared_ptr<http_client::EventHandler> &handler) {
375+
kept.push_back(handler);
376+
});
377+
ExportOnce(*fixture.exporter);
378+
379+
std::thread slow([&fixture] { fixture.exporter->ForceFlush(std::chrono::seconds{3}); });
380+
std::this_thread::sleep_for(std::chrono::milliseconds{100});
381+
382+
const auto start = std::chrono::steady_clock::now();
383+
EXPECT_FALSE(fixture.exporter->ForceFlush(std::chrono::milliseconds{20}));
384+
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
385+
std::chrono::steady_clock::now() - start)
386+
.count();
387+
slow.join();
388+
389+
EXPECT_LT(ms, 1000) << "waited behind the first caller instead of its own deadline";
390+
}
369391
#endif // ENABLE_ASYNC_EXPORT

0 commit comments

Comments
 (0)