amoghrajesh opened a new pull request, #73027:
URL: https://github.com/apache/airflow/pull/73027

    <!-- SPDX-License-Identifier: Apache-2.0
         https://www.apache.org/licenses/LICENSE-2.0 -->
   
   <!--
   Thank you for contributing!
   
   Please provide above a brief description of the changes made in this pull 
request.
   Write a good git commit message following this guide: 
https://chris.beams.io/posts/git-commit/
   
   Please make sure that your code changes are covered with tests.
   And in case of new features or big changes remember to adjust the 
documentation.
   
   For user-facing UI changes, please attach before/after screenshots (or a 
short
   screen recording) so reviewers can assess the visual impact.
   
   Feel free to ping (in general) for the review if you do not see reaction for 
a few days
   (72 Hours is the minimum reaction time you can expect from volunteers) - we 
sometimes miss notifications.
   
   In case of an existing issue, reference it using one of the following:
   
   * closes: #ISSUE
   * related: #ISSUE
   -->
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   <!--
   If generative AI tooling has been used in the process of authoring this PR, 
please
   change below checkbox to `[X]` followed by the name of the tool, uncomment 
the "Generated-by".
   -->
   
   - [ ] Yes (please specify the tool below)
   
   <!--
   Generated-by: [Tool Name] following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   -->
   
   ### Motivation
   
   Today, when a task goes into "FAILED" it alone tells you nothing about why 
it stopped and that's something a retry policy in the first place should 
surface best. A few examples:
   
   1. Auth error, policy said don't bother retrying.
   Your task hits an API and gets a 403 because the key expired. The policy is 
smart enough to know retrying won't help (it'll just fail 3 more times the same 
way), so it fails immediately after try 1. Without a reason, you just see 
"FAILED after 1 try" and think something's broken with retries. With a reason, 
you see: "auth error, policy chose not to retry" now you know exactly what to 
go fix (the API key), not the code.
   
   2. Rate limit, retries all used up.
   Your task keeps hitting a rate limit. The policy says "retry" each time, so 
it retries 3 times, but the budget (retries=3) runs out. Without a reason, you 
just see 3 red X's and no explanation. With a reason: "rate limit; retries 
exhausted (3 of 3)" and you immediately know it's not a bug; it's just that the 
external service was too slow to respond, and maybe you should bump retries or 
add a delay.
   
   3. Different failures on different tries.
   Try 1 failed for one reason, try 2 for a totally different reason. Right now 
all you see is a row of identical red icons with no way to tell if its the same 
recurring problem or three unrelated ones. Reasons attached to each try let 
someone debugging a flaky task actually see the story, instead of guessing.
   
   The underlying idea: today, all that classification work the policy does 
(LLM or exception-based) happens, gets logged once in task logs. Only the "it 
retried" case kept the reason. This change makes sure the reason survives for 
the FAILED case too, so a future screen can show a plain sentence like "Stopped 
at try 2: auth error, no retry" instead of just a bare failure with no story 
behind it.
   
   ### What
   
   If you've set a `retry_policy` on a task, it can decide things like "this 
looks like an auth error, dont bother retrying" or "retries are exhausted, this 
was a rate limit," but today that explanation is only ever logged to a log line 
and thrown away otherwise. It never reaches the database, so no API response or 
UI screen(I am proposing we build it to provide a better UX to users and its 
more "in the face") can ever show it, no matter how much we build on top later.
   
   This PR is the first, necessary step toward fixing that: make sure the 
reason actually gets saved whenever a task fails, not just when it retries. 
Once its reliably in the database, a future PR can expose it through the API 
and the UI, so a Dag author looking at a failed task instance can see a plain 
reason instead of just "FAILED" with no explanation.
   
   ### Current behaviour
   
   `retry_reason` is already written to the database, but only when a retry 
policy chooses to retry. The two failure outcomes people most want explained 
never save anything: a policy deciding FAIL outright, and a policy deciding 
RETRY but the retry budget being exhausted. Both currently build a bare 
`TaskState(state=FAILED)` with no reason, so the classification text is logged 
and discarded.
   
   - Policy says FAIL: `TaskState(state=FAILED)`, reason dropped.
   - Policy says RETRY but the budget is exhausted: same, reason dropped.
   - Policy says RETRY with budget remaining: `RetryTask(retry_reason=...)`, 
already persisted (unchanged).
   - No retry policy at all: unaffected either way.
   
   ### Proposed change
   
   Thread `retry_reason` through both FAILED paths, end to end:
   
   - `_handle_current_task_failed`'s FAIL branch and `_finalize_task_failure`'s 
exhausted budget branch now attach a reason to the `TaskState` they build. When 
the budget is what stopped it, the reason is combined with an explicit note, 
e.g. `"<reason>; retries exhausted (3 of 3)"`.
   - `TaskState` (task-sdk message) gains a `retry_reason` field.
   - FAILED does not go through the same "send immediately" path as RETRY; it's 
deferred until the subprocess exits (the safety net that also covers a hard 
crash with no message at all). So the supervisor now captures `retry_reason` 
off the `TaskState` message and forwards it to the deferred `finish()` call.
   - `TITerminalStatePayload` (execution API request schema) gains 
`retry_reason`, gated behind a new Cadwyn version change added to the existing 
unreleased `2026-10-30` version (tentative date)
   - The FAILED branch of the state update route now truncates to 500 chars 
(matching the existing RETRY branch) and persists the reason to 
`task_instance.retry_reason`.
   
   Only tasks with a configured retry policy are affected; a plain `retries=N` 
task with no policy gets no reason and no behaviour change, since there's 
nothing meaningful to attach.
   
   ### Testing
   
   After running the `example_retry_policy` dag, earlier if I ran:
   
   ```sql
   SELECT task_instance.state, task_instance.retry_reason from task_instance;
   ```
   
   I would get:
   ```
   failed,
   failed,
   up_for_retry,"rate_limit: The error message explicitly indicates a 429 HTTP 
status code (""Too Many Requests"") with a rate limit exceeded message. The 
error also provides guidance to retry after 60 seconds. Despite this being 
attempt 3 of 3, rate limit errors are transient API throttling issues that 
should be retried with appropriate backoff."
   ```
   
   Now we see:
   ```
   failed,auth: The error indicates an authentication failure due to an expired 
API key for a service account. This is a credentials issue that requires manual 
intervention to refresh or rotate the API key. Retrying will not resolve this 
problem without updating the credentials.
   up_for_retry,"rate_limit: The error message explicitly indicates ""429 Too 
Many Requests"" and ""Rate limit exceeded"", which is a clear signal of API 
throttling. The error also provides guidance to retry after 60 seconds."
   failed,"data: This is a schema validation error where the input data does 
not match the expected schema. The column 'user_id' was expected to be of type 
INT but a STRING value was provided in row 42. This is a data quality issue 
with the input data itself, not a transient infrastructure problem. Retrying 
will not resolve this - the underlying data needs to be corrected or the schema 
needs to be adjusted."
   
   ```
   
   
   
   ### What's next
   
   `retry_reason` still isn't exposed anywhere outside the database. Adding it 
to `TaskInstanceResponse` (API) and rendering it in the Task Instance UI panel 
are the next two steps, so this data can actually reach a Dag author or surface 
it on the UI.
   
   
   
   ---
   
   * Read the **[Pull Request 
Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)**
 for more information. Note: commit author/co-author name and email in commits 
become permanently public when merged.
   * For fundamental code changes, an Airflow Improvement Proposal 
([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals))
 is needed.
   * When adding dependency, check compliance with the [ASF 3rd Party License 
Policy](https://www.apache.org/legal/resolved.html#category-x).
   * For significant user-facing changes create newsfragment: 
`{pr_number}.significant.rst`, in 
[airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments).
 You can add this file in a follow-up commit after the PR is created so you 
know the PR number.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to