Skip to content

Commit c89e715

Browse files
committed
Add user-facing docs for the new Task Loops feature.
Task Loops, introduced in AIP-111, are a whole new way for dag authors to thing Loops chaNge how a Dag author thinks about repeating work, and the change to implement it reaches storage, the scheduler, the Execution API and the UI. Since all these changes are relatively large, it is helpful before we introduct those that reviewers have a mental model of what the behaviour should be, and the features we want to allow are. Rather than explaining this just in PR messages or review threads, lets add this as user facing docs, as most of that will be needed there too. The new Loops doc covers how a loop decides whether to run another pass, how a task reads the previous pass's result, what clearing part of a loop does to the rest, and how loops interact with mapped tasks. At this point/in this PR, nothing exists as code mind you. The short "loop or mapped tasks" page is net-new, and is ther mostly as a jumping off point for "here are the dynamic features you can build your dag with". This oVerview is also why "dynamic mapped tasks" is getting a name change. Mapping is no longer the only way for a Dag's shape to depend on data at runtime, and arguably, mapped tasks aren't really that "dynamic" anymore. "Mapped tasks" is what the UI and the API already call the result ("Clear All Mapped Tasks", MappedOperator), and it reads next to "Loops" as the title of the page the overview links to. "Task mapping" never appears in the UI. However, please note that the rename is "soft" on purpose. The mappIng page keeps its file name and its `dynamic-task-mapping` label, and the old name stays in its opening sentence so existing searches still land. Provider docs link that page by path through intersphinx so moving the file would break their builds until each one is updated. The `dynamic-task-mapping` capability key that the language SDKs report is an identifier rather than prose and is left alone. The snippets are inline here because the example Dags that the guide will include ship with the authoring API, and a file that calls `.loop()` cannot be imported before `.loop()` exists. That change replaces them with included copies that a test executes, so the documented code cannot drift from the behaviour. The placeholder newsfragment is also here because `.loop()` is a new user-visible Dag authoring feature that ships through task-sdk with airflow-core. It will grow and be adjusted by future PRs in this stack.
1 parent 73a4ffa commit c89e715

13 files changed

Lines changed: 507 additions & 23 deletions

File tree

‎airflow-core/docs/administration-and-deployment/dag-serialization.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ The system automatically fills in any missing values by walking up the hierarchy
187187
MappedOperator Default Handling
188188
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
189189

190-
MappedOperators (dynamic task mapping) also participate in the default value system:
190+
MappedOperators (task mapping) also participate in the default value system:
191191

192192
.. code-block:: python
193193

‎airflow-core/docs/authoring-and-scheduling/deferring.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ After the trigger has finished executing, the task may be sent back to the worke
395395
start_from_trigger: bool,
396396
**kwargs: dict[str, Any],
397397
) -> None:
398-
# This whole method will be skipped during dynamic task mapping.
398+
# This whole method will be skipped during task mapping.
399399
400400
super().__init__(*args, **kwargs)
401401
self.start_trigger_args.trigger_kwargs = trigger_kwargs

‎airflow-core/docs/authoring-and-scheduling/dynamic-task-mapping.rst‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,29 @@
1616
under the License.
1717
1818
.. _dynamic-task-mapping:
19+
.. _mapped-tasks:
1920

20-
====================
21-
Dynamic Task Mapping
22-
====================
21+
============
22+
Mapped tasks
23+
============
2324

24-
Dynamic Task Mapping allows a way for a workflow to create a number of tasks at runtime based upon current data, rather than the Dag author having to know in advance how many tasks would be needed.
25+
Task mapping, also called dynamic task mapping, allows a way for a workflow to create a number of tasks at runtime based upon current data, rather than the Dag author having to know in advance how many tasks would be needed.
2526

2627
This is similar to defining your tasks in a for loop, but instead of having the DAG file fetch the data and do that itself, the scheduler can do this based on the output of an upstream task.
27-
Unlike a Python for-loop executed at DAG parse time, dynamic task mapping defers task creation until runtime, allowing the scheduler to determine the exact number of task instances based on upstream task outputs.
28+
Unlike a Python for-loop executed at DAG parse time, task mapping defers task creation until runtime, allowing the scheduler to determine the exact number of task instances based on upstream task outputs.
2829
Right before a mapped task is executed the scheduler will create *n* copies of the task, one for each input.
2930

3031
It is also possible to have a task operate on the collected output of a mapped task, commonly known as map and reduce.
3132

33+
.. seealso::
34+
Not sure whether you need mapped tasks or a loop? See :ref:`loops-and-mapped-tasks`.
35+
3236
Simple mapping
3337
==============
3438

3539
In its simplest form you can map over a list defined directly in your Dag file using the ``expand()`` function instead of calling your task directly.
3640

37-
If you want to see a simple usage of Dynamic Task Mapping, you can look below:
41+
If you want to see a simple usage of task mapping, you can look below:
3842

3943
.. exampleinclude:: /../src/airflow/example_dags/example_dynamic_task_mapping.py
4044
:language: python
@@ -388,7 +392,7 @@ For example, this code will *not* work:
388392
389393
When code in ``my_task_group`` is executed, ``value`` would still only be a reference, not the real value, so the ``if not value`` branch will not work as you likely want. However, if you pass that reference into a task, it will become resolved when the task is executed, and the three ``my_task`` instances will therefore receive 1, 2, and 3, respectively.
390394

391-
It is, therefore, important to remember that, if you intend to perform any logic on a value passed into a task group function, you must always use a task to run the logic, such as ``@task.branch`` (or ``BranchPythonOperator``) for conditions, and task mapping methods for loops.
395+
It is, therefore, important to remember that, if you intend to perform any logic on a value passed into a task group function, you must always use a task to run the logic, such as ``@task.branch`` (or ``BranchPythonOperator``) for conditions, and ``expand()`` to iterate over values. To repeat a group of tasks, see :ref:`loops-and-mapped-tasks`.
392396

393397
.. note:: Task-mapping in a mapped task group is not permitted
394398

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
"""Repeat task groups with fixed counts, runtime conditions, and mapped tasks."""
18+
19+
from __future__ import annotations
20+
21+
# [START refine_estimate]
22+
from airflow.sdk import dag, task, task_group
23+
24+
25+
@dag(schedule=None, catchup=False, tags=["example"])
26+
def refine_estimate():
27+
@task_group
28+
def refine():
29+
@task
30+
def improve(*, loop):
31+
estimate = 1.0 if loop.index == 0 else loop.previous["estimate"]
32+
return (estimate + 2.0 / estimate) / 2.0
33+
34+
@task
35+
def evaluate(estimate):
36+
return {"estimate": estimate, "error": abs(estimate * estimate - 2.0)}
37+
38+
evaluate(improve())
39+
40+
def accurate_enough(*, loop):
41+
return loop.result["error"] < 0.000001
42+
43+
@task
44+
def finished():
45+
print("Refinement finished.")
46+
47+
refinement = refine.loop(max_iterations=10, until=accurate_enough)
48+
refinement >> finished()
49+
50+
51+
refine_estimate()
52+
# [END refine_estimate]
53+
54+
55+
# [START fixed_loop]
56+
@dag(schedule=None, catchup=False, tags=["example"])
57+
def fixed_task_loop():
58+
@task_group
59+
def accumulate():
60+
@task
61+
def increment(*, loop):
62+
return (0 if loop.index == 0 else loop.previous) + 1
63+
64+
increment()
65+
66+
accumulate.loop(max_iterations=3)
67+
68+
69+
fixed_task_loop()
70+
# [END fixed_loop]
71+
72+
73+
# [START mapped_loop]
74+
@dag(schedule=None, catchup=False, tags=["example"])
75+
def mapped_task_loop():
76+
@task_group
77+
def process_batch():
78+
@task
79+
def choose_items(*, loop):
80+
return [1, 2] if loop.index == 0 else [value + 1 for value in loop.previous]
81+
82+
@task
83+
def process(value, *, loop, ti):
84+
print(f"Iteration {loop.index}, mapped position {ti.map_index}")
85+
return value + loop.index
86+
87+
process.expand(value=choose_items())
88+
89+
def batch_ready(*, loop):
90+
return min(loop.result) >= 2
91+
92+
process_batch.loop(max_iterations=3, until=batch_ready)
93+
94+
95+
mapped_task_loop()
96+
# [END mapped_loop]
97+
98+
99+
# [START partial_override_loop]
100+
@dag(schedule=None, catchup=False, tags=["example"])
101+
def partial_override_loop():
102+
@task_group
103+
def accumulate(increment_by):
104+
@task
105+
def increment(increment_by, *, loop):
106+
return (0 if loop.index == 0 else loop.previous) + increment_by
107+
108+
increment(increment_by)
109+
110+
accumulate.override(group_id="accumulate_more").partial(increment_by=5).loop(max_iterations=3)
111+
112+
113+
partial_override_loop()
114+
# [END partial_override_loop]

‎airflow-core/docs/authoring-and-scheduling/index.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ It's recommended that you first review the pages in :doc:`core concepts </core-c
3131
deferring
3232
serializers
3333
connections
34+
loops-and-mapped-tasks
3435
dynamic-task-mapping
36+
loops
3537
assets
3638
dag-result
3739
Non-Python Task SDKs <language-sdks/index>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
.. Licensed to the Apache Software Foundation (ASF) under one
2+
or more contributor license agreements. See the NOTICE file
3+
distributed with this work for additional information
4+
regarding copyright ownership. The ASF licenses this file
5+
to you under the Apache License, Version 2.0 (the
6+
"License"); you may not use this file except in compliance
7+
with the License. You may obtain a copy of the License at
8+
9+
.. http://www.apache.org/licenses/LICENSE-2.0
10+
11+
.. Unless required by applicable law or agreed to in writing,
12+
software distributed under the License is distributed on an
13+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
KIND, either express or implied. See the License for the
15+
specific language governing permissions and limitations
16+
under the License.
17+
18+
.. _loops-and-mapped-tasks:
19+
20+
======================
21+
Loops and mapped tasks
22+
======================
23+
24+
A Dag does not have to be static. How much work it does can depend on data that only exists at
25+
runtime: how many files arrived, what the previous step found, or whether an answer is good enough
26+
yet. Airflow has two features for this, as well as mechanisms like ``retries`` and dynamic Dag generation that fit adjacent use cases.
27+
28+
Choose a mechanism
29+
==================
30+
31+
.. list-table::
32+
:header-rows: 1
33+
:widths: 30 30 40
34+
35+
* - You want to
36+
- Use
37+
- How it works
38+
* - Run the same task once for each item in a collection
39+
- :ref:`Mapped tasks <mapped-tasks>`
40+
- ``task.expand(...)`` creates one task instance per item once the collection is known. The
41+
instances can run concurrently.
42+
* - Repeat a group of tasks, each iteration building on the result of the previous one, until a
43+
condition is met
44+
- :ref:`Loops <loops>`
45+
- ``task_group.loop(...)`` creates the next iteration of task instances only if the last iteration says
46+
another is needed.
47+
* - Give a failed task another try
48+
- Retries, set with ``retries`` on the task. See :doc:`/core-concepts/tasks`.
49+
- A retry reruns the same task instance. It does not create new task instances, and it does not advance a
50+
loop.
51+
* - Create tasks from something known when the Dag file is parsed
52+
- A Python ``for`` loop in the Dag file. See :doc:`/howto/dynamic-dag-generation`.
53+
- The Dag has the same shape in every run.
54+
55+
Use them together
56+
=================
57+
58+
An iteration of a loop can contain mapped tasks, so each iteration can fan out over a different collection of
59+
items. See :ref:`Mapped tasks inside a loop <loops-mapped-tasks>`.
60+
61+
Mapping a whole loop, nesting loops, and placing a loop inside a mapped task group are not supported.

0 commit comments

Comments
 (0)