open-telemetry / open-telemetry/opentelemetry-cpp

[BUG] A log handler that calls ForceFlush can deadlock the export that is logging

Open
#4,435 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
Dominant language
C++
Stars
1.4k
Forks
632
Avg merge
1d 13h
Merged PRs (30d)
75

Description

Describe your environment main at 5af96de2, Ubuntu, gcc 14, CMake, -DWITH_ASYNC_EXPORT_PREVIEW=ON.

LogHandler::Handle() says nothing about what it may call, and an application can replace the handler and raise the level to Debug through GlobalLogHandler. Two of the diagnostics an exporter writes are dispatched from places where calling ForceFlush() back into that exporter cannot finish.

Before the request has been handed to the client

ElasticsearchLogRecordExporter::Export() registers the export, then calls Session::SendRequest(). SendRequest builds the HttpOperation, whose constructor dispatches Created, which the exporter's handler answers with OTEL_INTERNAL_LOG_DEBUG("[ES Log Exporter] Session created"). The background thread that would run the request is only asked for after SendAsync() returns, in Session::SendRequest.

So a handler that calls the no deadline ForceFlush() from that line waits for an export whose request has not been given to anything yet, and SendRequest() cannot return while it waits. Nothing else can move.

From a callback the client dispatches

The bundled curl client runs one background thread per HttpClient. A terminal diagnostic written from a completion runs on that thread. A handler that flushes from there waits for every other outstanding export, and those can only be advanced by the thread it is blocking. #4402 is the same shape for FinishSession().

Not specific to this exporter

Nothing above is about Elasticsearch. Any exporter whose ForceFlush() blocks and whose diagnostics are written from an export or a callback has both shapes. The OTLP HTTP exporter logs from the same places.

Not new

main already waits without a bound: ForceFlush() there sets timeout_steady to duration::max() when the caller asks for no deadline.

Correction, 17 Aug. I wrote above that the Created debug line is already on main. It is on main in the synchronous ResponseHandler, and it is not in the asynchronous one, which is the handler this issue is about and the only one whose ForceFlush() waits for anything. On 60c3d11e that handler's OnEvent is a default: break; with no progress logging at all. So the progress diagnostics were introduced by #4337 rather than inherited, and #4337 now removes them again: the states on the way to an outcome report nothing, and the exhaustive switch stays so a new state is still a compile error.

What remains in this issue after that is the general shape and not the five progress events: a handler that flushes from any callback the HTTP client dispatches can still wait on work only that client thread can advance. #4337 changes how the wait decides it is finished, and it makes the current export's own terminal diagnostic safe by retiring before logging, but it neither introduces nor removes either shape above.

What would settle it

Two ways, and the choice is a design one.

Write the restriction down, as part of what LogHandler is: Handle() must not synchronously call ForceFlush, Shutdown or Export on a provider or exporter involved in producing that record. That costs nothing at runtime and makes the current behaviour a stated contract rather than an accident.

Or make it safe, which means a flush entered from an export or a callback of the same exporter has to fail fast rather than wait. That is per exporter machinery for a case the interface never promised, and it would have to be added to each one, so it is worth deciding whether the promise is wanted before anybody writes it.

I have not sent a patch. Which of the two you want decides whether this is a documentation change or a change to every exporter that blocks.

Found while working through the ForceFlush accounting in #4337.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing LogHandler::Handle through GlobalLogHandler, ElasticsearchLogRecordExporter::Export, Session::SendRequest, HttpOperation, SendAsync, and ForceFlush. Decide whether the issue should establish a LogHandler restriction or add exporter-specific fail-fast behavior; done means the chosen approach addresses both export-time and client-callback deadlock shapes, including the OTLP HTTP exporter.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
observability
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.