Uh oh!
There was an error while loading. Please reload this page.
Add on_kill() hook to BaseTrigger to handle user actions on triggers - #65590
Conversation
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
2ecbb75 to
393fdabCompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
kaxil
commented
Apr 21, 2026
As discussed on Slack with you and @ashb, two suggestions: 1. Rename
2. Distinguish from
So |
ashb
commented
Apr 21, 2026
And I know of at least one person who did exactly this. |
Uh oh!
There was an error while loading. Please reload this page.
d2a8966 to
ea3e28cCompare| Maximum number of seconds the triggerer will wait for ``BaseTrigger.on_kill()`` to complete | ||
| before giving up and logging a warning. Prevents a slow or hung external API call from | ||
| blocking the triggerer indefinitely when a deferred task is killed by a user. | ||
| version_added: 3.2.0 |
There was a problem hiding this comment.
| version_added: 3.2.0 | |
| version_added: 3.3.0 |
…n_kill() Replace the asyncio.CancelledError pattern in both triggers with the BaseTrigger.on_kill() hook introduced in apache#65590 (Airflow 3.3.0), as tracked in apache#65733. For Airflow >= 3.3.0, on_kill() fires exclusively on user-initiated kills, making the CancelledError + safe_to_cancel() pattern unnecessary. For Airflow < 3.3.0, the triggerer does not call on_kill(), so the existing CancelledError handler is kept and guarded by not AIRFLOW_V_3_3_0_PLUS. Changes: - Add AIRFLOW_V_3_3_0_PLUS constant to version_compat.py - Add on_kill() to DataprocSubmitTrigger and DataprocSubmitJobDirectTrigger that cancels the Dataproc job via the sync hook using sync_to_async - Keep safe_to_cancel(), get_task_state(), and get_task_instance() under not AIRFLOW_V_3_3_0_PLUS guard; remove them only for Airflow 3.3.0+ - Keep the CancelledError handler in run() under not AIRFLOW_V_3_3_0_PLUS guard so Airflow 2.x/3.x users retain the existing cancellation behaviour - Add focused on_kill tests covering: cancellation, cancel_on_kill=False, and (DataprocSubmitJobDirectTrigger) no-op when job_id is None - Restore old CancelledError tests, skipped on Airflow 3.3.0+ via pytest.mark.skipif(AIRFLOW_V_3_3_PLUS) Closesapache#65733
…n_kill() Replace the asyncio.CancelledError pattern in both triggers with the BaseTrigger.on_kill() hook introduced in apache#65590 (Airflow 3.3.0), as tracked in apache#65733. For Airflow >= 3.3.0, on_kill() fires exclusively on user-initiated kills, making the CancelledError + safe_to_cancel() pattern unnecessary. For Airflow < 3.3.0, the triggerer does not call on_kill(), so the existing CancelledError handler is kept and guarded by not AIRFLOW_V_3_3_0_PLUS. Changes: - Add AIRFLOW_V_3_3_0_PLUS constant to version_compat.py - Add on_kill() to DataprocSubmitTrigger and DataprocSubmitJobDirectTrigger that cancels the Dataproc job via the sync hook using sync_to_async - Keep safe_to_cancel(), get_task_state(), and get_task_instance() under not AIRFLOW_V_3_3_0_PLUS guard; remove them only for Airflow 3.3.0+ - Keep the CancelledError handler in run() under not AIRFLOW_V_3_3_0_PLUS guard so Airflow 2.x/3.x users retain the existing cancellation behaviour - Add focused on_kill tests covering: cancellation, cancel_on_kill=False, and (DataprocSubmitJobDirectTrigger) no-op when job_id is None - Restore old CancelledError tests, skipped on Airflow 3.3.0+ via pytest.mark.skipif(AIRFLOW_V_3_3_PLUS) Closesapache#65733
…n_kill() Replace the asyncio.CancelledError pattern in both triggers with the BaseTrigger.on_kill() hook introduced in apache#65590 (Airflow 3.3.0), as tracked in apache#65733. For Airflow >= 3.3.0, on_kill() fires exclusively on user-initiated kills, making the CancelledError + safe_to_cancel() pattern unnecessary. For Airflow < 3.3.0, the triggerer does not call on_kill(), so the existing CancelledError handler is kept and guarded by not AIRFLOW_V_3_3_0_PLUS. Changes: - Add AIRFLOW_V_3_3_0_PLUS constant to version_compat.py - Add on_kill() to DataprocSubmitTrigger and DataprocSubmitJobDirectTrigger that cancels the Dataproc job via the sync hook using sync_to_async - Keep safe_to_cancel(), get_task_state(), and get_task_instance() under not AIRFLOW_V_3_3_0_PLUS guard; remove them only for Airflow 3.3.0+ - Keep the CancelledError handler in run() under not AIRFLOW_V_3_3_0_PLUS guard so Airflow 2.x/3.x users retain the existing cancellation behaviour - Add focused on_kill tests covering: cancellation, cancel_on_kill=False, and (DataprocSubmitJobDirectTrigger) no-op when job_id is None - Restore old CancelledError tests, skipped on Airflow 3.3.0+ via pytest.mark.skipif(AIRFLOW_V_3_3_PLUS) Closesapache#65733
…#65740) Migrate EmrServerlessStartJobTrigger from the old asyncio.CancelledError catch pattern to the new BaseTrigger.on_kill() hook (added via #65590). Changes: - Add async on_kill() method that cancels the EMR Serverless job only on user-initiated task kills - Remove the CancelledError handler from run(), letting the parent class AwsBaseWaiterTrigger.run() handle the standard waiter flow - Remove safe_to_cancel(), get_task_instance(), get_task_state() which are no longer needed (on_kill only fires on user kills) - Clean up unused imports (asyncio, AIRFLOW_V_3_0_PLUS, TaskInstanceState, etc.) - Update tests to directly verify on_kill() behavior Closes: #65733 (EmrServerlessStartJobTrigger part)
…pache#65590) Deferred tasks don’t currently have a standard way to cancel external jobs when users take actions (fail, clear, mark success), which can leave orphaned jobs running. Add `on_kill()` to BaseTrigger so trigger authors can handle cleanup and cancel external work when a task is terminated.
Was generative AI tooling used to co-author this PR?
closes: #36090
Why + What
Deferred tasks interacting with external systems (BQ, DBX, etc.) have no standard way to cancel the external job when a user marks the task failed, clears it, or marks it succeeded. This leaves orphaned jobs running in the background.
I intend to fix that by adding
on_kill()toBaseTrigger— a no-op by default that trigger authors override to cancel their external job when the task is acted on by a user.The triggerer already passes trigger IDs through a
to_cancelqueue when a trigger is removed. Everything in that queue is a user action —> redistribution only happens when a triggerer is considered dead, at which point it is no longer processing its queue. We useasyncio.Task.cancel("user-action")to pass the reason through the existing cancellation path, sorun_trigger()can checke.args[0]andinvoke on_kill()without any extra API round-trip.Impact
Existing trigger subclasses
Purely additive.
on_kill()defaults to a no-op so nothing breaks if you do not implement it.Triggerer shutdown / redistribution
No performance impact.
on_kill()is only invoked whenCancelledErrorcarries the "user-action" message. Shutdown firestask.cancel("EOF - shutting down")and redistribution goes through theto_createpath — neither triggerson_kill().Provider authors
Override
on_kill()to cancel external jobs. The framework guarantees it is only called for user initiated cancellations and you can implement it as needed.Testing
Trigger
The trigger is such that if
on_cancelis called, once I mark it failed from UI, it will create a sentinel file.The first one is marked failed, second is mark success and third is cancelled.
root@0c61b077a443:/opt/airflow$ ls /tmp/on_cancel_was_called ls: cannot access '/tmp/on_cancel_was_called': No such file or directory [Breeze:3.10.19] root@0c61b077a443:/opt/airflow$ [Breeze:3.10.19] root@0c61b077a443:/opt/airflow$The image shows 3 files created for each action mentioned in 1.
Whats next?
DatabricksExecutionTriggerandDatabricksSQLStatementExecutionTrigger(follow-up PR)boilerplate to
on_kill(){pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.