Repository navigation
Let retry policies reschedule a task without using a retry - #74719
Conversation
b5b8f44 to
56f2873
Compare
A retry policy can now return RetryAction.RESCHEDULE for a failure that means "not yet" rather than "this went wrong", such as a Kubernetes namespace quota that is full. The task goes to up_for_reschedule, the same state a sensor in reschedule mode uses, so the try number and retries are untouched and on_retry_callback does not fire. Every RESCHEDULE must carry max_reschedules, reschedule_timeout, or both, and the task runner enforces them: past a bound the failure goes through the standard retry logic with a retry_reason naming the bound. ChainRetryPolicy, its common.compat copy and ClassifierRetryPolicy now copy the deciding decision with dataclasses.replace instead of rebuilding it field by field, so the bounds survive composition. Closes apache#74660
Read the first reschedule date only through get_first_reschedule_date, fail closed whenever the task raised rather than per state, and tidy the reschedule tests.
56f2873 to
efe34af
Compare
pankajastro
left a comment
There was a problem hiding this comment.
A few questions inline.
Drafted-by: Claude Code (Sonnet 5.5); reviewed by @pankajastro before posting
…ule skips The reschedule timeout needs the context to find the first reschedule date, so the failure handler now requires one instead of defaulting to None. The docs list the callbacks, listener hook, email and metrics that wait until a bound is reached, and explain what happens when a custom policy builds an unbounded RESCHEDULE.
Backport failed to create: v3-4-test. View the failure log Run detailsNote: As of Merging PRs targeted for Airflow 3.X In matter of doubt please ask in #release-management Slack channel.
You can attempt to backport this manually by running: cherry_picker 7c2c922 v3-4-testThis should apply the commit to the v3-4-test branch and leave the commit in conflict state marking After you have resolved the conflicts, you can continue the backport process by running: cherry_picker --continueIf you don't have cherry-picker installed, see the installation guide. |
…74993) A retry policy can now return RetryAction.RESCHEDULE for a failure that means "not yet" rather than "this went wrong", such as a Kubernetes namespace quota that is full. The task goes to up_for_reschedule, the same state a sensor in reschedule mode uses, so the try number and retries are untouched and on_retry_callback does not fire. Every RESCHEDULE must carry max_reschedules, reschedule_timeout, or both, and the task runner enforces them: past a bound the failure goes through the standard retry logic with a retry_reason naming the bound. ChainRetryPolicy, its common.compat copy and ClassifierRetryPolicy now copy the deciding decision with dataclasses.replace instead of rebuilding it field by field, so the bounds survive composition. Closes #74660 (cherry picked from commit 7c2c922) Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
…74993) A retry policy can now return RetryAction.RESCHEDULE for a failure that means "not yet" rather than "this went wrong", such as a Kubernetes namespace quota that is full. The task goes to up_for_reschedule, the same state a sensor in reschedule mode uses, so the try number and retries are untouched and on_retry_callback does not fire. Every RESCHEDULE must carry max_reschedules, reschedule_timeout, or both, and the task runner enforces them: past a bound the failure goes through the standard retry logic with a retry_reason naming the bound. ChainRetryPolicy, its common.compat copy and ClassifierRetryPolicy now copy the deciding decision with dataclasses.replace instead of rebuilding it field by field, so the bounds survive composition. Closes #74660 (cherry picked from commit 7c2c922) Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
Closes #74660
A retry policy can decide RETRY, FAIL or DEFAULT, but some failures mean "not yet" rather than "this went wrong". A KubernetesPodOperator task whose namespace
ResourceQuotais full gets a 403 on pod creation; at the start of the day that can last hours. Today the choice is to subclass the operator and raiseAirflowRescheduleException, or to set dozens of retries, which then fireon_retry_callbackevery few minutes and leave no retries for a real failure once capacity frees up.This adds
RetryAction.RESCHEDULE. The task goes toup_for_reschedule, the state a sensor in reschedule mode already uses, so the try number andretriesare untouched andon_retry_callbackdoes not fire. It works withretries=0.Design rationale
No Execution API or database change. Any task can already move to
up_for_reschedule: the worker sends the sameRescheduleTaskmessage a sensor does, and the server and scheduler paths are unchanged. The policy's reason is logged rather than stored; storing it would need a new field on the reschedule payload.Every RESCHEDULE is bounded. A rule matching a broad exception would otherwise turn every real failure into an endless loop that never touches
retries.max_reschedulesandreschedule_timeoutlive onRetryDecisionandRetryRule, a RESCHEDULE without either raisesValueErrorwhen it is created (so a bad rule fails at parse time), and the task runner enforces them rather than trusting each policy to. Past a bound, the failure goes through the standard retry logic with aretry_reasonsuch asReschedule limit reached (reschedule_timeout=4:00:00): Warehouse busy. Setting either bound on another action also raises.Bounds count the try's reschedules, the records sensors already use.
task_reschedule_countand the first reschedule date reset when a new try starts, so a retry gets a fresh allowance andretriesstill caps the total.This is for an external system the task drives, not for disruption of the Airflow worker. The policy runs in the worker and only sees the exception the task raised. A preempted worker pod is the executor's business (the subject of the AIP-97 classification and AIP-122 infrastructure retry proposals), and a policy never sees it.
Composing policies keeps the bounds.
ChainRetryPolicy, itscommon.compatbackport andClassifierRetryPolicy's fallback paths rebuilt the winning decision field by field, which would drop the new fields and turn a valid RESCHEDULE into aValueError. They now usedataclasses.replace.A rescheduled failure still fails closed. If the terminal-state message cannot reach the supervisor, a task whose exception was turned into a reschedule now exits non-zero like FAILED and UP_FOR_RETRY do, so it is not reported as SUCCESS. A sensor's own reschedule is unchanged.
Run end to end with LocalExecutor and a task that always raises (
max_reschedules=2,retries=1,retry_delay=5s, 10s reschedule delay): try 1 ran three times (two reschedules, then the bound), retried after the task's own 5s delay, try 2 ran three times the same way and then failed.task_rescheduleheld two rows per try, and both tries storedretry_reason = Reschedule limit reached (max_reschedules=2): cluster full.The same Dag in the UI, raising a quota error with a 20s reschedule delay (the
needs_capacityrow is from an earlier version of the Dag). During try 1, after its second reschedule; both reschedules are in the same try's log:After the bound on try 2, with the stored reason in the header:
Known issues
[workers] missing_dag_retries. Sensors in reschedule mode already have this interaction. Telling them apart needs a source column ontask_reschedule.reschedule=True, so a policy-driven reschedule emits another START for the same run.apache-airflow-providers-common-aiversions rebuild decisions field by field, so a RESCHEDULE fromClassifierRetryPolicy'sfallback_policyorfallback_rulesfalls back to DEFAULT until the provider ships this change.