Skip to content

Fix none_failed_min_one_success trigger rule checks#67873

Open
nailo2c wants to merge 3 commits into
apache:mainfrom
nailo2c:fix-none-failed-min-one-success-trigger-rule
Open

Fix none_failed_min_one_success trigger rule checks#67873
nailo2c wants to merge 3 commits into
apache:mainfrom
nailo2c:fix-none-failed-min-one-success-trigger-rule

Conversation

@nailo2c
Copy link
Copy Markdown
Contributor

@nailo2c nailo2c commented Jun 1, 2026

Summary

none_failed_min_one_success requires all upstream tasks to avoid failed / upstream_failed and at least one upstream task to succeed.

The existing TriggerRuleDep dep-check path shared the same failure-count logic as none_failed. As a result, zero-success upstream states could be reported as dependencies met, even though the trigger rule's min-one-success contract could not be satisfied.

This showed up in two counterexamples:

  • all upstream tasks skipped while flag_upstream_failed=False, where direct dependency checks such as airflow tasks failed-deps missed the trigger-rule failure reason;
  • dynamically mapped upstreams where all upstream task instances were removed, where the scheduler rewrite path also did not assign a terminal failure state.

Before the fix, airflow tasks failed-deps did not report the trigger-rule violation for an all-skipped upstream case:

Reproduction Dag
import pendulum

from airflow.providers.standard.operators.empty import EmptyOperator
from airflow.providers.standard.operators.python import BranchPythonOperator
from airflow.sdk import DAG


def choose_branch():
    return "branch_b"


with DAG(
    dag_id="pattern5_repro",
    schedule=None,
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
    tags=["z3-counterexample"],
) as dag:
    pick = BranchPythonOperator(
        task_id="pick",
        python_callable=choose_branch,
    )

    branch_a = EmptyOperator(task_id="branch_a")
    branch_b = EmptyOperator(task_id="branch_b")

    downstream = EmptyOperator(
        task_id="downstream",
        trigger_rule="none_failed_min_one_success",
    )

    pick >> [branch_a, branch_b]
    branch_a >> downstream
[Breeze:3.10.20] root@57ce6ad76e64:/opt/airflow$ airflow tasks failed-deps pattern5_repro downstream manual__2026-05-31T23:57:32.573541+00:00
Task instance dependencies not met:
Dagrun Running: Task instance's dagrun was not in the 'running' state but in the state 'success'.
Task Instance State: Task is in the 'skipped' state.

After the fix, the missing trigger-rule dependency is reported:

[Breeze:3.10.20] root@57ce6ad76e64:/opt/airflow$ airflow tasks failed-deps pattern5_repro downstream manual__2026-05-31T23:57:32.573541+00:00
Task instance dependencies not met:
Trigger Rule: Task's trigger rule 'none_failed_min_one_success' requires at least one upstream task success, but none were found.
Dagrun Running: Task instance's dagrun was not in the 'running' state but in the state 'success'.
Task Instance State: Task is in the 'skipped' state.

Fix

Fail the none_failed_min_one_success dep check whenever no upstream task has succeeded.

Also extend the flag_upstream_failed=True rewrite path so that, once all upstreams are terminal and none succeeded, the downstream task instance is marked UPSTREAM_FAILED. This covers mapped tasks whose relevant upstream task instances were all removed.

The existing all-skipped behavior is preserved: that case is still rewritten to SKIPPED before the new all-terminal zero-success branch can run.

Test

Updated regression coverage for none_failed_min_one_success:

  • all-skipped upstreams now report the missing upstream success for both flag_upstream_failed=True and False;
  • all-removed mapped upstreams now report the missing upstream success, and with flag_upstream_failed=True the task instance is rewritten to UPSTREAM_FAILED.

Verified with:

uv run ruff format airflow-core/tests/unit/ti_deps/deps/test_trigger_rule_dep.py
uv run ruff check --fix airflow-core/tests/unit/ti_deps/deps/test_trigger_rule_dep.py
breeze run --backend postgres --python 3.10 --skip-image-upgrade-check --answer no pytest airflow-core/tests/unit/ti_deps/deps/test_trigger_rule_dep.py::TestTriggerRuleDep::test_none_failed_min_one_success_tr_skipped airflow-core/tests/unit/ti_deps/deps/test_trigger_rule_dep.py::TestTriggerRuleDep::test_mapped_task_upstream_all_removed_with_none_failed_min_one_success_trigger_rule -xvs

Result: 4 passed, 1 warning.

Reported by

SMT solver sweep of Airflow trigger-rule semantics using a local Z3 model.


Was generative AI tooling used to co-author this PR?
  • Yes - Codex (GPT-5)

Generated-by: Codex (GPT-5) following the guidelines

Comment thread airflow-core/newsfragments/67873.bugfix.rst Outdated
Copy link
Copy Markdown
Contributor

@Pedrinhonitz Pedrinhonitz left a comment

Choose a reason for hiding this comment

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

This is fine by me, we can move on to a member review.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants