Skip to content

Commit 884e687

Browse files
authored
Fix short-circuit inside a mapped task group not skipping downstream (#73959)
1 parent a91216d commit 884e687

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

‎airflow-core/src/airflow/ti_deps/deps/not_previously_skipped_dep.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ def _get_dep_statuses(self, ti, dep_context, *, session):
6060

6161
# Use the parent's map context to look up the XCom. An unmapped parent
6262
# (e.g. LatestOnlyOperator) writes XCom with map_index=-1, so we must
63-
# query with -1 instead of the child's map_index.
64-
xcom_map_index = ti.map_index if parent.is_mapped else -1
63+
# query with -1 instead of the child's map_index. A parent inside a
64+
# mapped task group is expanded like a mapped task, so it writes XCom
65+
# per map index even though ``is_mapped`` is False.
66+
xcom_map_index = ti.map_index if parent.get_needs_expansion() else -1
6567
prev_result = ti.xcom_pull(
6668
task_ids=parent.task_id,
6769
key=XCOM_SKIPMIXIN_KEY,

‎airflow-core/tests/unit/ti_deps/deps/test_not_previously_skipped_dep.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
from airflow.models.xcom import XComModel
2828
from airflow.providers.standard.operators.empty import EmptyOperator
2929
from airflow.providers.standard.operators.python import BranchPythonOperator
30+
from airflow.sdk import task, task_group
3031
from airflow.sdk.bases.xcom import BaseXCom
3132
from airflow.ti_deps.dep_context import DepContext
3233
from airflow.ti_deps.deps.not_previously_skipped_dep import (
3334
XCOM_SKIPMIXIN_FOLLOWED,
3435
XCOM_SKIPMIXIN_KEY,
36+
XCOM_SKIPMIXIN_SKIPPED,
3537
NotPreviouslySkippedDep,
3638
)
3739
from airflow.utils.state import State
@@ -219,6 +221,48 @@ def test_unmapped_parent_skip_mapped_downstream(session, dag_maker):
219221
assert tis["op2"].state == State.SKIPPED
220222

221223

224+
def test_parent_in_mapped_task_group_skips_same_map_index(session, dag_maker):
225+
"""
226+
A SkipMixin parent inside a mapped task group writes XCom per map index, so
227+
each child TI in the group must read the decision for its own map index.
228+
"""
229+
with dag_maker("test_mapped_group_skip_dag", schedule=None, session=session):
230+
231+
@task.short_circuit(task_id="gate")
232+
def gate(value):
233+
return value
234+
235+
@task_group
236+
def group(value):
237+
gate(value) >> EmptyOperator(task_id="child")
238+
239+
group.expand(value=[True, False])
240+
241+
dr = dag_maker.create_dagrun(run_type=DagRunType.MANUAL, state=State.RUNNING)
242+
tis = {(ti.task_id, ti.map_index): ti for ti in dr.task_instances}
243+
for map_index in (0, 1):
244+
tis[("group.gate", map_index)].state = State.SUCCESS
245+
session.merge(tis[("group.gate", map_index)])
246+
# Only the map index 1 gate short-circuited, as SkipMixin.skip records it.
247+
XComModel.set(
248+
key=XCOM_SKIPMIXIN_KEY,
249+
value={XCOM_SKIPMIXIN_SKIPPED: ["group.child"]},
250+
dag_id=dr.dag_id,
251+
task_id="group.gate",
252+
run_id=dr.run_id,
253+
map_index=1,
254+
session=session,
255+
)
256+
session.flush()
257+
258+
dep = NotPreviouslySkippedDep()
259+
260+
assert not dep.is_met(tis[("group.child", 1)], session=session)
261+
assert tis[("group.child", 1)].state == State.SKIPPED
262+
assert dep.is_met(tis[("group.child", 0)], session=session)
263+
assert tis[("group.child", 0)].state != State.SKIPPED
264+
265+
222266
def test_branch_skip_decision_bypasses_custom_xcom_backend(session, dag_maker):
223267
"""
224268
A value-externalizing custom XCom backend must not break branch-skip of

0 commit comments

Comments
 (0)