Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f1070b3
Parse coordinator-claimed Dag files with their runtime in the Dag pro…
jason810496 Oct 1, 2026
c8a3306
Report a Lang-SDK parse past its import timeout as an import error
jason810496 Sep 30, 2026
bbea648
Report a native Dag's task that reaches a Python worker
jason810496 Sep 30, 2026
18de8bf
Drop callbacks for Lang-SDK Dag files before queuing the file
jason810496 Oct 2, 2026
a635923
Start each Dag file processor with explicit arguments
jason810496 Oct 2, 2026
17f65af
Find the claiming coordinator in the Task SDK and wrap it in core
jason810496 Oct 2, 2026
2d6dd21
Fail a native Dag's task on a Python worker without retries
jason810496 Oct 2, 2026
1169dbb
Match the schema error's JSON path in a Lang-SDK parse test
jason810496 Oct 2, 2026
303b0be
Kill what a Lang-SDK runtime leaves in its process group
jason810496 Oct 2, 2026
19810c7
Name the setting in a Lang-SDK parse timeout error
jason810496 Oct 2, 2026
b5ed506
Spec the mocks of a hand-built Lang-SDK processor in tests
jason810496 Oct 2, 2026
58c6665
Apply the multi-team task checks to Lang-SDK Dags
jason810496 Oct 2, 2026
913e536
Clear import errors after the native-file Dag bag sync test
jason810496 Oct 2, 2026
22cd5aa
Note in ADR-0010 that the manager routes claimed files for now
jason810496 Oct 2, 2026
e434a39
Move the client-less Lang-SDK parse out of this change
jason810496 Oct 2, 2026
0e680a3
Wait for the killed leftover in the Lang-SDK processor test
jason810496 Oct 2, 2026
eb25849
Find a native Dag's coordinator key through its importer
jason810496 Oct 2, 2026
4ad0614
Pass a parse log handle to the Lang-SDK processor in tests
jason810496 Oct 2, 2026
435e639
Stub Dag file discovery with DagFileInfo in the Lang-SDK manager tests
jason810496 Oct 2, 2026
e62a481
Name the coordinator class a native Dag task must be routed to
jason810496 Oct 3, 2026
18744e8
Find a claiming Dag importer instead of its coordinator in Dag proces…
jason810496 Oct 3, 2026
302b4e0
Test the import errors of a bundle with several coordinators of a class
jason810496 Oct 3, 2026
9007b91
Apply the team rules to the Dag that validation returns
jason810496 Oct 3, 2026
cb9d1e9
Catch a runtime's dead connection when its parse request is sent
jason810496 Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions airflow-core/adr/lang-sdk/0010-native-dag-processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ DagModelOperation → PERSIST

`DagImportResult.dags` is `list[DAG]`, so the importer wraps each serialized entry as a `LazyDeserializedDAG` and transforms it into an `airflow.sdk.DAG`.

### Interim: the Dag processor manager routes claimed files

Until importers own their parse process (#73457), the Dag processor manager routes a coordinator-claimed file itself, as of #74035. When a file's importer in the bundle's
registry is a coordinator's, the manager starts `LangSDKDagFileProcessorProcess` for it instead of `DagFileProcessorProcess`. That process execs the runtime, which connects
back to it. The importer's `import_definition` only reports that the Dag processor parses the file, which is what a Dag bag outside the Dag processor sees.

This is a bridge, not the end state. Running the runtime inside the Python parse child would need that child to relay every request and reply between the runtime and the
manager over its fd 0. Starting the runtime from the manager lets it reach the manager's request handlers directly, with no relay.

## Consequences

- The Java and TypeScript importers are never configured by hand. The runtime is declared once, in `[sdk] coordinators`, and the importer follows from it.
Expand Down
62 changes: 32 additions & 30 deletions airflow-core/src/airflow/dag_processing/dagbag.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from airflow._shared.timezones import timezone
from airflow.configuration import conf
from airflow.dag_processing.bundles.local import LocalDagBundle
from airflow.dag_processing.importer_routing import get_claiming_importer
from airflow.exceptions import (
AirflowClusterPolicyError,
AirflowClusterPolicySkipDag,
Expand All @@ -57,6 +58,7 @@
from airflow import DAG
from airflow.models.dagwarning import DagWarning
from airflow.sdk.importers import AbstractDagImporter, DagDefinition, DagImportWarning, DagSourceCode
from airflow.serialization.definitions.dag import SerializedDAG


class FileLoadStat(NamedTuple):
Expand Down Expand Up @@ -103,27 +105,24 @@ def _executor_exists(executor_name: str, team_name: str | None) -> bool:
return False


def _validate_executor_fields(dag: DAG, bundle_name: str | None = None) -> None:
def _get_bundle_team_name(bundle_name: str | None) -> str | None:
"""Return the team that owns *bundle_name* when multi-team is on, else ``None``."""
if not conf.getboolean("core", "multi_team") or not bundle_name:
return None
from airflow.dag_processing.bundles.manager import DagBundlesManager

return DagBundlesManager()._bundle_config[bundle_name].team_name


def _validate_executor_fields(dag: DAG | SerializedDAG, bundle_name: str | None = None) -> None:
"""Validate that executors specified in tasks are available and owned by the same team as the dag bundle."""
import logging

log = logging.getLogger(__name__)
dag_team_name = None

# Check if multi team is available by reading the multi_team configuration (which is boolean)
if conf.getboolean("core", "multi_team"):
# Get team name from bundle configuration if available
if bundle_name:
from airflow.dag_processing.bundles.manager import DagBundlesManager

bundle_manager = DagBundlesManager()
bundle_config = bundle_manager._bundle_config[bundle_name]

dag_team_name = bundle_config.team_name
if dag_team_name:
log.debug(
"Found team '%s' for DAG '%s' via bundle '%s'", dag_team_name, dag.dag_id, bundle_name
)
dag_team_name = _get_bundle_team_name(bundle_name)
if dag_team_name:
log.debug("Found team '%s' for DAG '%s' via bundle '%s'", dag_team_name, dag.dag_id, bundle_name)

for task in dag.tasks:
if not task.executor:
Expand All @@ -150,18 +149,7 @@ def _assign_default_team_pools(
bundle_name: str | None = None,
) -> None:
"""Assign the default team pool to tasks that do not explicitly specify a pool."""
dag_team_name = None

if conf.getboolean("core", "multi_team"):
if bundle_name:
from airflow.dag_processing.bundles.manager import DagBundlesManager

bundle_manager = DagBundlesManager()
bundle_config = bundle_manager._bundle_config[bundle_name]

dag_team_name = bundle_config.team_name

if not dag_team_name:
if not (dag_team_name := _get_bundle_team_name(bundle_name)):
return

for task in dag.tasks:
Expand Down Expand Up @@ -583,16 +571,30 @@ def sync_bag_to_db(
version_data: dict[str, Any] | None = None,
session: Session = NEW_SESSION,
) -> None:
"""Save attributes about list of DAG to the DB."""
"""
Save attributes about list of DAG to the DB.

Files that a Lang-SDK runtime parses are left out, with their import errors: the Dag processor
stores those.
"""
from airflow.dag_processing.collection import update_dag_parsing_results_in_db

import_errors = {(bundle_name, rel_path): error for rel_path, error in dagbag.import_errors.items()}
def is_parsed_by_runtime(rel_path: str) -> bool:
return get_claiming_importer(Path(dagbag.bundle_path or "", rel_path), bundle_name) is not None

import_errors = {
(bundle_name, rel_path): error
for rel_path, error in dagbag.import_errors.items()
if not is_parsed_by_runtime(rel_path)
}

# Build the set of all files that were parsed and include files with import errors
# in case they are not in parsed_definitions
files_parsed = set(import_errors)
if dagbag.bundle_path:
for rel_path in dagbag.parsed_definitions:
if is_parsed_by_runtime(rel_path):
continue
files_parsed.add((bundle_name, rel_path))
# A definition nested in an archive also clears the archive's own discovery errors.
if enclosing_file := find_enclosing_file(Path(dagbag.bundle_path, rel_path)):
Expand Down
47 changes: 47 additions & 0 deletions airflow-core/src/airflow/dag_processing/importer_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Route Dag files to the process that parses them, by the bundle's Dag importer registry."""

from __future__ import annotations

import logging
import os
from typing import TYPE_CHECKING

from airflow.sdk.coordinators._dag_importer import find_claiming_importer

if TYPE_CHECKING:
from airflow.sdk.coordinators._dag_importer import CoordinatorDagImporter # noqa: SDK001

log = logging.getLogger(__name__)


def get_claiming_importer(
path: str | os.PathLike[str], bundle_name: str | None
) -> CoordinatorDagImporter | None:
"""
Return the coordinator Dag importer that claims ``path``, or ``None`` when a Python child parses it.

A runtime parses the file when its importer is a coordinator's Dag importer. When the bundle's
importers cannot be loaded, the error is logged and a Python child parses the file.
"""
try:
return find_claiming_importer(path, bundle_name)
except Exception:
log.exception("Cannot load the Dag importer for %s in bundle %s", path, bundle_name)
return None
Loading