This is an automated email from the ASF dual-hosted git repository.
amoghrajesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 4b1e9d286c8 Add example Dag for checkpointing progress from an async
task (#71870)
4b1e9d286c8 is described below
commit 4b1e9d286c84f62e87d7e59e533552dbcabeb66c
Author: Amogh Desai <[email protected]>
AuthorDate: Wed Sep 9 15:57:52 2026 +0530
Add example Dag for checkpointing progress from an async task (#71870)
---
.../example_dags/example_task_state_store_async.py | 94 ++++++++++++++++++++++
docs/spelling_wordlist.txt | 1 +
2 files changed, 95 insertions(+)
diff --git
a/airflow-core/src/airflow/example_dags/example_task_state_store_async.py
b/airflow-core/src/airflow/example_dags/example_task_state_store_async.py
new file mode 100644
index 00000000000..18c69174ac0
--- /dev/null
+++ b/airflow-core/src/airflow/example_dags/example_task_state_store_async.py
@@ -0,0 +1,94 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Example DAG for an async task that checkpoints its progress with
``aget``/``aset``.
+
+The task fetches pages concurrently and records which ones finished. A staged
failure
+part way through the first attempt shows the retry resuming from the
checkpoint instead
+of re-fetching everything, and the Dag run still ends successfully.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from datetime import datetime
+
+from airflow.sdk import DAG, task
+from airflow.sdk.execution_time.context import NEVER_EXPIRE
+
+log = logging.getLogger(__name__)
+
+PAGES = list(range(1, 13))
+BATCH_SIZE = 4 # pages awaited concurrently before each checkpoint
+CRASH_AFTER = 6 # staged failure, first attempt only
+
+
+async def _fetch_page(page: int) -> int:
+ """Placeholder for an awaited API call; returns the row count for the
page."""
+ await asyncio.sleep(0.2)
+ return page * 100
+
+
+with DAG(
+ dag_id="example_task_state_store_async",
+ schedule=None,
+ start_date=datetime(2026, 1, 1),
+ catchup=False,
+ tags=["example", "task-state-store"],
+ doc_md=__doc__,
+) as dag:
+
+ @task(retries=2, retry_delay=5)
+ async def ingest_pages(task_state_store=None, ti=None) -> dict:
+ """Fetch every page, checkpointing after each concurrent batch."""
+ # Progress and the running total live in one key so a single write
keeps
+ # them consistent with each other.
+ progress = await task_state_store.aget("progress", default={"done":
[], "rows": 0})
+ done = set(progress["done"])
+ rows = progress["rows"]
+
+ if done:
+ log.info("Resuming: %d of %d pages already fetched", len(done),
len(PAGES))
+ else:
+ log.info("Starting from the top: %d pages to fetch", len(PAGES))
+
+ remaining = [page for page in PAGES if page not in done]
+
+ for start in range(0, len(remaining), BATCH_SIZE):
+ batch = remaining[start : start + BATCH_SIZE]
+ rows += sum(await asyncio.gather(*(_fetch_page(page) for page in
batch)))
+ done.update(batch)
+
+ # Only this coroutine writes the checkpoint. If each _fetch_page
wrote its
+ # own, the writes would interleave at their await points, each
overwriting a
+ # stale copy of the set, and finished pages would vanish from the
checkpoint.
+ await task_state_store.aset(
+ "progress",
+ {"done": sorted(done), "rows": rows},
+ retention=NEVER_EXPIRE,
+ )
+ log.info("Checkpointed %d/%d pages after batch %s", len(done),
len(PAGES), batch)
+
+ if CRASH_AFTER and ti.try_number == 1 and len(done) >= CRASH_AFTER:
+ raise RuntimeError(
+ f"Staged worker loss after {len(done)} pages. The retry
picks up from the checkpoint."
+ )
+
+ log.info("All %d pages fetched, %d rows total", len(done), rows)
+ return {"pages": len(done), "rows": rows}
+
+ ingest_pages()
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 842dc941795..f5d77467181 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -242,6 +242,7 @@ chatbot
chatbots
CheckOperator
checkpointed
+checkpointing
checksums
childs
chmod