Skip to content

Commit c35472f

Browse files
committed
Report a native Dag's task that reaches a Python worker
A Python worker handed a task from a Dag file that a coordinator's Dag importer claims now exits with an error naming the queue to route. It checks before building a Dag bag, so it never starts the runtime.
1 parent 7543b47 commit c35472f

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

‎task-sdk/src/airflow/sdk/execution_time/task_runner.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from airflow.sdk.bases.skipmixin import XCOM_SKIPMIXIN_KEY
6161
from airflow.sdk.bases.xcom import BaseXCom
6262
from airflow.sdk.configuration import conf
63+
from airflow.sdk.coordinators._dag_importer import CoordinatorDagImporter
6364
from airflow.sdk.definitions._internal.dag_parsing_context import _airflow_parsing_context_manager
6465
from airflow.sdk.definitions._internal.types import NOTSET, ArgNotSet, is_arg_set
6566
from airflow.sdk.definitions.asset import (
@@ -150,6 +151,7 @@
150151
from airflow.sdk.execution_time.sentry import Sentry
151152
from airflow.sdk.execution_time.tracing import detail_span
152153
from airflow.sdk.execution_time.xcom import XCom
154+
from airflow.sdk.importers import get_importer_registry
153155
from airflow.sdk.listener import get_listener_manager
154156
from airflow.sdk.observability.metrics import stats_utils
155157
from airflow.sdk.serde import allow_class, iter_pydantic_models
@@ -1018,6 +1020,16 @@ def _register_deserialization_allowed_classes(dag, log: Logger) -> None:
10181020
)
10191021

10201022

1023+
def _is_lang_sdk_dag_file(path: str, bundle_name: str) -> bool:
1024+
"""Return whether a coordinator's Dag importer claims *path*, so that a Lang-SDK runtime parses it."""
1025+
try:
1026+
importer = get_importer_registry(bundle_name).get_importer(path)
1027+
except Exception:
1028+
# Building the Dag bag reports a broken importer configuration.
1029+
return False
1030+
return isinstance(importer, CoordinatorDagImporter)
1031+
1032+
10211033
@detail_span("parse")
10221034
def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance:
10231035
# TODO: Task-SDK:
@@ -1030,6 +1042,17 @@ def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance:
10301042
bundle_prepare_ms = int((time.monotonic() - bundle_prepare_start) * 1000)
10311043

10321044
dag_absolute_path = os.fspath(Path(bundle_instance.path, what.dag_rel_path))
1045+
if _is_lang_sdk_dag_file(dag_absolute_path, bundle_info.name):
1046+
log.error(
1047+
"A task of a native Lang-SDK Dag cannot run in Python. Route its queue to the coordinator "
1048+
"that parses the Dag, with [sdk] queue_to_coordinator",
1049+
dag_id=what.ti.dag_id,
1050+
task_id=what.ti.task_id,
1051+
queue=what.ti.queue,
1052+
path=what.dag_rel_path,
1053+
)
1054+
sys.exit(1)
1055+
10331056
dag_file_parse_start = time.monotonic()
10341057
bag = BundleDagBag(
10351058
dag_folder=dag_absolute_path,

‎task-sdk/tests/task_sdk/execution_time/test_task_runner.py‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from unittest import mock
3232
from unittest.mock import call, patch
3333

34+
import attrs
3435
import pandas as pd
3536
import pytest
3637
import structlog
@@ -78,6 +79,8 @@
7879
)
7980
from airflow.sdk.bases.operator import ExecutorSafeguard
8081
from airflow.sdk.bases.xcom import BaseXCom
82+
from airflow.sdk.coordinators._dag_importer import CoordinatorDagImporter
83+
from airflow.sdk.coordinators._subprocess import SubprocessCoordinator
8184
from airflow.sdk.definitions._internal.types import NOTSET, SET_DURING_EXECUTION, is_arg_set
8285
from airflow.sdk.definitions.asset import Asset, AssetAlias, AssetUniqueKey, AssetUriRef, Dataset, Model
8386
from airflow.sdk.definitions.param import DagParam
@@ -176,6 +179,7 @@
176179
TaskRunnerMarker,
177180
_defer_task,
178181
_execute_task,
182+
_is_lang_sdk_dag_file,
179183
_make_task_span,
180184
_push_xcom_if_needed,
181185
_register_deserialization_allowed_classes,
@@ -189,6 +193,7 @@
189193
startup,
190194
)
191195
from airflow.sdk.execution_time.xcom import XCom
196+
from airflow.sdk.importers import DagSourceCode, reset_importer_registry
192197
from airflow.sdk.serde import deserialize
193198
from airflow.triggers.base import BaseEventTrigger, BaseTrigger, TriggerEvent
194199
from airflow.triggers.callback import CallbackTrigger
@@ -902,6 +907,93 @@ def test_parse_module_in_bundle_root(tmp_path: Path, make_ti_context):
902907
assert ti.task.dag.dag_id == "dag_name"
903908

904909

910+
class NativeDagImporter(CoordinatorDagImporter):
911+
artifact_suffix = ".native"
912+
supported_extensions = [".native"]
913+
914+
def get_source_code(self, definition):
915+
return DagSourceCode(source_code=definition.read_text(), language="native")
916+
917+
918+
@attrs.define(kw_only=True)
919+
class NativeCoordinator(SubprocessCoordinator):
920+
"""A coordinator whose Dag importer claims ``.native`` files in every bundle."""
921+
922+
def get_dag_importer(self):
923+
return NativeDagImporter(coordinator=self)
924+
925+
926+
@patch("airflow.dag_processing.dagbag.BundleDagBag", autospec=True)
927+
def test_parse_rejects_a_task_of_a_native_dag(mock_bag, tmp_path: Path, make_ti_context):
928+
tmp_path.joinpath("dag.native").write_text("{}")
929+
what = StartupDetails(
930+
ti=TaskInstance(
931+
id=uuid7(),
932+
task_id="a",
933+
dag_id="native_dag",
934+
run_id="c",
935+
try_number=1,
936+
dag_version_id=uuid7(),
937+
queue="default",
938+
),
939+
dag_rel_path="dag.native",
940+
bundle_info=BundleInfo(name="my-bundle", version=None),
941+
ti_context=make_ti_context(),
942+
start_date=timezone.utcnow(),
943+
sentry_integration="",
944+
)
945+
bundle_config = [
946+
{
947+
"name": "my-bundle",
948+
"classpath": "airflow.dag_processing.bundles.local.LocalDagBundle",
949+
"kwargs": {"path": str(tmp_path), "refresh_interval": 1},
950+
}
951+
]
952+
coordinators = {"native": {"classpath": f"{__name__}.NativeCoordinator", "kwargs": {}}}
953+
log = mock.Mock()
954+
955+
reset_importer_registry()
956+
try:
957+
with (
958+
patch.dict(
959+
os.environ,
960+
{
961+
"AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST": json.dumps(bundle_config),
962+
"AIRFLOW__SDK__COORDINATORS": json.dumps(coordinators),
963+
},
964+
),
965+
pytest.raises(SystemExit, match="1"),
966+
):
967+
parse(what, log)
968+
finally:
969+
reset_importer_registry()
970+
971+
mock_bag.assert_not_called()
972+
log.error.assert_called_once_with(
973+
"A task of a native Lang-SDK Dag cannot run in Python. Route its queue to the coordinator "
974+
"that parses the Dag, with [sdk] queue_to_coordinator",
975+
dag_id="native_dag",
976+
task_id="a",
977+
queue="default",
978+
path="dag.native",
979+
)
980+
981+
982+
@pytest.mark.parametrize(
983+
("file_name", "expected"),
984+
[("dag.native", True), ("dag.py", False), ("dag.pyc", False), ("dags.zip", False)],
985+
)
986+
def test_is_lang_sdk_dag_file(file_name, expected):
987+
coordinators = {"native": {"classpath": f"{__name__}.NativeCoordinator", "kwargs": {}}}
988+
989+
reset_importer_registry()
990+
try:
991+
with patch.dict(os.environ, {"AIRFLOW__SDK__COORDINATORS": json.dumps(coordinators)}):
992+
assert _is_lang_sdk_dag_file(f"/bundle/{file_name}", "my-bundle") is expected
993+
finally:
994+
reset_importer_registry()
995+
996+
905997
@pytest.mark.parametrize("use_queues", [False, True])
906998
def test_run_deferred_basic(time_machine, create_runtime_ti, mock_supervisor_comms, use_queues: bool):
907999
"""Test that a task can transition to a deferred state."""

0 commit comments

Comments
 (0)