This is an automated email from the ASF dual-hosted git repository.

o-nikolas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 2671465495a Migrate EmrServerlessStartJobTrigger from CancelledError 
to on_kill() (#65740)
2671465495a is described below

commit 2671465495a5165e183e046d658548d948a7e29f
Author: Mohammed Riyaan <[email protected]>
AuthorDate: Tue Apr 28 04:51:34 2026 +0530

    Migrate EmrServerlessStartJobTrigger from CancelledError to on_kill() 
(#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)
---
 .../airflow/providers/amazon/aws/triggers/emr.py   | 23 +++++++++
 .../tests/unit/amazon/aws/triggers/test_emr.py     | 58 ++++++++++++++++++++++
 2 files changed, 81 insertions(+)

diff --git a/providers/amazon/src/airflow/providers/amazon/aws/triggers/emr.py 
b/providers/amazon/src/airflow/providers/amazon/aws/triggers/emr.py
index 5fb5759a9de..b995192e4ee 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/triggers/emr.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/triggers/emr.py
@@ -470,6 +470,10 @@ class EmrServerlessStartJobTrigger(AwsBaseWaiterTrigger):
                 )
             yield TriggerEvent({"status": "success", self.return_key: 
self.return_value})
         except asyncio.CancelledError:
+            # TODO: Remove this CancelledError handler once the minimum 
Airflow version
+            # supported by the provider is >= 3.3. In Airflow 3.3+, 
BaseTrigger.on_kill()
+            # handles cancellation instead. Keeping this for backward 
compatibility with
+            # older Airflow versions.
             if self.job_id and self.cancel_on_kill and await 
self.safe_to_cancel():
                 self.log.info(
                     "Task was cancelled. Cancelling EMR Serverless job. 
Application ID: %s, Job ID: %s",
@@ -489,6 +493,25 @@ class EmrServerlessStartJobTrigger(AwsBaseWaiterTrigger):
         except Exception as e:
             yield TriggerEvent({"status": "failure", "message": str(e)})
 
+    async def on_kill(self) -> None:
+        """
+        Cancel the EMR Serverless job when the trigger is cancelled by a user 
action.
+
+        This hook is available in Airflow 3.3+ via BaseTrigger.on_kill().
+        For older Airflow versions, the CancelledError handler in run() 
provides
+        the same cancellation behavior.
+        """
+        if self.job_id and self.cancel_on_kill:
+            self.log.info(
+                "Cancelling EMR Serverless job. Application ID: %s, Job ID: 
%s",
+                self.application_id,
+                self.job_id,
+            )
+            await sync_to_async(self.hook().conn.cancel_job_run)(
+                applicationId=self.application_id, jobRunId=self.job_id
+            )
+            self.log.info("EMR Serverless job %s cancelled successfully.", 
self.job_id)
+
 
 class EmrServerlessDeleteApplicationTrigger(AwsBaseWaiterTrigger):
     """
diff --git a/providers/amazon/tests/unit/amazon/aws/triggers/test_emr.py 
b/providers/amazon/tests/unit/amazon/aws/triggers/test_emr.py
index fef643dc927..002c5132aad 100644
--- a/providers/amazon/tests/unit/amazon/aws/triggers/test_emr.py
+++ b/providers/amazon/tests/unit/amazon/aws/triggers/test_emr.py
@@ -403,6 +403,64 @@ class TestEmrServerlessStartJobTrigger:
 
         mock_hook.conn.cancel_job_run.assert_not_called()
 
+    @pytest.mark.asyncio
+    async def test_on_kill_cancels_job(self):
+        """Test that on_kill() cancels the EMR Serverless job via the hook."""
+        trigger = EmrServerlessStartJobTrigger(
+            application_id="test_app",
+            job_id="test_job",
+            waiter_delay=30,
+            waiter_max_attempts=60,
+            aws_conn_id="aws_default",
+            cancel_on_kill=True,
+        )
+
+        mock_hook = mock.MagicMock()
+        mock_hook.conn.cancel_job_run.return_value = {"ResponseMetadata": 
{"HTTPStatusCode": 200}}
+
+        with mock.patch.object(trigger, "hook", return_value=mock_hook):
+            await trigger.on_kill()
+
+        
mock_hook.conn.cancel_job_run.assert_called_once_with(applicationId="test_app", 
jobRunId="test_job")
+
+    @pytest.mark.asyncio
+    async def test_on_kill_skips_when_cancel_disabled(self):
+        """Test that on_kill() does nothing when cancel_on_kill=False."""
+        trigger = EmrServerlessStartJobTrigger(
+            application_id="test_app",
+            job_id="test_job",
+            waiter_delay=30,
+            waiter_max_attempts=60,
+            aws_conn_id="aws_default",
+            cancel_on_kill=False,
+        )
+
+        mock_hook = mock.MagicMock()
+
+        with mock.patch.object(trigger, "hook", return_value=mock_hook):
+            await trigger.on_kill()
+
+        mock_hook.conn.cancel_job_run.assert_not_called()
+
+    @pytest.mark.asyncio
+    async def test_on_kill_skips_when_no_job_id(self):
+        """Test that on_kill() does nothing when job_id is None."""
+        trigger = EmrServerlessStartJobTrigger(
+            application_id="test_app",
+            job_id=None,
+            waiter_delay=30,
+            waiter_max_attempts=60,
+            aws_conn_id="aws_default",
+            cancel_on_kill=True,
+        )
+
+        mock_hook = mock.MagicMock()
+
+        with mock.patch.object(trigger, "hook", return_value=mock_hook):
+            await trigger.on_kill()
+
+        mock_hook.conn.cancel_job_run.assert_not_called()
+
 
 class TestEmrServerlessDeleteApplicationTrigger:
     def test_serialization(self):

Reply via email to