From 2ae69e0a061e7e604b7a3de780f971843d8d631d Mon Sep 17 00:00:00 2001 From: yulit0738 Date: Mon, 23 Mar 2026 14:29:48 +0900 Subject: [PATCH] Fix DagRun._emit_dagrun_span crash on None context_carrier DagRuns created before OTel tracing was enabled have context_carrier=NULL in the database. When these DagRuns complete, _emit_dagrun_span() passes None to TraceContextTextMapPropagator().extract(), which crashes with AttributeError: 'NoneType' object has no attribute 'get'. Use `self.context_carrier or {}` so OTel receives a valid carrier and emits a root span per the OTel spec, consistent with task_runner.py:148. Co-Authored-By: Claude Opus 4.6 (1M context) --- airflow-core/src/airflow/models/dagrun.py | 2 +- airflow-core/tests/unit/models/test_dagrun.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/models/dagrun.py b/airflow-core/src/airflow/models/dagrun.py index c37713da4d843..40318baf9c3f7 100644 --- a/airflow-core/src/airflow/models/dagrun.py +++ b/airflow-core/src/airflow/models/dagrun.py @@ -1020,7 +1020,7 @@ def is_effective_leaf(task): return leaf_tis def _emit_dagrun_span(self, state: DagRunState): - ctx = TraceContextTextMapPropagator().extract(self.context_carrier) + ctx = TraceContextTextMapPropagator().extract(self.context_carrier or {}) span = trace.get_current_span(context=ctx) span_context = span.get_span_context() with override_ids(span_context.trace_id, span_context.span_id): diff --git a/airflow-core/tests/unit/models/test_dagrun.py b/airflow-core/tests/unit/models/test_dagrun.py index 850357b68c999..7ad78292a1edd 100644 --- a/airflow-core/tests/unit/models/test_dagrun.py +++ b/airflow-core/tests/unit/models/test_dagrun.py @@ -3518,3 +3518,42 @@ def test_emit_dagrun_span_attributes_and_status(self, dag_maker, session, final_ expected_status = StatusCode.OK if final_state == DagRunState.SUCCESS else StatusCode.ERROR assert span.status.status_code == expected_status + + @pytest.mark.parametrize("carrier_value", [None, {}]) + def test_emit_dagrun_span_with_none_or_empty_carrier(self, dag_maker, session, carrier_value): + """_emit_dagrun_span should emit a root span when context_carrier is None or empty. + + This happens for DagRuns created before OTel tracing was enabled, or whose + context_carrier was cleared/backfilled to NULL. Per OTel spec, missing context + results in a new root span rather than a crash. + """ + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + + from airflow._shared.observability.traces import OverrideableRandomIdGenerator + + in_mem_exporter = InMemorySpanExporter() + provider = TracerProvider(id_generator=OverrideableRandomIdGenerator()) + provider.add_span_processor(SimpleSpanProcessor(in_mem_exporter)) + test_tracer = provider.get_tracer("test") + + with dag_maker("test_tracing_none_carrier", session=session) as dag: + EmptyOperator(task_id="t1") + + dr = dag_maker.create_dagrun(state=DagRunState.RUNNING) + ti = dr.get_task_instance("t1", session=session) + ti.state = TaskInstanceState.SUCCESS + session.flush() + dr.dag = dag + + # Simulate a DagRun with missing context_carrier + dr.context_carrier = carrier_value + + with mock.patch("airflow.models.dagrun.tracer", test_tracer): + dr.update_state(session=session) + + # A root span should still be emitted + spans = in_mem_exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].name == f"dag_run.{dr.dag_id}"