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

shahar1 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 f3784d2e9e1 Cancel the BigQuery job when BigQueryToGCSOperator is 
killed (#74564)
f3784d2e9e1 is described below

commit f3784d2e9e10c175ba4421e2afbb14b61feb3519
Author: Yuseok Jo <[email protected]>
AuthorDate: Sat Oct 17 22:44:18 2026 +0900

    Cancel the BigQuery job when BigQueryToGCSOperator is killed (#74564)
---
 .../google/cloud/transfers/bigquery_to_gcs.py      | 15 ++++++++
 .../google/cloud/transfers/test_bigquery_to_gcs.py | 42 ++++++++++++++++++++++
 2 files changed, 57 insertions(+)

diff --git 
a/providers/google/src/airflow/providers/google/cloud/transfers/bigquery_to_gcs.py
 
b/providers/google/src/airflow/providers/google/cloud/transfers/bigquery_to_gcs.py
index c9ec3df8023..a15a6eb08b7 100644
--- 
a/providers/google/src/airflow/providers/google/cloud/transfers/bigquery_to_gcs.py
+++ 
b/providers/google/src/airflow/providers/google/cloud/transfers/bigquery_to_gcs.py
@@ -86,6 +86,10 @@ class BigQueryToGCSOperator(BaseOperator):
     :param reattach_states: Set of BigQuery job's states in case of which we 
should reattach
         to the job. Should be other than final states.
     :param deferrable: Run operator in the deferrable mode
+    :param cancel_on_kill: Flag which indicates whether cancel the hook's job 
or not, when on_kill is called.
+        With ``force_rerun=False``, a retry reuses the same job ID, and a 
cancelled job cannot be
+        reattached through ``reattach_states``. Set ``force_rerun=True`` for 
retries to start a new
+        job, or ``cancel_on_kill=False`` to keep reattaching to the running 
one.
     :return: URIs for the objects created in Google Cloud Storage
     """
 
@@ -122,6 +126,7 @@ class BigQueryToGCSOperator(BaseOperator):
         force_rerun: bool = False,
         reattach_states: set[str] | None = None,
         deferrable: bool = conf.getboolean("operators", "default_deferrable", 
fallback=False),
+        cancel_on_kill: bool = True,
         **kwargs,
     ) -> None:
         super().__init__(**kwargs)
@@ -143,6 +148,7 @@ class BigQueryToGCSOperator(BaseOperator):
         self.reattach_states: set[str] = reattach_states or set()
         self.hook: BigQueryHook | None = None
         self.deferrable = deferrable
+        self.cancel_on_kill = cancel_on_kill
 
     @staticmethod
     def _handle_job_error(job: BigQueryJob | UnknownJob) -> None:
@@ -270,6 +276,7 @@ class BigQueryToGCSOperator(BaseOperator):
                     project_id=self.project_id or self.hook.project_id,
                     location=self.location or self.hook.location,
                     impersonation_chain=self.impersonation_chain,
+                    cancel_on_kill=self.cancel_on_kill,
                 ),
                 method_name="execute_complete",
             )
@@ -296,6 +303,14 @@ class BigQueryToGCSOperator(BaseOperator):
 
         return self.destination_cloud_storage_uris
 
+    def on_kill(self) -> None:
+        if self.job_id and self.cancel_on_kill:
+            self.hook.cancel_job(  # type: ignore[union-attr]
+                job_id=self.job_id, project_id=self.project_id, 
location=self.location
+            )
+        else:
+            self.log.info("Skipping to cancel job: %s:%s.%s", self.project_id, 
self.location, self.job_id)
+
     def get_openlineage_facets_on_complete(self, task_instance):
         """Implement on_complete as we will include final BQ job id."""
         from airflow.providers.common.compat.openlineage.facet import (
diff --git 
a/providers/google/tests/unit/google/cloud/transfers/test_bigquery_to_gcs.py 
b/providers/google/tests/unit/google/cloud/transfers/test_bigquery_to_gcs.py
index ba1beed291d..1231375beb8 100644
--- a/providers/google/tests/unit/google/cloud/transfers/test_bigquery_to_gcs.py
+++ b/providers/google/tests/unit/google/cloud/transfers/test_bigquery_to_gcs.py
@@ -191,6 +191,48 @@ class TestBigQueryToGCSOperator:
             nowait=True,
         )
 
+    
@mock.patch("airflow.providers.google.cloud.transfers.bigquery_to_gcs.BigQueryHook")
+    def test_execute_deferrable_mode_passes_cancel_on_kill_to_trigger(self, 
mock_hook):
+        mock_hook.return_value.split_tablename.return_value = (PROJECT_ID, 
TEST_DATASET, TEST_TABLE_ID)
+        mock_hook.return_value.insert_job.return_value = 
MagicMock(job_id="real_job_id", error_result=False)
+
+        operator = BigQueryToGCSOperator(
+            task_id=TASK_ID,
+            
source_project_dataset_table=f"{PROJECT_ID}:{TEST_DATASET}.{TEST_TABLE_ID}",
+            destination_cloud_storage_uris=["gs://some-bucket/some-file.txt"],
+            project_id=JOB_PROJECT_ID,
+            deferrable=True,
+            cancel_on_kill=False,
+        )
+        with pytest.raises(TaskDeferred) as exc:
+            operator.execute(context=mock.MagicMock())
+
+        assert exc.value.trigger.cancel_on_kill is False
+
+    
@mock.patch("airflow.providers.google.cloud.transfers.bigquery_to_gcs.BigQueryHook")
+    def test_on_kill(self, mock_hook):
+        mock_hook.return_value.split_tablename.return_value = (PROJECT_ID, 
TEST_DATASET, TEST_TABLE_ID)
+        mock_hook.return_value.insert_job.return_value = 
MagicMock(job_id="real_job_id", error_result=False)
+
+        operator = BigQueryToGCSOperator(
+            task_id=TASK_ID,
+            
source_project_dataset_table=f"{PROJECT_ID}:{TEST_DATASET}.{TEST_TABLE_ID}",
+            destination_cloud_storage_uris=["gs://some-bucket/some-file.txt"],
+            project_id=JOB_PROJECT_ID,
+            location="US",
+            cancel_on_kill=False,
+        )
+        operator.execute(context=mock.MagicMock())
+
+        operator.on_kill()
+        mock_hook.return_value.cancel_job.assert_not_called()
+
+        operator.cancel_on_kill = True
+        operator.on_kill()
+        mock_hook.return_value.cancel_job.assert_called_once_with(
+            job_id="real_job_id", project_id=JOB_PROJECT_ID, location="US"
+        )
+
     def test_execute_complete_reassigns_job_id(self):
         """Assert that we use job_id from event after deferral."""
 

Reply via email to