Skip to content

Concurrent multi-processors never flush at interpreter exit - #5569

Open
dwin-gharibi wants to merge 2 commits into
open-telemetry:mainfrom
dwin-gharibi:fix/concurrent-processor-shutdown-at-exit
Open

Concurrent multi-processors never flush at interpreter exit#5569
dwin-gharibi wants to merge 2 commits into
open-telemetry:mainfrom
dwin-gharibi:fix/concurrent-processor-shutdown-at-exit

Conversation

@dwin-gharibi

Copy link
Copy Markdown

Closes #5568.

Description

TracerProvider and LoggerProvider register their shutdown with atexit.register(). concurrent.futures registers its cleanup through threading._register_atexit(), and CPython runs threading._shutdown() before the atexit queue.

So by the time the provider's shutdown runs, the thread pool is already closed. _submit_and_await raises RuntimeError: cannot schedule new futures after shutdown, and the underlying BatchSpanProcessor / BatchLogRecordProcessor is never shut down - its buffered telemetry is simply lost.

Root cause

  • opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py:272 (_submit_and_await) and :308 (force_flush)
  • opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py:470 (_submit_and_wait) and :494 (force_flush)

Each submits unconditionally to a ThreadPoolExecutor that is guaranteed to be closed by the time an atexit-driven shutdown reaches it.

Approach

Catch the RuntimeError from submit and run the callable inline on the calling thread. During interpreter shutdown there is no concurrency to preserve anyway - the point of the pool is parallelism across child processors, and correctness matters more than that here.

force_flush takes the same treatment, propagating a False result from an inline call so the return value stays meaningful.

Deliberately narrow: nothing changes while the pool is healthy, which the control test pins down.

Files changed

  • opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
  • opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py
  • opentelemetry-sdk/tests/test_concurrent_processor_at_exit.py
  • .changelog/5568.fixed

Testing

The defect only appears at genuine interpreter exit, so the primary tests drive a real subprocess for each signal and assert both that the telemetry was exported and that no RuntimeError was reported on stderr.

Unit tests close the executor explicitly and assert shutdown and force_flush still reach the child processor for both the span and log variants. A control test asserts a healthy pool is still used, so the fallback cannot quietly become the only path.

Eight of the nine fail before the change.

Result: 864 passed in opentelemetry-sdk (855 baseline plus 9 new).

Risk / compatibility

The inline fallback only triggers once the pool refuses work, which in practice means interpreter shutdown. Behaviour during normal operation is unchanged. Inline execution is sequential rather than parallel, which is the correct trade at that point in the process lifecycle.

The providers register shutdown with atexit, but concurrent.futures registers
its cleanup through threading._register_atexit and CPython runs
threading._shutdown() before the atexit queue, so the pool is already closed
when the provider's shutdown runs.

Drive both signals in a subprocess, since the defect only appears at real
interpreter exit, and assert the telemetry is exported and no RuntimeError is
reported. Unit tests close the pool explicitly and assert the work still runs
inline, with a control asserting a healthy pool is unaffected.

These tests fail against the current implementation.
TracerProvider and LoggerProvider register their shutdown with atexit, but
concurrent.futures registers its own cleanup through
threading._register_atexit and CPython runs threading._shutdown() before the
atexit queue. The thread pool is therefore always closed by the time an
atexit-driven provider shutdown reaches it, so submitting raised
"RuntimeError: cannot schedule new futures after shutdown" and the underlying
batch processor was never shut down. Every buffered span and log record was
lost on every clean exit, reported only as an ignored atexit error.

Fall back to running the callable inline when the pool refuses work. There is
no concurrency worth preserving at that point in the process lifecycle.
force_flush gets the same treatment and propagates a False result from an
inline call.
@dwin-gharibi
dwin-gharibi requested a review from a team as a code owner August 23, 2026 16:16
Copilot AI lite review requested due to automatic review settings August 23, 2026 16:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@opentelemetry-pr-dashboard

Copy link
Copy Markdown

Pull request dashboard status

Waiting on reviewers · refreshed 2026-08-23 16:17 UTC

Review the latest changes.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Concurrent multi-processors never flush at interpreter exit

2 participants