Skip to content

Commit dbfdde9

Browse files
committed
[BUG] Retire a refused export before describing the refusal
The export is registered before the shutdown check, and the guard that retires it could only run after the refusal had already been described. The log handler is replaceable, so a handler that calls ForceFlush() from that error waited for the Export() that was calling it. The same path also described one refusal twice, once itself and once through the guard. Retiring is now separate from reporting. The refusal retires, disarms the guard, and says so once. The completion still answers true, because changing what it answers would change what the response handler reads. Measured with a handler that calls ForceFlush(20ms) from the shutdown error. Before: the flush returns false after waiting out its full 20 ms, 3 of 3. After: it returns true in 0 ms, 3 of 3, and one refusal reads as one line. The file holds at 25 passing, 3 of 3. Three statements alongside it claimed more than they hold. An array cannot notice that an enum grew, so the comment now names the switch without a default under -Wswitch as what catches a new state. The curl completion lambda tests the response first and the abort in an else, so the comment that called it two independent ifs is out of date. And the fixture put a fresh default log handler back rather than the one it found. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent c0dd224 commit dbfdde9

2 files changed

Lines changed: 58 additions & 12 deletions

File tree

‎exporters/elasticsearch/src/es_log_record_exporter.cc‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,16 +458,24 @@ sdk::common::ExportResult ElasticsearchLogRecordExporter::Export(
458458
synchronization_data_->running_sessions.insert(session_id);
459459
}
460460

461-
using Completion = std::function<bool(opentelemetry::sdk::common::ExportResult)>;
462-
Completion complete = [span_count, session_id,
463-
synchronization_data](opentelemetry::sdk::common::ExportResult result) {
461+
// Retiring the export is separate from describing what happened to it, because the refusal
462+
// below has to retire without having an outcome to report through the completion.
463+
auto retire = [session_id, synchronization_data]() noexcept {
464+
bool retired = false;
464465
{
465466
// Published under the mutex ForceFlush() waits on. A waiter that has evaluated its
466467
// predicate but not yet parked would otherwise not see this until the next wakeup.
467468
std::lock_guard<std::mutex> lock(synchronization_data->force_flush_cv_m);
468-
synchronization_data->running_sessions.erase(session_id);
469+
retired = synchronization_data->running_sessions.erase(session_id) == 1;
469470
}
470471
synchronization_data->force_flush_cv.notify_all();
472+
return retired;
473+
};
474+
475+
using Completion = std::function<bool(opentelemetry::sdk::common::ExportResult)>;
476+
Completion complete = [span_count,
477+
retire](opentelemetry::sdk::common::ExportResult result) noexcept {
478+
retire();
471479

472480
// Logged after the session is retired. The log handler is replaceable, and one that calls
473481
// ForceFlush() would otherwise wait for the very session this call has not let go of yet.
@@ -512,6 +520,12 @@ sdk::common::ExportResult ElasticsearchLogRecordExporter::Export(
512520
// Return failure if this exporter has been shutdown
513521
if (isShutdown())
514522
{
523+
#ifdef ENABLE_ASYNC_EXPORT
524+
// Retired before anything replaceable runs, and reported here rather than through the guard,
525+
// so a log handler that flushes does not wait for this Export() and one refusal reads as one.
526+
guard.report = nullptr;
527+
retire();
528+
#endif
515529
OTEL_INTERNAL_LOG_ERROR("[ES Log Exporter] Exporting "
516530
<< records.size() << " log(s) failed, exporter is shutdown");
517531
return sdk::common::ExportResult::kFailure;

‎exporters/elasticsearch/test/es_log_record_exporter_test.cc‎

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ class ElasticsearchAsyncCompletionTests : public ::testing::Test
626626
#else
627627
// One skip point, because GTEST_SKIP returns and a second one after it would leave the rest of
628628
// this body unreachable, which MSVC reports as C4702 under maintainer mode.
629+
previous_handler_ = internal_log::GlobalLogHandler::GetLogHandler();
629630
handler_ = nostd::shared_ptr<internal_log::LogHandler>(new CompletionCountingLogHandler());
630631
internal_log::GlobalLogHandler::SetLogHandler(handler_);
631632
previous_level_ = internal_log::GlobalLogHandler::GetLogLevel();
@@ -638,8 +639,7 @@ class ElasticsearchAsyncCompletionTests : public ::testing::Test
638639
if (handler_)
639640
{
640641
internal_log::GlobalLogHandler::SetLogLevel(previous_level_);
641-
internal_log::GlobalLogHandler::SetLogHandler(
642-
nostd::shared_ptr<internal_log::LogHandler>(new internal_log::DefaultLogHandler()));
642+
internal_log::GlobalLogHandler::SetLogHandler(previous_handler_);
643643
}
644644
}
645645

@@ -651,6 +651,7 @@ class ElasticsearchAsyncCompletionTests : public ::testing::Test
651651
int Completions() const { return Counter().completions(); }
652652

653653
nostd::shared_ptr<internal_log::LogHandler> handler_;
654+
nostd::shared_ptr<internal_log::LogHandler> previous_handler_;
654655
internal_log::LogLevel previous_level_ = internal_log::LogLevel::Warning;
655656
};
656657
} // namespace
@@ -664,8 +665,9 @@ TEST_F(ElasticsearchAsyncCompletionTests, EverySessionStateIsClassifiedAndReport
664665
bool terminal;
665666
};
666667

667-
// Every member of the enum, so a state added upstream shows up here as a missing row rather than
668-
// as a session that quietly never finishes. The switch has no default label for the same reason.
668+
// Every member of the enum as it stands. An array cannot notice that the enum grew, so what
669+
// catches a new state is the switch in the exporter having no default, under the -Wswitch that
670+
// maintainer mode turns into an error. This table is the second half of that.
669671
const Case cases[] = {
670672
{http_client::SessionState::CreateFailed, true},
671673
{http_client::SessionState::Created, false},
@@ -846,9 +848,13 @@ namespace
846848
class FlushingLogHandler : public internal_log::LogHandler
847849
{
848850
public:
849-
void Watch(logs_exporter::ElasticsearchLogRecordExporter *exporter) noexcept
851+
// The needle picks which diagnostic re-enters the exporter, because the two paths that log
852+
// one describe it differently.
853+
void Watch(logs_exporter::ElasticsearchLogRecordExporter *exporter,
854+
const char *needle = "Logs were not written") noexcept
850855
{
851856
exporter_ = exporter;
857+
needle_ = needle;
852858
}
853859

854860
void Handle(internal_log::LogLevel /* level */,
@@ -861,10 +867,11 @@ class FlushingLogHandler : public internal_log::LogHandler
861867
{
862868
return;
863869
}
864-
if (std::string(msg).find("Logs were not written") == std::string::npos)
870+
if (std::string(msg).find(needle_) == std::string::npos)
865871
{
866872
return;
867873
}
874+
lines_.fetch_add(1, std::memory_order_relaxed);
868875
if (reentered_.exchange(true, std::memory_order_relaxed))
869876
{
870877
return;
@@ -874,11 +881,14 @@ class FlushingLogHandler : public internal_log::LogHandler
874881

875882
bool reentered() const noexcept { return reentered_.load(std::memory_order_relaxed); }
876883
bool flushed() const noexcept { return flushed_.load(std::memory_order_relaxed); }
884+
int lines() const noexcept { return lines_.load(std::memory_order_relaxed); }
877885

878886
private:
879887
logs_exporter::ElasticsearchLogRecordExporter *exporter_{nullptr};
888+
const char *needle_{"Logs were not written"};
880889
std::atomic<bool> reentered_{false};
881890
std::atomic<bool> flushed_{false};
891+
std::atomic<int> lines_{0};
882892
};
883893
} // namespace
884894

@@ -903,6 +913,27 @@ TEST_F(ElasticsearchAsyncCompletionTests, AFlushFromInsideTheLogHandlerDoesNotWa
903913
raw->Watch(nullptr);
904914
}
905915

916+
// The same rule on the path that refuses the batch. The export is registered before the shutdown
917+
// check, so reporting the refusal before retiring it makes a flushing handler wait for the
918+
// Export() that is calling it, and the refusal is described twice.
919+
TEST_F(ElasticsearchAsyncCompletionTests, AFlushFromTheShutdownErrorDoesNotWaitForItsOwnExport)
920+
{
921+
auto fixture = MakeExporter([](const std::shared_ptr<http_client::EventHandler> &) {});
922+
ASSERT_TRUE(fixture.exporter->Shutdown());
923+
924+
auto watcher = nostd::shared_ptr<internal_log::LogHandler>(new FlushingLogHandler());
925+
auto *raw = static_cast<FlushingLogHandler *>(watcher.get());
926+
raw->Watch(fixture.exporter.get(), "exporter is shutdown");
927+
internal_log::GlobalLogHandler::SetLogHandler(watcher);
928+
929+
ExportOnce(*fixture.exporter);
930+
931+
ASSERT_TRUE(raw->reentered()) << "the shutdown refusal never reached the log handler";
932+
EXPECT_TRUE(raw->flushed()) << "the flush waited for the export that was refusing itself";
933+
EXPECT_EQ(1, raw->lines()) << "one refusal was described " << raw->lines() << " times";
934+
raw->Watch(nullptr);
935+
}
936+
906937
// Two responses for one request write the same body and race for the same outcome. The body is a
907938
// local so there is nothing shared to tear, and the exchange decides which one reports.
908939
TEST_F(ElasticsearchAsyncCompletionTests, TwoConcurrentResponsesReportOnce)
@@ -958,8 +989,9 @@ TEST_F(ElasticsearchAsyncCompletionTests, TwoTerminalEventsCountAsOneSession)
958989
<< "the second session is still in flight";
959990
}
960991

961-
// The other in-tree double fire: the completion lambda uses two independent ifs, so an aborted
962-
// operation that also has a response reports Cancelled and then delivers the response.
992+
// The curl client's completion lambda tests the response first and the abort in an else, so this
993+
// ordering no longer comes from it. It still comes from anywhere: EventHandler promises callers
994+
// nothing about how many terminal events arrive, and #4360 is open on exactly that.
963995
TEST_F(ElasticsearchAsyncCompletionTests, CancelledThenAResponseCountsOnce)
964996
{
965997
std::vector<std::shared_ptr<http_client::EventHandler>> kept;

0 commit comments

Comments
 (0)