Add ChainRetryPolicy to compose retry policies in order - #73508
Merged
Merged
Conversation
A task's retry_policy can now be a sequence of policies. Each one sees the original exception in turn; the first RETRY or FAIL wins, and a DEFAULT means the next policy is asked. When every policy returns DEFAULT the task's own retries and retry_delay apply. This makes two orders expressible that were not: deterministic rules in front of a slow or costly policy, so known failures never reach it, and a rules floor behind a policy that can fail on its own, such as one that calls a model. Policies need not know about each other and can come from different packages. A policy that raises or returns something other than a RetryDecision is logged and treated as DEFAULT; BaseException propagates. The winning reason names the deciding policy first and the earlier verdicts after it, so it survives the 500-character cut on retry_reason. Also fixes the RetryDecision.retry(retry_delay=...) keyword in the custom-policy example on the Tasks page; the parameter is delay.
kaxil
force-pushed
the
chain-retry-policy
branch
from
September 21, 2026 22:41
062a68d to
92de5bc
Compare
vatsrahul1001
approved these changes
Sep 22, 2026
kaxil
marked this pull request as ready for review
September 22, 2026 07:05
This was referenced Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to AIP-105 (#65474). A task's
retry_policycan now be a sequence of policies.Summary
ChainRetryPolicy([...])consults policies in order with the original task exception. The first RETRY or FAIL wins. A DEFAULT means the policy has nothing to add and the next one is asked; when every policy returns DEFAULT the task's ownretriesandretry_delayapply, as they do today.Two orders become expressible that were not:
ExceptionRetryPolicybefore a slower or costlier policy (one that calls a model, for instance) is consulted, so it never sees them.Design rationale
DEFAULT means "next policy", not "stop". The SDK documents
RetryAction.DEFAULTas "fall through to standard retry logic". Inside a chain that is what you get once the chain is exhausted, so DEFAULT is the natural abstention signal and no new enum value or wrapper type is needed. Two consequences are documented: aRetryRulewithaction=DEFAULTpasses control on rather than ending the chain, and anExceptionRetryPolicywithdefault=RetryAction.FAILends the chain wherever it sits.A broken policy does not take the floor down with it. An ordinary exception from a policy, or a return value that is not a
RetryDecision, is logged and treated as DEFAULT, so the rules after it still run.BaseExceptionpropagates, so a cancelled or terminated task behaves as it does now.The reason survives truncation. The winning decision's reason names the deciding policy first and the earlier verdicts after it:
HTTPStatusRetryPolicy: HTTP 404 (after ExceptionRetryPolicy: no decision). The worker storesretry_reasontruncated to 500 characters, so the verdict comes first and the trail is what gets cut. The worker stores the reason only on a RETRY; on FAIL and DEFAULT it appears in the task log, and the docs say so.Why the SDK. The class composes
RetryPolicy,RetryDecisionandRetryAction, all SDK types, and a chain of two rule policies or a user's own policy followed by rules is a use with no provider involved. Third-partyRetryPolicysubclasses compose without depending on anything else. The constructor takes a sequence likeExceptionRetryPolicy(rules=[...])rather than*policies, so a chain built from configuration is a plain list. Members are validated asRetryPolicyinstances at construction, which is stricter than the operator's duck-typedretry_policyargument, so a typo fails at parse time rather than at the first task failure.No serialization or migration.
retry_policyis already a flag-only serialized field and the worker re-parses the Dag, so a chain rides along like any other policy object.ChainRetryPolicydoes not implementserialize(); nothing in Airflow calls it, and the base docstring now says so.Live run
Airflow from this branch under Breeze, with
LLMRetryPolicyfrom the common.ai provider onmainas the middle rung andanthropic:claude-sonnet-5behind it. One chain shape for every task: a rule set that fails onPermissionError, then the model policy, then a rule set that retriesConnectionErrorafter 15 seconds. Each task raises a different failure so a different rung decides.retries=1.rules_first__403PermissionErrorExceptionRetryPolicy: never retry a 403model_decides__429RuntimeError("429 Too Many Requests ... Retry-After: 45")LLMRetryPolicy: category=rate_limit ... action=retry delay=60s (after ExceptionRetryPolicy: no decision)model_decides__bad_dataValueError("unexpected column ...")LLMRetryPolicy: category=data ... action=fail (after ExceptionRetryPolicy: no decision)floor__model_unreachableConnectionError, model connection id does not existExceptionRetryPolicy: floor (after ExceptionRetryPolicy: no decision; LLMRetryPolicy: LLM classification not applied (model_error); task retry settings apply)The last row is the case the chain exists for: the model policy abstains with a DEFAULT that carries its own explanation, and the rules after it still decide. Task logs, filtered to the decision line:
Docs
A "Chaining policies" section on the Tasks page, marked
versionadded 3.4.0, with an example fromexample_retry_policy.py. While there, the custom-policy example on the same page calledRetryDecision.retry(retry_delay=...); the parameter isdelay, so copying the page raisedTypeErroron the first 429. Fixed.