amoghrajesh opened a new issue, #72128:
URL: https://github.com/apache/airflow/issues/72128

   ### Summary
   
   An operator submits a job to an external system (Spark, Glue, Databricks, 
BigQuery) and polls it. If
   the task fails without being killed, and has no retries left, nothing 
cancels that external job. The
   Dag run finishes, the task is red, and the job keeps consuming compute with 
nothing in Airflow
   tracking it.
   
   This is not specific to durable execution. It affects any operator that 
submits work externally and
   puts its cancellation logic in `on_kill`, which is currently 54 provider 
operators.
   
   ### Why `on_kill` does not cover it
   
   `on_kill()` is where operators put cancellation logic, but the task runner 
calls it in only two
   situations:
   
   - `SIGTERM`, via the handler registered in `task_runner.py`
   - `execution_timeout` being exceeded, which raises `AirflowTaskTimeout`
   
   Ordinary failure is neither. The runner catches whatever the task raised and 
routes it to
   `_handle_current_task_failed`, which picks `FAILED` or `UP_FOR_RETRY` from 
the `should_retry` flag the
   server sent down before execution started. The operator is never notified, 
and there is no distinct
   exception for "this was the final attempt".
   
   ### Two failure modes, and they need different fixes
   
   These get conflated, but only one of them is solvable in the worker process.
   
   **Mode 1: the task raises and the process is still alive.**
   
   A transient polling error, an expired credential, a network partition, or a 
failure in code that runs
   after the job was submitted. The worker is healthy and could run cleanup, it 
just is not asked to.
   This affects every operator that submits externally, durable or not. 
`should_retry` is already in the
   TI context at this point, so the runner knows whether this was the last 
attempt without any new
   plumbing.
   
   **Mode 2: the worker is SIGKILLed.**
   
   Out of memory, or the cloud reclaims the worker machine. `SIGKILL` cannot be 
trapped, so no in-process hook can run for any operator under any design. 
Cleanup has to happen somewhere else and later, which means the external job id 
must have been persisted somewhere that survives the process.
   
   Only durable operators have that. `ResumableJobMixin` writes the id to the 
task state store. A
   non-durable operator keeps it in instance attributes that die with the 
process, so there is nothing
   left to cancel with. Mode 2 is therefore fixable only for durable operators, 
and it is fixable
   precisely because the mixin already persists the id.
   
   ### Durable execution reduces this leak rather than causing it
   
   Worth stating, because the problem surfaced during durable execution review 
and reads like a
   regression otherwise.
   
   A non-durable operator orphans a job on every crashed attempt. Attempt 1 
submits job A and dies,
   attempt 2 submits B, attempt 3 submits C, and A and B are still running. 
That is the duplicate
   submission problem `ResumableJobMixin` exists to solve.
   
   A durable operator reconnects on attempts 2 through N instead of 
resubmitting, so it orphans once, at
   the terminal attempt, rather than N times. This issue is about closing the 
remaining case.
   
   ### Proposed direction
   
   **For mode 1**, give operators a hook that fires when a task reaches a 
terminal failed state, and have
   `ResumableJobMixin` implement it by cancelling the tracked job. Operators 
outside the mixin can adopt
   it individually. `KubernetesPodOperator` shows the shape: cleanup on any 
exit path, with an opt-out
   for users who want the job to outlive the task, as 
`on_finish_action="keep_pod"` allows today.
   
   **For mode 2**, the worker is gone, so the options are server-side 
reconciliation or having the next
   attempt cancel the orphan it finds. Both need the persisted id, so both are 
durable-only. This half is
   larger and could be split into its own issue.
   
   Open questions:
   
   - New operator hook, or `ResumableJobMixin` wrapping `poll_until_complete` 
in `try/except`?
   - Should cleanup fire on every failed attempt or only the terminal one? 
Cancelling on every attempt
     would defeat the reconnect that makes retries work.
   - What does the opt-out look like, and should cancelling be the default?
   - Is mode 2 worth solving now, or is documenting it enough for a first pass?
   


-- 
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