Skip to content

Stop spurious Failed to detach context errors from the OpenTelemetry interceptors - #1911

Merged
tconley1428 merged 6 commits into
mainfrom
otel/detach-on-attaching-thread
Oct 1, 2026
Merged

tconley1428 merged 6 commits into
mainfrom
otel/detach-on-attaching-thread

Conversation

@DABH

@DABH DABH commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What was changed

TracingInterceptor and OpenTelemetryInterceptor (the one OpenTelemetryPlugin installs) attach an OpenTelemetry context around workflow, query, update, activity, and Nexus handler code and detach it in a finally. "Context" means two different things in that sentence:

  • the OpenTelemetry Context: an immutable value holding the current span and baggage;
  • the contextvars.Context: the runtime container a thread or asyncio task runs in, which holds the current OpenTelemetry Context in a ContextVar.

opentelemetry.context.attach sets that ContextVar in the current container and returns a token; detach resets it with the token, which is only valid inside the container that created it. If the finally runs in another container, detach catches the ValueError and logs Failed to detach context at ERROR with a traceback. Nothing is wrong at that point: there is nothing to detach there. That happens when the context manager is finalized by garbage collection on another thread (an abandoned workflow coroutine, for example). #1174 added a guard for it, detach only if the attached Context value is still the current one, with test_opentelemetry_safe_detach asserting the log is gone. #1596 proposed a second guard for a copied container and was closed.

The guard compares the value, but validity depends on the container, and the two can disagree. OpenTelemetry's threading instrumentation, which strands enables for every Agent, propagates the same Context value into every thread started while it is current, so on such a thread the guard passes and the invalid detach is attempted. test_opentelemetry_safe_detach fails deterministically whenever a strands test ran earlier in the same xdist worker (10 of 13 macos-arm attempts on #1868), and strands + OpenTelemetryPlugin users get the same error in their logs. Since #1868 merged, both macos-arm lanes on main fail that test on every run (for example run 36895669970), together with test_opentelemetry_context_restored_after_activity, which counts process-global attach/detach calls and so also sees the instrumentation's per-thread pair.

The thread is not a usable test either (Brian's review): activations move between pool threads while the asyncio task keeps its container, and those detaches must happen. Only contextvars knows which container a token belongs to. So attached_context, a context manager replacing the seven try/finally sites, keeps the value check and then performs the same ContextVar.reset that opentelemetry.context.detach performs, ignoring the ValueError for a token from another container. Valid detaches are unchanged. The only behavior change is that the spurious error is no longer logged.

Testing

  • test_opentelemetry_safe_detach: interceptor × ThreadingInstrumentor matrix; the instrumented cases fail on main.
  • test_opentelemetry_detach_after_thread_change and test_opentelemetry_context_restored_after_activation_thread_change: enter on one thread, leave on another, both interceptors; fail with a thread-based guard.
  • test_opentelemetry_context_restored_after_activity now records the interceptors' own detach results instead of patching opentelemetry.context.attach/detach process-wide, so a thread started before the test and exiting during it no longer skews the count.
  • tests/contrib/opentelemetry 84 passed; poe lint clean.

The tracing interceptor's best-effort detach checked that the attached
Context was still current, so a generator finalized by GC on another
thread would skip the detach instead of using a token that is invalid
there. OpenTelemetry's threading instrumentation, which strands enables
whenever an Agent is created, copies the same Context object into new
threads, so that check passes on the wrong thread and detach logs
"Failed to detach context". Record the attaching thread with the token
and skip the detach anywhere else.

The regression test runs the existing safe-detach scenario with the
instrumentation active; it fails without this change.
@DABH
DABH requested a review from a team as a code owner October 1, 2026 05:58
@DABH
DABH requested a review from brianstrauch October 1, 2026 05:58
@DABH
DABH requested a balanced review from Copilot October 1, 2026 06:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The plugin interceptor remains vulnerable, and recyclable thread identifiers do not reliably establish thread identity.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds thread-aware OpenTelemetry context detachment to prevent foreign-thread token errors.

Changes:

  • Introduces shared context attach/detach helpers.
  • Adds threading-instrumentation regression coverage.
  • Documents the fix.
File Description
temporalio/​contrib/​opentelemetry/​_interceptor.py Adds thread-aware detachment.
tests/​contrib/​opentelemetry/​test_opentelemetry.py Adds regression coverage.
CHANGELOG.md Records the fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread temporalio/contrib/opentelemetry/_interceptor.py Outdated
OpenTelemetryPlugin installs OpenTelemetryInterceptor, whose four
attach/detach sites had the same context-identity guard. Move the helpers
into _context.py and use them from both interceptors. Compare the attaching
threading.Thread object rather than its id, which the OS can reuse. The
regression test is now a matrix over interceptor and threading
instrumentation; the OpenTelemetryInterceptor case fails without this.

@brianstrauch brianstrauch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Requesting changes for the P2 below: the thread identity guard skips valid context cleanup when workflow activations move between executor threads. All 80 existing OpenTelemetry tests passed, but a targeted workflow reproduction fails for both interceptors with the PR guard and passes with the original guard.

Comment thread temporalio/contrib/opentelemetry/_context.py Outdated
Workflow activations run on a thread pool while the asyncio task keeps its
contextvars.Context, so a context attached in one activation is legitimately
detached in a later one on another thread. The thread check skipped those
detaches and left the inner context attached for outer interceptors.

Let contextvars decide instead: perform the reset that
opentelemetry.context.detach performs and treat its ValueError for a foreign
Context as nothing to detach. Covers the thread change with a unit test on
both interceptors and a worker test whose executor alternates activations
between two threads.
@DABH DABH changed the title Detach OpenTelemetry context only on the thread that attached it Detach OpenTelemetry context only where its token is still valid Oct 1, 2026
@DABH
DABH requested a review from brianstrauch October 1, 2026 16:52
mypy rejects assigning to a method (method-assign); pytest's monkeypatch
does the same thing and restores it on teardown.


@dataclass(frozen=True)
class AttachedContext:

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.

This seems like a context manager.

@tconley1428

tconley1428 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

I don't totally follow. The description refers to "since" a PR that was never completed. And the title is incorrect, it is still detaching, just catching a theoretical failure to suppress a log. How is this doing anything about any test failures? If the goal is to not have that log statement, that seems to be the only thing it is accomplishing.

Edit: I see, the test which is checking for the log.

attached_context(context) replaces the seven attach/try/finally/detach
sites; the guarded detach moves to _detach, which the leak test counts.
@DABH DABH changed the title Detach OpenTelemetry context only where its token is still valid Stop spurious Failed to detach context errors from the OpenTelemetry interceptors Oct 1, 2026
The test patched opentelemetry.context.attach/detach process-wide, which
also counts the threading instrumentation's attach/detach around every
thread's run(). A pool thread started before the patch and exiting during
the test adds a detach with no counted attach (9 attaches vs 10 detaches
on 3.10 macos-arm). Record the interceptors' own detach results instead.
@tconley1428
tconley1428 merged commit df6f4ed into main Oct 1, 2026
46 of 49 checks passed
@tconley1428
tconley1428 deleted the otel/detach-on-attaching-thread branch October 1, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants