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

    <!-- 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: 
http://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.
   
   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".
   -->
   
   - [x] Yes:  Claude code
   <!--
   Generated-by: [Tool Name] following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   -->
   
   closes: https://github.com/apache/airflow/issues/36090
   
   
   ### Why + What
   
   Deferred tasks interacting with external systems (BigQuery, Databricks, 
etc.) previously had no standard way to cancel the external job when a user 
marked the task failed, cleared it, or marked it succeeded. This left orphaned 
jobs running in the background.
   
   To tackle this, adding three methods to `BaseTrigger`:
   - `on_cancel()` — plug point to cancel the external job; no-op by default
   - `safe_to_cancel()` — returns `True` only when the task state is not 
DEFERRED,  distinguishing user-initiated cancellation from triggerer 
restart/redistribution by using task execution API
   - `get_task_state()` — queries and gets the current task state via the 
execution API
   
   The `triggerer_job_runner` now calls `safe_to_cancel()` + `on_cancel()` 
inside the existing `asyncio.CancelledError` handler, with 
`suppress(BaseException)` so that `on_cancel()` failures do not prevent the 
outer `CancelledError` from propagating.
   
   ### Impact
   
   1. Existing trigger subclasses
   Anyone who already implemented `get_task_state()` or `safe_to_cancel()` on 
their own trigger subclass (like BigQuery) will need to rework it.
   
   2. During normal triggerer shutdown or redistribution, every active trigger 
fires CancelledError. The framework now calls safe_to_cancel() for each, which 
makes a synchronous Execution API call via `sync_to_async`. Under high trigger 
counts this can add latency to shutdown.
   
   3. Provider authors / external triggers
   Purely additive. `on_cancel()` defaults to a no-op so nothing breaks if you 
do not implement it. Anyone who wants to hook into user-initiated cancellation 
can override it.
   
   
   ### Testing
   
   Trigger
   
   ```python
   log = logging.getLogger(__name__)
   
   SENTINEL_FILE = "/tmp/on_cancel_was_called"
   
   
   class VerifyOnCancelTrigger(BaseTrigger):
       """
       Trigger that sleeps forever.
   
       on_cancel() writes a sentinel file so we can verify it was called
       without needing a real external service like Databricks or BigQuery.
       """
   
       def serialize(self) -> tuple[str, dict]:
           return ("verify_on_cancel_trigger.VerifyOnCancelTrigger", {})
   
       async def run(self) -> AsyncIterator[TriggerEvent]:
           log.info("VerifyOnCancelTrigger running — mark this task as failed 
to test on_cancel()")
           await asyncio.sleep(99999)
           yield TriggerEvent("done")  # never reached
   
       async def on_cancel(self) -> None:
           log.info("VerifyOnCancelTrigger.on_cancel() CALLED — simulated 
external job cancelled")
           with open(SENTINEL_FILE, "w") as f:
               f.write("cancelled")
   ```
   
   DAG:
   ```python
   
   class DeferForeverOperator(BaseOperator):
       def execute(self, context):
           self.defer(trigger=VerifyOnCancelTrigger(), method_name="resume")
   
       def resume(self, context, event=None):
           log.info("Resumed after trigger fired (should not happen in cancel 
test)")
   
   
   with DAG(
       dag_id="test_on_cancel",
       start_date=datetime(2024, 1, 1),
       schedule=None,
       tags=["manual-test"],
   ) as dag:
       DeferForeverOperator(task_id="deferred_task")
   ```
   
   The trigger is such that if `on_cancel` is called, once I mark it failed 
from UI, it will create a sentinel file.
   
   
   1. Cancelled trigger
   
   <img width="1666" height="899" alt="image" 
src="https://github.com/user-attachments/assets/ce095446-b002-443f-b811-77b0809d4280";
 />
   
   
   2. Before run
   ```shell script
    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$
   ```
   
   3. After run
   
   ```shell script
    root@0c61b077a443:/opt/airflow$
   [Breeze:3.10.19] root@0c61b077a443:/opt/airflow$ ls /tmp/on_cancel_was_called
   /tmp/on_cancel_was_called
   [Breeze:3.10.19] root@0c61b077a443:/opt/airflow$
   [Breeze:3.10.19] root@0c61b077a443:/opt/airflow$
   [Breeze:3.10.19] root@0c61b077a443:/opt/airflow$ cat 
/tmp/on_cancel_was_called
   cancelled[Breeze:3.10.19] root@0c61b077a443:/opt/airflow
   ```
   
   
   
   
   ---
   
   * 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