Skip to content

Commit 7e59ab9

Browse files
prepare-sync: clear active-span ContextVar after detach
PR-C.3 review fixup. The cleared spec lifecycle reasoning at the coord thread covered only the happy path: "ContextVar gets overwritten on the next prepare_sync." If a subsequent prepare_sync raises or early-returns without publishing — for any reason — the engine reads the previous node's span and attaches it around the new node's body, producing wrong log correlation. Bound the "ContextVar is set" window to the node-body scope by clearing it to None in innermost's finally right after the OTel detach (both _step_function_node's and _step_fan_out_node's paths). Between dispatches and during merge / completed-event dispatch the ContextVar is now None, so a failing or early-returning prepare_sync can't reveal a stale span when the engine reads. Lifecycle ownership stays with the attach/detach scope rather than fanning out across observers in _dispatch. Updated current_active_observer_span's docstring to reflect the narrower lifecycle.
1 parent 21e1fc7 commit 7e59ab9

2 files changed

Lines changed: 19 additions & 2 deletions

File tree

src/openarmature/graph/compiled.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
_reset_invocation_id,
6565
_reset_namespace_prefix,
6666
_set_active_dispatch,
67+
_set_active_observer_span,
6768
_set_active_observers,
6869
_set_attempt_index,
6970
_set_correlation_id,
@@ -744,7 +745,11 @@ async def innermost(s: Any) -> Mapping[str, Any]:
744745
# the node body — before any ``await`` — pick up the
745746
# right trace_id/span_id via OTel's LoggingHandler.
746747
# Detach in ``finally`` so retries / merge / completed
747-
# dispatch don't run with the span still active.
748+
# dispatch don't run with the span still active, and
749+
# clear ``current_active_observer_span`` to ``None`` so
750+
# the next dispatch that raises or early-returns from
751+
# ``prepare_sync`` can't reveal this node's span as a
752+
# stale value to the engine's read.
748753
otel_token = _attach_active_observer_span()
749754
try:
750755
try:
@@ -763,6 +768,7 @@ async def innermost(s: Any) -> Mapping[str, Any]:
763768
raise
764769
finally:
765770
_detach_active_observer_span(otel_token)
771+
_set_active_observer_span(None)
766772

767773
try:
768774
merged = _merge_partial(s, partial, self.reducers, current)
@@ -1112,7 +1118,10 @@ async def innermost(s: Any) -> Mapping[str, Any]:
11121118
# trace_id/span_id. Per-instance bodies get their own
11131119
# attach inside their ``_step_function_node``
11141120
# innermost when the recursive invocation hits leaf
1115-
# nodes.
1121+
# nodes. ``finally`` clears the ContextVar so a later
1122+
# dispatch whose ``prepare_sync`` raises or early-
1123+
# returns can't reveal this fan-out's span as a stale
1124+
# value to the engine's read.
11161125
otel_token = _attach_active_observer_span()
11171126
try:
11181127
try:
@@ -1149,6 +1158,7 @@ async def innermost(s: Any) -> Mapping[str, Any]:
11491158
raise wrapped from e
11501159
finally:
11511160
_detach_active_observer_span(otel_token)
1161+
_set_active_observer_span(None)
11521162

11531163
try:
11541164
merged = _merge_partial(s, partial, self.reducers, current)

src/openarmature/observability/correlation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,13 @@ def current_active_observer_span() -> object | None:
330330
on the first line, before any ``await`` — pick up the span's
331331
trace_id/span_id via OTel's ``LoggingHandler``.
332332
333+
Lifecycle: the value is ``None`` outside a node-body scope (between
334+
dispatches, during merge, during completed-event dispatch). The
335+
engine's ``innermost`` clears it to ``None`` in its ``finally``
336+
block right after the OTel detach — so a subsequent
337+
``prepare_sync`` that raises or early-returns can't reveal a stale
338+
span from a previous node when the engine reads.
339+
333340
Backend coupling note: typed as ``object | None`` so this primitive
334341
works in installs without the ``[otel]`` extras. OTel observers
335342
write OpenTelemetry ``Span`` instances; the engine treats the

0 commit comments

Comments
 (0)