Skip to content

Emit parse-time DAG warnings for partial dynamic factory failures - #75673

Open
ydsakshi wants to merge 28 commits into
apache:mainfrom
ydsakshi:dag_failure_warning_emit
Open

ydsakshi wants to merge 28 commits into
apache:mainfrom
ydsakshi:dag_failure_warning_emit

Conversation

@ydsakshi

@ydsakshi ydsakshi commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Description

Addresses #29275.

Partial failures in dynamic DAG factories/files can call emit_dag_warning() during file parse. The DAG processor takes those warnings on DagFileParsingResult & stores them in dag_warning table (with columns - bundle_name & filename) & exposes them via GET /api/v2/dagWarnings & a Dashboard parse-failure card.

Reproduced the issue using the below sample script on main & then verified the fix on feature branch.

Evidence

Sample dynamic DAG creator file :-

from __future__ import annotations

import logging
from datetime import datetime
import traceback


from airflow.sdk import dag, task
#from airflow.sdk.definitions.parse_warning import emit_dag_warning

log = logging.getLogger(__name__)

FAILING_DAG_IDS = {"B", "E"}

for dag_id in "ABCDE":
    try:
        if dag_id in FAILING_DAG_IDS:
            raise RuntimeError(f"Failed to create dynamic DAG {dag_id}")

        @dag(dag_id=f"repro_29275_{dag_id}", schedule=None, start_date=datetime(2023, 1, 31), catchup=False)
        def dynamic_dag():
            @task
            def t():
                print(f"inside task {dag_id}")

            t()

        dynamic_dag()
    except Exception:
        #emit_dag_warning(
        #    f"repro_{dag_id}", f"Failed to create dynamic DAG {dag_id}", stacktrace=traceback.format_exc()
        #)
        log.exception("Failed to create dynamic DAG %s", dag_id)
Scenario Evidence
Correct Dags(A,C,D) Load (main) Screenshot From 2026-10-25 16-22-16
Correct Dags(A,C,D) Load (feature) Screenshot From 2026-10-26 21-13-55
__ __
Dashboard UI(main) - No warnings for failing DAGs(B,E) Screenshot From 2026-10-26 11-58-57
Dashboard UI(feature) - Proper warnings(with exceptions) for failing DAGs(B,E) Screenshot From 2026-10-26 21-14-33, Screenshot From 2026-10-26 21-14-59

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Not generated by AI, used Cursor for approach & design finalization. Code is fully vetted by me.


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@ydsakshi ydsakshi left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local testing & Evidences attached ✅
Self Review ✅
Local Pre-commit checks ✅
Have highlighted auto generated files.

Comment thread ts-sdk/schema/supervisor-schema.json Resolved
Comment thread go-sdk/schema/supervisor-schema.json Resolved
Comment thread ts-sdk/src/generated/supervisor.ts Resolved
Comment thread airflow-core/docs/migrations-ref.rst Resolved
Comment thread airflow-core/src/airflow/utils/db.py Resolved

@FrankYang0529 FrankYang0529 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. Leave some comments.

@shahar1
shahar1 requested review from uranusjr and removed request for FrankYang0529 October 31, 2026 20:31

@shahar1 shahar1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution!
The idea looks solid overall, the PR should be polished a bit more.
Besides, I think it's worth to post a discussion followed by lazy consensus on the dev list, since it involves migrations and schema changes (I wouldn't write a full AIP for that).

@uranusjr - you might want to take a look as well at some point.

session.execute(
delete(DagWarning).where(
tuple_(DagWarning.bundle_name, DagWarning.filename).in_(files_parsed),
DagWarning.warning_type == PARSE_FAILURE_WARNING_TYPE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. emit_dag_warning() accepts a custom warning_type, but the cleanup functions, the access filter and the Dashboard card only look at PARSE_FAILURE_WARNING_TYPE`.

  2. The cleanup for inactive Dags also skips
    these rows, so a warning with a custom type stays in the table forever.

)
return statement.where(
or_(
DagWarning.dag_id.in_(authorized),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyone who can read one Dag in the file sees the stacktrace. For import errors from the same file, the API hides
the stacktrace unless the user can read all Dags in it. If no Dag in the file loaded, nobody can see the warning.

)


class AddParseWarningsToDagFileParsingResult(VersionChange):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add a docstring? (otherwise description below is None)

)

self._process_modules(modules, result, bundle=bundle)
from airflow.sdk.definitions.parse_warning import drain_parse_warnings

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you extract the import to the top-level of this module?

),
Index("idx_dag_warning_dag_id", dag_id),
bundle_name: Mapped[str | None] = mapped_column(StringID(), nullable=True)
filename: Mapped[str | None] = mapped_column(String(1024), nullable=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every parse now runs an extra DELETE on (bundle_name, filename), and neither column is indexed. The PR raised the
statement counts in test_manager.py to fit it.

_DEFAULT_WARNING_TYPE = "user:dynamic_dag_failure"


def emit_dag_warning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no tests for task-sdk at all - this one needs to be well-covered, since it's the essence of this PR.

session.merge(warning_to_add)


def dag_parse_failure_warnings(bundle_name: str, warnings_by_rel_path: dict[str, list]) -> list[DagWarning]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please rename the function so it would start with a verb?

Comment on lines +404 to +408
def _update_file_scoped_parse_failure_warnings(
files_parsed: set[tuple[str, str]],
parse_failure_warnings: Iterable[DagWarning],
session: Session,
) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: session should be keyword-only argument

Suggested change
def _update_file_scoped_parse_failure_warnings(
files_parsed: set[tuple[str, str]],
parse_failure_warnings: Iterable[DagWarning],
session: Session,
) -> None:
def _update_file_scoped_parse_failure_warnings(
*,
files_parsed: set[tuple[str, str]],
parse_failure_warnings: Iterable[DagWarning],
session: Session,
) -> None:


import { useAutoRefresh } from "src/utils";

const PARSE_FAILURE_WARNING_TYPE = "user:dynamic_dag_failure";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You repeat the literal user:dynamic_dag_failure few times in the code rather importing it from a single source of truth, please consider changing that where it's not necessary to repeat.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants