Skip to content

Fix retry policy overrides not persisted to task instance history - #69235

Merged
vatsrahul1001 merged 1 commit into
mainfrom
fix-retry-policy-history-audit
Jul 2, 2026
Merged

Fix retry policy overrides not persisted to task instance history#69235
vatsrahul1001 merged 1 commit into
mainfrom
fix-retry-policy-history-audit

Conversation

@vatsrahul1001

Copy link
Copy Markdown
Contributor

What's wrong

AIP-105 lets a retry policy choose a custom retry delay and reason each time a task fails. Those are meant to be recorded per-try in task_instance_history as a durable audit trail — "why did the policy wait N seconds on try 3, and for what reason?"

Currently TIH table is always empty: retry_delay_override and retry_reason come out NULL on every task_instance_history row, even when a policy clearly set them.

Root cause

In the Execution API retry handler, the override + reason are written only into the live-row UPDATE, and the task instance is archived to history (prepare_db_for_next_try()TaskInstanceHistory.record_ti()) before those values are set on the object being archived. record_ti() snapshots columns off the in-memory task-instance object, so it captures the still-unset (NULL) values.

Fix

Set retry_delay_override / retry_reason on the task instance before it's archived, so record_ti() picks them up. Live-row behaviour and retry timing are unchanged.

Testing

Adds a regression test that a policy-driven retry persists the override + reason into task_instance_history (fails on main, passes with the fix). The existing tests only asserted the live row, which is why this shipped.

Before
image
After
image

DAG used for testing


from __future__ import annotations

from datetime import timedelta

from airflow.sdk import DAG, ExceptionRetryPolicy, RetryAction, RetryRule, task

QA_POLICY = ExceptionRetryPolicy(
    rules=[
        RetryRule(
            exception=PermissionError,
            action=RetryAction.FAIL,
            reason="Auth failure, not retryable",
        ),
        RetryRule(
            exception=ConnectionError,
            action=RetryAction.RETRY,
            retry_delay=timedelta(seconds=30),
            reason="Transient, backing off 30s",
        ),
        RetryRule(
            exception=TimeoutError,
            action=RetryAction.RETRY,
            retry_delay=timedelta(minutes=2),
            reason="Parked long so live row keeps override at downgrade snapshot",
        ),
    ],
    default=RetryAction.DEFAULT,
)

with DAG(
    dag_id="aip105_migration_baseline",
    schedule=None,
    catchup=False,
    tags=["qa", "aip105", "migration", "retry-policy"],
):

    @task
    def succeeds():
        return "ok"

    # F-01: PermissionError -> FAIL immediately, no override columns written.
    @task(retries=3, retry_delay=timedelta(minutes=5), retry_policy=QA_POLICY)
    def fail_fast_on_auth():
        raise PermissionError("403 Forbidden")

    # F-02 / history: ConnectionError -> RETRY 30s. Cycles through retries; each
    # try archived to task_instance_history with override=30, reason set.
    @task(retries=3, retry_delay=timedelta(minutes=5), retry_policy=QA_POLICY)
    def retry_short_cycle():
        raise ConnectionError("connection reset")

    # Live-row populated data: TimeoutError -> RETRY 2h. After the first failure
    # it sits UP_FOR_RETRY; its LIVE task_instance row holds override=7200 and
    # reason until the next attempt (2h out) — snapshot + downgrade before then.
    @task(retries=5, retry_delay=timedelta(minutes=5), retry_policy=QA_POLICY)
    def retry_parked_long():
        raise TimeoutError("parked for downgrade snapshot")

    # F-03: unmatched exception -> DEFAULT -> standard retry, override stays NULL.
    @task(retries=2, retry_delay=timedelta(minutes=5), retry_policy=QA_POLICY)
    def default_path():
        raise ValueError("unmatched error")

    succeeds() >> [fail_fast_on_auth(), retry_short_cycle(), retry_parked_long(), default_path()]



Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.8)

Generated-by: Claude Code (Opus 4.8) following the guidelines

AIP-105's per-try audit trail (retry_delay_override, retry_reason) is meant to
live durably in task_instance_history — the columns on the live task_instance
row are transient and cleared when the task next enters RUNNING. The Execution
API retry handler wrote the overrides only into the live-row UPDATE and archived
the task instance to history before those values were applied, so the history
columns were always NULL. That silently dropped the audit trail the feature
advertises, while retry timing (which reads the live row) stayed correct.

Setting the overrides on the task instance before prepare_db_for_next_try() lets
record_ti() snapshot them into task_instance_history, restoring the per-try
record.
@boring-cyborg boring-cyborg Bot added area:API Airflow's REST/HTTP API area:task-sdk labels Jul 2, 2026
@vatsrahul1001 vatsrahul1001 added the backport-to-v3-3-test Backport to v3-3-test label Jul 2, 2026
@kaxil

kaxil commented Jul 2, 2026

Copy link
Copy Markdown
Member

Lgtm and can be merged. Although unsure if this in TIH would be useful at all for anything. Maybe auditing 🤷‍♂️

@vatsrahul1001

Copy link
Copy Markdown
Contributor Author

Lgtm and can be merged. Although unsure if this in TIH would be useful at all for anything. Maybe auditing 🤷‍♂️

Yeah auditing is what was mentioned in docs

@vatsrahul1001
vatsrahul1001 merged commit 3bfa5ff into main Jul 2, 2026
77 checks passed
@vatsrahul1001
vatsrahul1001 deleted the fix-retry-policy-history-audit branch July 2, 2026 09:45
@github-actions github-actions Bot added this to the Airflow 3.3.1 milestone Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Hi maintainer, this PR was merged without a milestone set.
We've automatically set the milestone to Airflow 3.3.1 based on: backport label targeting v3-3-test
If this milestone is not correct, please update it to the appropriate milestone.

This comment was generated by Milestone Tag Assistant.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Backport successfully created: v3-3-test

Note: As of Merging PRs targeted for Airflow 3.X
the committer who merges the PR is responsible for backporting the PRs that are bug fixes (generally speaking) to the maintenance branches.

In matter of doubt please ask in #release-management Slack channel.

Status Branch Result
v3-3-test PR Link

github-actions Bot pushed a commit to aws-mwaa/upstream-to-airflow that referenced this pull request Jul 2, 2026
… history (apache#69235)

AIP-105's per-try audit trail (retry_delay_override, retry_reason) is meant to
live durably in task_instance_history — the columns on the live task_instance
row are transient and cleared when the task next enters RUNNING. The Execution
API retry handler wrote the overrides only into the live-row UPDATE and archived
the task instance to history before those values were applied, so the history
columns were always NULL. That silently dropped the audit trail the feature
advertises, while retry timing (which reads the live row) stayed correct.

Setting the overrides on the task instance before prepare_db_for_next_try() lets
record_ti() snapshot them into task_instance_history, restoring the per-try
record.
(cherry picked from commit 3bfa5ff)

Co-authored-by: Rahul Vats <43964496+vatsrahul1001@users.noreply.github.com>
jason810496 added a commit to jason810496/airflow that referenced this pull request Jul 2, 2026
Follow-up to apache#69235. The task_instance_history row for a retried try
stamped archive-time utcnow() as end_date instead of the end date the
worker reported, and missed the final rendered map index when the
mid-run update was suppressed (e.g. template errors during failure
handling). Snapshot both onto the TI before archiving, and let
record_ti() respect a pre-set end_date so the audit trail reflects
when the try actually ended.
aws-airflow-bot pushed a commit to aws-mwaa/upstream-to-airflow that referenced this pull request Jul 2, 2026
… history (apache#69235)

AIP-105's per-try audit trail (retry_delay_override, retry_reason) is meant to
live durably in task_instance_history — the columns on the live task_instance
row are transient and cleared when the task next enters RUNNING. The Execution
API retry handler wrote the overrides only into the live-row UPDATE and archived
the task instance to history before those values were applied, so the history
columns were always NULL. That silently dropped the audit trail the feature
advertises, while retry timing (which reads the live row) stayed correct.

Setting the overrides on the task instance before prepare_db_for_next_try() lets
record_ti() snapshot them into task_instance_history, restoring the per-try
record.
(cherry picked from commit 3bfa5ff)

Co-authored-by: Rahul Vats <43964496+vatsrahul1001@users.noreply.github.com>
vatsrahul1001 added a commit that referenced this pull request Jul 3, 2026
… history (#69235) (#69241)

AIP-105's per-try audit trail (retry_delay_override, retry_reason) is meant to
live durably in task_instance_history — the columns on the live task_instance
row are transient and cleared when the task next enters RUNNING. The Execution
API retry handler wrote the overrides only into the live-row UPDATE and archived
the task instance to history before those values were applied, so the history
columns were always NULL. That silently dropped the audit trail the feature
advertises, while retry timing (which reads the live row) stayed correct.

Setting the overrides on the task instance before prepare_db_for_next_try() lets
record_ti() snapshot them into task_instance_history, restoring the per-try
record.
(cherry picked from commit 3bfa5ff)

Co-authored-by: Rahul Vats <43964496+vatsrahul1001@users.noreply.github.com>
vatsrahul1001 added a commit that referenced this pull request Jul 3, 2026
… history (#69235) (#69241)

AIP-105's per-try audit trail (retry_delay_override, retry_reason) is meant to
live durably in task_instance_history — the columns on the live task_instance
row are transient and cleared when the task next enters RUNNING. The Execution
API retry handler wrote the overrides only into the live-row UPDATE and archived
the task instance to history before those values were applied, so the history
columns were always NULL. That silently dropped the audit trail the feature
advertises, while retry timing (which reads the live row) stayed correct.

Setting the overrides on the task instance before prepare_db_for_next_try() lets
record_ti() snapshot them into task_instance_history, restoring the per-try
record.
(cherry picked from commit 3bfa5ff)

Co-authored-by: Rahul Vats <43964496+vatsrahul1001@users.noreply.github.com>
jason810496 added a commit that referenced this pull request Jul 6, 2026
…#69248)

* Archive worker-reported end date and rendered map index on task retry

Follow-up to #69235. The task_instance_history row for a retried try
stamped archive-time utcnow() as end_date instead of the end date the
worker reported, and missed the final rendered map index when the
mid-run update was suppressed (e.g. template errors during failure
handling). Snapshot both onto the TI before archiving, and let
record_ti() respect a pre-set end_date so the audit trail reflects
when the try actually ended.

* Add test for record_ti fallback end_date stamping

Cover the conditional branch where record_ti() archives a non-finished
TI with end_date=None, verifying it gets stamped with utcnow() and
duration is computed correctly.

* Clarify record_ti comment covers pre-set duration too

* Snapshot rendered_map_index in TIH when a retry explicitly clears it
github-actions Bot pushed a commit to aws-mwaa/upstream-to-airflow that referenced this pull request Jul 6, 2026
…n task retry (apache#69248)

* Archive worker-reported end date and rendered map index on task retry

Follow-up to apache#69235. The task_instance_history row for a retried try
stamped archive-time utcnow() as end_date instead of the end date the
worker reported, and missed the final rendered map index when the
mid-run update was suppressed (e.g. template errors during failure
handling). Snapshot both onto the TI before archiving, and let
record_ti() respect a pre-set end_date so the audit trail reflects
when the try actually ended.

* Add test for record_ti fallback end_date stamping

Cover the conditional branch where record_ti() archives a non-finished
TI with end_date=None, verifying it gets stamped with utcnow() and
duration is computed correctly.

* Clarify record_ti comment covers pre-set duration too

* Snapshot rendered_map_index in TIH when a retry explicitly clears it
(cherry picked from commit 66b803d)

Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com>
aws-airflow-bot pushed a commit to aws-mwaa/upstream-to-airflow that referenced this pull request Jul 6, 2026
…n task retry (apache#69248)

* Archive worker-reported end date and rendered map index on task retry

Follow-up to apache#69235. The task_instance_history row for a retried try
stamped archive-time utcnow() as end_date instead of the end date the
worker reported, and missed the final rendered map index when the
mid-run update was suppressed (e.g. template errors during failure
handling). Snapshot both onto the TI before archiving, and let
record_ti() respect a pre-set end_date so the audit trail reflects
when the try actually ended.

* Add test for record_ti fallback end_date stamping

Cover the conditional branch where record_ti() archives a non-finished
TI with end_date=None, verifying it gets stamped with utcnow() and
duration is computed correctly.

* Clarify record_ti comment covers pre-set duration too

* Snapshot rendered_map_index in TIH when a retry explicitly clears it
(cherry picked from commit 66b803d)

Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com>
henry3260 pushed a commit that referenced this pull request Jul 6, 2026
…n task retry (#69248) (#69458)

* Archive worker-reported end date and rendered map index on task retry

Follow-up to #69235. The task_instance_history row for a retried try
stamped archive-time utcnow() as end_date instead of the end date the
worker reported, and missed the final rendered map index when the
mid-run update was suppressed (e.g. template errors during failure
handling). Snapshot both onto the TI before archiving, and let
record_ti() respect a pre-set end_date so the audit trail reflects
when the try actually ended.

* Add test for record_ti fallback end_date stamping

Cover the conditional branch where record_ti() archives a non-finished
TI with end_date=None, verifying it gets stamped with utcnow() and
duration is computed correctly.

* Clarify record_ti comment covers pre-set duration too

* Snapshot rendered_map_index in TIH when a retry explicitly clears it
(cherry picked from commit 66b803d)

Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com>
vatsrahul1001 pushed a commit that referenced this pull request Jul 7, 2026
…n task retry (#69248) (#69458)

* Archive worker-reported end date and rendered map index on task retry

Follow-up to #69235. The task_instance_history row for a retried try
stamped archive-time utcnow() as end_date instead of the end date the
worker reported, and missed the final rendered map index when the
mid-run update was suppressed (e.g. template errors during failure
handling). Snapshot both onto the TI before archiving, and let
record_ti() respect a pre-set end_date so the audit trail reflects
when the try actually ended.

* Add test for record_ti fallback end_date stamping

Cover the conditional branch where record_ti() archives a non-finished
TI with end_date=None, verifying it gets stamped with utcnow() and
duration is computed correctly.

* Clarify record_ti comment covers pre-set duration too

* Snapshot rendered_map_index in TIH when a retry explicitly clears it
(cherry picked from commit 66b803d)

Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com>
jason810496 pushed a commit to jason810496/airflow that referenced this pull request Jul 7, 2026
… history (apache#69235) (apache#69241)

AIP-105's per-try audit trail (retry_delay_override, retry_reason) is meant to
live durably in task_instance_history — the columns on the live task_instance
row are transient and cleared when the task next enters RUNNING. The Execution
API retry handler wrote the overrides only into the live-row UPDATE and archived
the task instance to history before those values were applied, so the history
columns were always NULL. That silently dropped the audit trail the feature
advertises, while retry timing (which reads the live row) stayed correct.

Setting the overrides on the task instance before prepare_db_for_next_try() lets
record_ti() snapshot them into task_instance_history, restoring the per-try
record.
(cherry picked from commit 3bfa5ff)

Co-authored-by: Rahul Vats <43964496+vatsrahul1001@users.noreply.github.com>
vatsrahul1001 pushed a commit that referenced this pull request Jul 9, 2026
…n task retry (#69248) (#69458)

* Archive worker-reported end date and rendered map index on task retry

Follow-up to #69235. The task_instance_history row for a retried try
stamped archive-time utcnow() as end_date instead of the end date the
worker reported, and missed the final rendered map index when the
mid-run update was suppressed (e.g. template errors during failure
handling). Snapshot both onto the TI before archiving, and let
record_ti() respect a pre-set end_date so the audit trail reflects
when the try actually ended.

* Add test for record_ti fallback end_date stamping

Cover the conditional branch where record_ti() archives a non-finished
TI with end_date=None, verifying it gets stamped with utcnow() and
duration is computed correctly.

* Clarify record_ti comment covers pre-set duration too

* Snapshot rendered_map_index in TIH when a retry explicitly clears it
(cherry picked from commit 66b803d)

Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com>
vatsrahul1001 pushed a commit that referenced this pull request Aug 5, 2026
…n task retry (#69248) (#69458)

* Archive worker-reported end date and rendered map index on task retry

Follow-up to #69235. The task_instance_history row for a retried try
stamped archive-time utcnow() as end_date instead of the end date the
worker reported, and missed the final rendered map index when the
mid-run update was suppressed (e.g. template errors during failure
handling). Snapshot both onto the TI before archiving, and let
record_ti() respect a pre-set end_date so the audit trail reflects
when the try actually ended.

* Add test for record_ti fallback end_date stamping

Cover the conditional branch where record_ti() archives a non-finished
TI with end_date=None, verifying it gets stamped with utcnow() and
duration is computed correctly.

* Clarify record_ti comment covers pre-set duration too

* Snapshot rendered_map_index in TIH when a retry explicitly clears it
(cherry picked from commit 66b803d)

Co-authored-by: Jason(Zhe-You) Liu <68415893+jason810496@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:API Airflow's REST/HTTP API area:task-sdk backport-to-v3-3-test Backport to v3-3-test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants