22
33import asyncio
44import logging
5+ import sys
56import uuid
67from concurrent .futures import ThreadPoolExecutor
78from dataclasses import dataclass
2122from temporalio .exceptions import ApplicationError , ApplicationErrorCategory
2223from temporalio .testing import WorkflowEnvironment
2324from temporalio .worker import UnsandboxedWorkflowRunner , Worker
25+ from tests .worker .test_workflow import (
26+ CacheEvictionTearDownWorkflow ,
27+ WaitForeverWorkflow ,
28+ wait_forever_activity ,
29+ )
2430
2531# Passing through because Python 3.9 has an import bug at
2632# https://github.com/python/cpython/issues/91351
@@ -321,7 +327,10 @@ def dump_spans(
321327 span_links : List [str ] = []
322328 for link in span .links :
323329 for link_span in spans :
324- if link_span .context .span_id == link .context .span_id :
330+ if (
331+ link_span .context is not None
332+ and link_span .context .span_id == link .context .span_id
333+ ):
325334 span_links .append (link_span .name )
326335 span_str += f" (links: { ', ' .join (span_links )} )"
327336 # Signals can duplicate in rare situations, so we make sure not to
@@ -331,7 +340,7 @@ def dump_spans(
331340 ret .append (span_str )
332341 ret += dump_spans (
333342 spans ,
334- parent_id = span .context .span_id ,
343+ parent_id = span .context .span_id if span . context else None ,
335344 with_attributes = with_attributes ,
336345 indent_depth = indent_depth + 1 ,
337346 )
@@ -448,3 +457,68 @@ async def test_opentelemetry_benign_exception(client: Client):
448457# * workflow failure and wft failure
449458# * signal with start
450459# * signal failure and wft failure from signal
460+
461+
462+ async def test_opentelemetry_safe_detach (client : Client ):
463+ # This test simulates forcing eviction. This purposely raises GeneratorExit on
464+ # GC which triggers the finally which could run on any thread Python
465+ # chooses. When this occurs, we should not detach the token from the context
466+ # b/c the context no longer exists
467+
468+ # Create a tracer that has an in-memory exporter
469+ exporter = InMemorySpanExporter ()
470+ provider = TracerProvider ()
471+ provider .add_span_processor (SimpleSpanProcessor (exporter ))
472+ tracer = get_tracer (__name__ , tracer_provider = provider )
473+
474+ class _OtelLogSpy (logging .Handler ):
475+ def __init__ (self , level : int | str = 0 ) -> None :
476+ self .seenOtelFailedMessage = False
477+ super ().__init__ (level )
478+
479+ def emit (self , record : logging .LogRecord ) -> None :
480+ if not self .seenOtelFailedMessage :
481+ self .seenOtelFailedMessage = (
482+ record .levelno == logging .ERROR
483+ and record .name == "opentelemetry.context"
484+ and record .message == "Failed to detach context"
485+ )
486+
487+ async with Worker (
488+ client ,
489+ workflows = [CacheEvictionTearDownWorkflow , WaitForeverWorkflow ],
490+ activities = [wait_forever_activity ],
491+ max_cached_workflows = 0 ,
492+ task_queue = f"task_queue_{ uuid .uuid4 ()} " ,
493+ disable_safe_workflow_eviction = True ,
494+ interceptors = [TracingInterceptor (tracer )],
495+ ) as worker :
496+ # Put a hook to catch unraisable exceptions
497+ old_hook = sys .unraisablehook
498+ hook_calls : List [sys .UnraisableHookArgs ] = []
499+ sys .unraisablehook = hook_calls .append
500+ log_spy = _OtelLogSpy ()
501+ logging .getLogger ().addHandler (log_spy )
502+ try :
503+ handle = await client .start_workflow (
504+ CacheEvictionTearDownWorkflow .run ,
505+ id = f"wf-{ uuid .uuid4 ()} " ,
506+ task_queue = worker .task_queue ,
507+ )
508+
509+ # CacheEvictionTearDownWorkflow requires 3 signals to be sent
510+ await handle .signal (CacheEvictionTearDownWorkflow .signal )
511+ await handle .signal (CacheEvictionTearDownWorkflow .signal )
512+ await handle .signal (CacheEvictionTearDownWorkflow .signal )
513+
514+ await handle .result ()
515+ finally :
516+ sys .unraisablehook = old_hook
517+ logging .getLogger ().removeHandler (log_spy )
518+
519+ # Confirm at least 1 exception
520+ assert hook_calls
521+
522+ assert (
523+ not log_spy .seenOtelFailedMessage
524+ ), "Detach from context message should not be logged"
0 commit comments