Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
875d8a5
refactor: Implemented Dynamic Task Iteration
dabla May 23, 2026
968aeb6
refactor: Renamed apply_upstream_relationship to register_with_dag
dabla May 23, 2026
dff7b43
refactor: Fixed MappedOperator serialization tests
dabla May 26, 2026
0b62a5b
Merge branch 'main' into feature/dynamic-task-iteration
dabla May 26, 2026
c1627eb
refactor: Make update of env variable thread-safe in _execute_task me…
dabla May 26, 2026
ce134e6
refactor: Refactored _run_tasks so it uses a while loop instead of re…
dabla May 26, 2026
fd63862
Merge branch 'main' into feature/dynamic-task-iteration
dabla May 26, 2026
2cd47b7
fix: Preserve partition_size through serialization for partitioned tasks
dabla May 26, 2026
995ae33
refactor: Updated Uv.lock file
dabla May 26, 2026
b88afe9
refactor: Reformatted TestPartitionedOperator
dabla May 27, 2026
f9753b7
refactor: Fixed assertions for TestPartitionedOperator
dabla May 28, 2026
5cfe75e
refactor: Fixed cartesian product issue in iter_values method of Dict…
dabla May 28, 2026
d5bed18
refactor: Clone the context before each indexed task execution to hav…
dabla May 29, 2026
85dee45
refactor: Make sure MappedIterableOperator doesn't retry itself
dabla May 29, 2026
a07a359
Merge branch 'main' into feature/dynamic-task-iteration
dabla May 29, 2026
c80e2ae
refactor: Extracted clone_context from IterableOperator as it should …
dabla May 29, 2026
50e9ed9
refactor: Fixed timeout resolution on IterableOperator
dabla May 29, 2026
705ef4f
refactor: Re-organized imports of expandinput
dabla May 29, 2026
cea8a25
refactor: Updated uv.lock file
dabla May 29, 2026
234d228
Merge branch 'main' into feature/dynamic-task-iteration
dabla May 29, 2026
01c4ad8
refactor: If DateTimeTrigger isn't available, then failed tasks won't…
dabla May 29, 2026
8204f49
refactor: Reformatted TestCurrentContext
dabla May 29, 2026
651238b
refactor: Reformatted TestIterableOperator
dabla May 29, 2026
501d058
refactor: Fixed assertions TestExpandInput
dabla May 29, 2026
5c8bfc9
refactor: Fixed TestPartitionedOperator
dabla May 29, 2026
ac7c530
refactor: Updated uv.lock file
dabla May 29, 2026
5453f06
Merge branch 'main' into feature/dynamic-task-iteration
dabla May 29, 2026
8347e83
refactor: Reformatted tests
dabla May 30, 2026
d0b9420
refactor: Refactored typing of optional DateTimeTrigger (e.g. from st…
dabla May 30, 2026
70cc977
refactor: Added retries setter for MappedIterableOperator to respect …
dabla May 30, 2026
1616fb4
refactor: Fixed typing of optional DateTimeTrigger import in Iterable…
dabla May 31, 2026
b118943
refactor: Improve iterable task execution with AsyncAwareExecutor and…
dabla Jun 1, 2026
39af3b8
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 2, 2026
933da36
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 2, 2026
fcfb3f4
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 2, 2026
b31cd47
refactor: Remove env variable mutation which isn't necessary anymore …
dabla Jun 4, 2026
bbdde66
refactor: Fix executor context manager blocking after map timeout
dabla Jun 4, 2026
dd4cbcb
refactor: Reformatted assert in test_execute_task_exports_context_var…
dabla Jun 5, 2026
5345f90
refactor: Do not deepcopy outlet_events in context
dabla Jun 5, 2026
f3a14a4
refactor: Fix context vars not accessible during task execution via g…
dabla Jun 5, 2026
20e5e04
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 5, 2026
eef635c
refactor: Fix context vars not accessible during task execution via g…
dabla Jun 5, 2026
f08ab36
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 6, 2026
4f588e6
refactor: Renamed PartitionedOperator to BatchedOperator
dabla Jun 6, 2026
b77dfc9
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 8, 2026
255b4ff
refactor: Make sure task_concurrency is validated and partial_kwargs …
dabla Jun 8, 2026
fc1a95b
refactor: Keep track of individual try_number per failed tasks in Ite…
dabla Jun 9, 2026
6698712
refactor: Make sure to also pop task_concurrency beside batch_size wh…
dabla Jun 9, 2026
bb5663a
refactor: Reformatted _run_tasks method in IterableOperator
dabla Jun 10, 2026
c6f7db7
refactor: IterableOperator must fail-fast on non-recoverable exceptions
dabla Jun 12, 2026
8a03f36
fix: Allow executor to shutdown gracefully without hanging
dabla Jun 12, 2026
2bbc479
refactor: Use suppress instead of try/except in shutdown method of As…
dabla Jun 13, 2026
a588578
refactor: Fixed calculation of reschedule_date in IterableOperator
dabla Jun 19, 2026
081e7b5
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 23, 2026
b0470c5
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 23, 2026
ded2c74
refactor: Added missing returns_dag_result property delegation on Bat…
dabla Jun 23, 2026
67ad4a3
refactor: Added task_retries property in IterableOperator
dabla Jun 25, 2026
a95f52a
refactor: Added batch_size property in MappedOperator and SerializedM…
dabla Jun 25, 2026
403f753
refactor: Added missing returns_dag_result property being passed to I…
dabla Jun 25, 2026
59fb1e0
refactor: Import SkipMixin from sdk
dabla Jun 25, 2026
2dc7d10
refactor: Check of task instance if an IndexedTaskInstance, if not th…
dabla Jun 25, 2026
bbb625f
refactor: Reorganized imports in _expand method of BatchedOperator
dabla Jun 25, 2026
b750376
refactor: Reformatted test_map_batch_size
dabla Jun 25, 2026
9e4fe20
refactor: Added returns_dag_result property on IterableOperator
dabla Jun 25, 2026
61148cb
refactor: Fixed typing of index for failed tasks dict as dict keys al…
dabla Jun 25, 2026
628a5c0
fix: Make sure the total number_of_tasks is always re-evaluated even …
dabla Jun 25, 2026
14c70ae
refactor: Length of XComIterable is now derived from ExpandInput inst…
dabla Jun 25, 2026
f1a085c
refactor: Fixed assertions in test TaskInstances
dabla Jun 26, 2026
69c73d5
refactor: Fixed test_iterate_literal_cross_product as get_parse_time_…
dabla Jun 26, 2026
ae00d95
refactor: Defined _length through attrs in ExpandInput
dabla Jun 26, 2026
35a44aa
refactor: Fixed assertions test_map_batch_size
dabla Jun 26, 2026
de6032e
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jun 29, 2026
b8c0bd7
refactor: Fixed assertions test_map_batch_size
dabla Jun 26, 2026
5d7a705
refactor: Fixed assertions test_execute_task_exports_context_vars_thr…
dabla Jun 26, 2026
8d6d984
refactor: Don't define _length via attrs so it doesn't get serialized.
dabla Jun 29, 2026
225b94f
Update task-sdk/src/airflow/sdk/definitions/_internal/expandinput.py
dabla Jun 30, 2026
ca6fe29
refactor: Removed more-itertools dependency from task-sdk
dabla Jun 30, 2026
f1167fc
refactor: Updated paths in codeowners for task-sdk related files
dabla Jun 30, 2026
84b0297
refactor: Refactored length calculation of BatchedExpandInput
dabla Jun 30, 2026
7c7ab61
refactor: Refactored set_current_context so it uses ContextVars for t…
dabla Jun 30, 2026
ce93835
refactor: Remove _length from attrs in ExpandInput to avoid serializa…
dabla Jul 1, 2026
d38e723
refactor: Added missing _resolve_size method in BatchedExpandInput
dabla Jul 2, 2026
7885929
refactor: Fixed logging in set_current_context
dabla Jul 2, 2026
2c78da7
refactor: Added missing task_concurrency parameter in partial interfa…
dabla Jul 2, 2026
cf0a71e
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jul 2, 2026
a5cae8a
refactor: Removed obsolete unit tests regarding async contextmanager …
dabla Jul 2, 2026
eca191a
refactor: Pass the rescheduled_date explicitly to the AirflowReschedu…
dabla Jul 2, 2026
2b87d85
refactor: Removed the dead asyncio.TimeoutError branch — it was never…
dabla Jul 2, 2026
6ec7491
refactor: Removed debug statement with result if raise is None in Ite…
dabla Jul 2, 2026
1c2fb52
refactor: Stream DictOfListsExpandInput.iter_values lazily without bu…
dabla Jul 2, 2026
1acfe8c
refactor: Renamed unused result to _result in _run_tasks of IterableO…
dabla Jul 2, 2026
37988c4
refactor: Fixed assertion of raised NotMapped exception in test_itera…
dabla Jul 2, 2026
579563f
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jul 3, 2026
242c684
refactor: Only allow batched task iteration if batch size is more tha…
dabla Jul 3, 2026
6adf5c3
refactor: Only allow integer for size of BatchedExpandInput and make …
dabla Jul 3, 2026
0cec164
refactor: Added missing setter for returns_dag_result in IterableOper…
dabla Jul 3, 2026
eb01d52
refactor: Fixed len calculation for BatchedExpandInput
dabla Jul 3, 2026
189ebb2
refactor: Removed obsolete filter on tasks in execute_failed_tasks me…
dabla Jul 3, 2026
a3d6472
refactor: Removed DecoratedDeferredAsyncOperator as deferred sensors …
dabla Jul 6, 2026
a4e3671
refactor: Updated docstring of IterableOperator
dabla Jul 6, 2026
9809027
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jul 6, 2026
a31540a
refactor: Added note regarding deferred operators in docstring of Ite…
dabla Jul 6, 2026
7097f76
refactor: Added unit test when iterating over deferred operator
dabla Jul 6, 2026
5ea4dcf
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jul 6, 2026
0976ee9
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jul 9, 2026
10cbdf0
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jul 16, 2026
6f3b51b
Merge branch 'main' into feature/dynamic-task-iteration
dabla Jul 16, 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
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ Dockerfile.ci @potiuk @ashb @gopidesupavan @amoghrajesh @jscheffl @bugraoz93 @ja
# AIP-72 - Task SDK
# Python SDK
/task-sdk/ @ashb @kaxil @amoghrajesh
/task-sdk/**/batchedoperator.py @dabla
/task-sdk/**/executor.py @dabla
/task-sdk/**/iterableoperator.py @dabla

# AIP-108 - Coordinators
/task-sdk/src/airflow/sdk/coordinators/ @jason810496 @uranusjr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ def on_failure_fail_dagrun(self) -> bool:
def on_failure_fail_dagrun(self, v) -> None:
self.partial_kwargs["on_failure_fail_dagrun"] = bool(v)

@property
def batch_size(self) -> int:
return self.partial_kwargs.get("batch_size", 0)

@classmethod
def get_serialized_fields(cls):
"""Fields to extract from JSON-Serialized DAG."""
Expand Down Expand Up @@ -463,15 +467,20 @@ def get_parse_time_mapped_ti_count(self) -> int:
current_count = self._get_specified_expand_input().get_parse_time_mapped_ti_count()

def _get_parent_count() -> int:
if (group := self.get_closest_mapped_task_group()) is None:
raise NotMapped()
return group.get_parse_time_mapped_ti_count()
try:
if (group := self.get_closest_mapped_task_group()) is None:
raise NotMapped()
return group.get_parse_time_mapped_ti_count()
except NotMapped:
return 1

try:
parent_count = _get_parent_count()
except NotMapped:
return current_count
return parent_count * current_count
parent_count = _get_parent_count()
mapped_ti_count = parent_count * current_count

if self.batch_size > 0:
if self.batch_size < mapped_ti_count:
return self.batch_size
return mapped_ti_count

def iter_mapped_dependencies(self) -> Iterator[Operator]:
"""Upstream dependencies that provide XComs used by this task for task mapping."""
Expand Down Expand Up @@ -519,6 +528,9 @@ def _(task: SerializedBaseOperator | TaskSDKBaseOperator, run_id: str, *, sessio
def _(task: SerializedMappedOperator | TaskSDKMappedOperator, run_id: str, *, session: Session) -> int:
from airflow.serialization.serialized_objects import BaseSerialization, _ExpandInputRef

if task.batch_size > 0:
return task.batch_size

exp_input = task._get_specified_expand_input()
# TODO (GH-52141): 'task' here should be scheduler-bound and returns scheduler expand input.
if not hasattr(exp_input, "get_total_map_length"):
Expand Down
69 changes: 69 additions & 0 deletions airflow-core/tests/unit/models/test_taskinstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from airflow.exceptions import (
AirflowException,
AirflowSkipException,
NotMapped,
)
from airflow.models.asset import (
AssetActive,
Expand Down Expand Up @@ -3487,6 +3488,74 @@ def show(a, b):
dag_maker.run_ti(ti.task_id, map_index=ti.map_index, dag_run=dag_run, session=session)
assert outputs == [(2, 5), (2, 10), (4, 5), (4, 10), (8, 5), (8, 10)]

def test_iterate_literal_cross_product(self, dag_maker, session):
"""Test an iterated task with literal cross product args properly."""
outputs = []

with dag_maker(dag_id="product_same_types", session=session, serialized=True) as dag:

@dag.task
def show(a, b):
outputs.append((a, b))

show.iterate(a=[2, 4, 8], b=[5, 10])

dag_run = dag_maker.create_dagrun()

show_task = dag.get_task("show")
with pytest.raises(NotMapped):
show_task.get_parse_time_mapped_ti_count()
with pytest.raises(NotMapped):
TaskMap.expand_mapped_task(show_task, dag_run.run_id, session=session)

tis = session.scalars(
select(TaskInstance)
.where(
TaskInstance.dag_id == dag.dag_id,
TaskInstance.task_id == "show",
TaskInstance.run_id == dag_run.run_id,
)
.order_by(TaskInstance.map_index)
).all()
for ti in tis:
ti.refresh_from_task(show_task)
dag_maker.run_ti(ti.task_id, map_index=ti.map_index, dag_run=dag_run, session=session)
assert outputs == [(2, 5), (2, 10), (4, 5), (4, 10), (8, 5), (8, 10)]

def test_iterate_literal_cross_product_in_batch(self, dag_maker, session):
"""Test a batched iterated task with literal cross product args properly."""
outputs = []

with dag_maker(dag_id="product_same_types", session=session, serialized=True) as dag:

@dag.task
def show(a, b):
outputs.append((a, b))

show.batch(size=2).iterate(a=[2, 4, 8], b=[5, 10])

dag_run = dag_maker.create_dagrun()

show_task = dag.get_task("show")
assert show_task.get_parse_time_mapped_ti_count() == 2
mapped_tis, max_map_index = TaskMap.expand_mapped_task(show_task, dag_run.run_id, session=session)
assert len(mapped_tis) == 0 # Expanded at parse!
assert max_map_index == 1

tis = session.scalars(
select(TaskInstance)
.where(
TaskInstance.dag_id == dag.dag_id,
TaskInstance.task_id == "show",
TaskInstance.run_id == dag_run.run_id,
)
.order_by(TaskInstance.map_index)
).all()
for ti in tis:
ti.refresh_from_task(show_task)
dag_maker.run_ti(ti.task_id, map_index=ti.map_index, dag_run=dag_run, session=session)
assert outputs == [(2, 5), (4, 5), (8, 5), (2, 10), (4, 10), (8, 10)]

def test_map_in_group(self, tmp_path: pathlib.Path, dag_maker, session):
out = tmp_path.joinpath("out")
out.touch()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2874,6 +2874,7 @@ def test_operator_expand_serde():
"template_ext": [".sh", ".bash"],
"template_fields_renderers": {"bash_command": "bash", "env": "json"},
"ui_color": "#f0ede4",
"_register_with_dag": True,
"_disallow_kwargs_override": False,
"_expand_input_attr": "expand_input",
}
Expand Down Expand Up @@ -2917,6 +2918,7 @@ def test_operator_expand_xcomarg_serde():
},
"task_id": "task_2",
"template_fields": ["arg1", "arg2"],
"_register_with_dag": True,
"_disallow_kwargs_override": False,
"_expand_input_attr": "expand_input",
}
Expand Down Expand Up @@ -2972,6 +2974,7 @@ def test_operator_expand_kwargs_literal_serde(strict):
},
"task_id": "task_2",
"template_fields": ["arg1", "arg2"],
"_register_with_dag": True,
"_disallow_kwargs_override": strict,
"_expand_input_attr": "expand_input",
}
Expand Down Expand Up @@ -3019,6 +3022,7 @@ def test_operator_expand_kwargs_xcomarg_serde(strict):
},
"task_id": "task_2",
"template_fields": ["arg1", "arg2"],
"_register_with_dag": True,
"_disallow_kwargs_override": strict,
"_expand_input_attr": "expand_input",
}
Expand Down Expand Up @@ -3127,6 +3131,7 @@ def x(arg1, arg2, arg3):
"op_args": "py",
"op_kwargs": "py",
},
"_register_with_dag": True,
"_disallow_kwargs_override": False,
"_expand_input_attr": "op_kwargs_expand_input",
"python_callable_name": "test_taskflow_expand_serde.<locals>.x",
Expand Down Expand Up @@ -3220,6 +3225,7 @@ def x(arg1, arg2, arg3):
"op_args": "py",
"op_kwargs": "py",
},
"_register_with_dag": True,
"_disallow_kwargs_override": strict,
"_expand_input_attr": "op_kwargs_expand_input",
}
Expand Down Expand Up @@ -3331,6 +3337,7 @@ def operator_extra_links(self):
"partial_kwargs": {
"retry_delay": {"__type": "timedelta", "__var": 300.0},
},
"_register_with_dag": True,
"_disallow_kwargs_override": False,
"_expand_input_attr": "expand_input",
"_operator_extra_links": {"airflow": "_link_AirflowLink2"},
Expand Down
19 changes: 17 additions & 2 deletions devel-common/src/tests_common/test_utils/mock_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,29 @@
from typing import TYPE_CHECKING, Any
from unittest import mock

from airflow.models import DagRun
from airflow.utils.types import DagRunType

from tests_common.test_utils.compat import Context
from tests_common.test_utils.taskinstance import create_task_instance
from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS, AIRFLOW_V_3_1_PLUS

if AIRFLOW_V_3_1_PLUS:
from airflow.sdk import timezone
else:
from airflow.utils import timezone # type: ignore[attr-defined,no-redef]

if TYPE_CHECKING:
from sqlalchemy.orm import Session


def mock_context(task) -> Context:
def generate_run_id() -> str:
if AIRFLOW_V_3_0_PLUS:
return DagRun.generate_run_id(run_type=DagRunType.MANUAL, run_after=timezone.utcnow())
return DagRun.generate_run_id(run_type=DagRunType.MANUAL, execution_date=timezone.utcnow()) # type: ignore[call-arg]


def mock_context(task, run_id: str | None = None) -> Context:
from airflow.models import TaskInstance
from airflow.utils.session import NEW_SESSION

Expand Down Expand Up @@ -64,6 +79,6 @@ def xcom_push(self, key: str, value: Any, session: Session = NEW_SESSION, **kwar
values[key] = value

values["ti"] = create_task_instance(task, dag_version_id=mock.MagicMock(), ti_type=MockedTaskInstance)
values["run_id"] = generate_run_id() if run_id is None else run_id

# See https://github.com/python/mypy/issues/8890 - mypy does not support passing typed dict to TypedDict
return Context(values) # type: ignore[misc]
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,7 @@ PodManager
PodSpec
podSpec
podspec
Pokémon
polars
poller
polyfill
Expand Down
2 changes: 2 additions & 0 deletions task-sdk/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ repos:
^src/airflow/sdk/definitions/asset/__init__\.py$|
^src/airflow/sdk/definitions/asset/decorators\.py$|
^src/airflow/sdk/definitions/taskgroup\.py$|
^src/airflow/sdk/definitions/iterableoperator\.py$|
^src/airflow/sdk/definitions/mappedoperator\.py$|
^src/airflow/sdk/definitions/batchedoperator\.py$|
^src/airflow/sdk/definitions/deadline\.py$|
^src/airflow/sdk/definitions/dag\.py$|
^src/airflow/sdk/definitions/_internal/types\.py$|
Expand Down
2 changes: 1 addition & 1 deletion task-sdk/docs/deferred-vs-async-operators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ concurrently using ``asyncio.gather`` while limiting concurrency with a semaphor

.. note::

The upcoming *Dynamic Task Iteration* feature will simplify patterns like this.
The new :ref:`Dynamic Task Iteration <sdk-dynamic-task-mapping-vs-iteration>`. feature will simplify patterns like this.
Instead of manually managing concurrency with constructs such as
``asyncio.gather`` and ``asyncio.Semaphore``, authors will be able to iterate
over asynchronous results directly in downstream tasks while still benefiting
Expand Down
Loading
Loading