anzinmhd opened a new pull request, #76323:
URL: https://github.com/apache/airflow/pull/76323
When a mapped task instance fails in a Dag with `fail_fast=True`, other
instances of the same mapped task can continue running or start afterward. This
PR identifies the triggering task instance by its unique execution ID so its
mapped siblings receive the existing fail-fast handling.
### Problem
Mapped task instances share a `task_id`, but represent separate executions.
For example:
- `mapped_work[0]` fails.
- `mapped_work[1]` is already running.
- `mapped_work[2]` is waiting for capacity.
With `fail_fast=True`, the running sibling should be marked failed and the
waiting sibling should be skipped.
Instead, the running sibling continues, and the waiting sibling can start
after the first instance fails. This allows additional work to execute despite
the Dag’s fail-fast setting.
### Cause
`_stop_remaining_tasks()` excludes the triggering instance using:
```python
ti.task_id == task_instance.task_id
```
Every mapped instance of the same task has that task ID. Consequently, the
condition excludes all mapped siblings from failure handling, rather than
excluding only the execution that triggered it.
Those siblings never reach the existing logic that marks running instances
failed and other unfinished instances skipped.
### Fix
Replace the task-definition ID comparison with a unique task-instance ID
comparison:
```python
ti.id == task_instance.id
```
This distinguishes the specific triggering execution from its mapped
siblings.
The helper now:
- Leaves the triggering instance’s failure transition to its caller.
- Processes other mapped instances, even though they share the same
`task_id`.
- Preserves already-successful and already-failed instances.
- Retains the existing teardown-task handling.
The identity check works for both mapped and unmapped instances, without
separate branches based on map index.
It also does not depend on the triggering instance already being marked
failed. This matters because the Execution API can invoke fail-fast handling
before executing that instance’s final state update.
The production change is limited to this comparison. No API schema, database
migration, or dependency changes are required.
### Reproduction
Use the following Dag with capacity for two concurrent task instances:
```python
import time
from airflow.sdk import DAG, task
with DAG(
dag_id="reproduce_fail_fast_mapped_siblings",
schedule=None,
catchup=False,
fail_fast=True,
max_active_tasks=2,
max_active_runs=1,
):
@task(retries=0)
def mapped_work(item: int) -> int:
if item == 0:
time.sleep(10)
raise ValueError("Intentional failure to exercise fail-fast
handling.")
time.sleep(120)
return item
mapped_work.expand(item=[0, 1, 2])
```
1. Unpause the Dag and trigger a new run.
2. Open the mapped task-instance list.
3. Confirm that indexes 0 and 1 are running while index 2 waits for capacity.
4. Wait for index 0 to fail after approximately 10 seconds.
5. Observe the remaining instances without manually changing their states.
**Before:** Index 1 continues running, and index 2 can start after index 0
fails.
**After:** Index 1 is marked failed and index 2 is skipped.
Use a new Dag run for each comparison. Sufficient executor and pool capacity
is needed to have indexes 0 and 1 running together before the intentional
failure.
### Version verification
- Reproduced with a live Dag on the local development `main` checkout.
- Before-and-after recordings demonstrate the failure and corrected behavior.
- Affected beta and released versions have not been independently
established.
### Tests and validation
Added eight database-backed regression cases:
- Six model-level cases covering running, scheduled, and queued mapped
siblings, with the triggering instance either still running or already failed.
- Two Execution API cases that send a failure update through the real
endpoint without mocking `_stop_remaining_tasks()`.
The model tests also verify that the triggering instance and
already-successful or already-failed mapped instances are preserved.
Validation completed:
- All eight new regression cases fail with the original task-ID comparison.
- All eight pass with the task-instance ID comparison.
- The latest focused verification run passed **41 tests**, including the new
cases and existing fail-fast, scheduler heartbeat-timeout, and retry coverage.
- Automated verification ran inside Breeze using isolated SQLite test
databases.
- Targeted Ruff formatting and lint checks pass.
- Live before-and-after reproduction completed.
The full CI and database-backend matrix have not been run locally.
### Before
Index 0 fails, but its mapped siblings continue executing. Index 2 can start
after the failure despite `fail_fast=True`.
https://github.com/user-attachments/assets/10971825-74c1-4856-9f64-cf7c09ba0b7f
### After
After index 0 fails, the running mapped sibling is marked failed and the
waiting mapped sibling is skipped.
https://github.com/user-attachments/assets/1471b774-b2f3-4a65-97da-37220dee0468
---
##### Was generative AI tooling used to co-author this PR?
- [X] Yes — Codex (GPT-6)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]