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

potiuk 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 b20f1a47d01 Fail deferred Cloud Composer tasks when the GCP operation 
errors (#70430)
b20f1a47d01 is described below

commit b20f1a47d018410eab4faba874eea7a623d7a8df
Author: Sepuri Sai Krishna <[email protected]>
AuthorDate: Sat Aug 1 09:37:48 2026 +0530

    Fail deferred Cloud Composer tasks when the GCP operation errors (#70430)
---
 .../google/cloud/triggers/cloud_composer.py        |  7 ++-
 .../google/cloud/triggers/test_cloud_composer.py   | 59 ++++++++++++++++++++++
 2 files changed, 64 insertions(+), 2 deletions(-)

diff --git 
a/providers/google/src/airflow/providers/google/cloud/triggers/cloud_composer.py
 
b/providers/google/src/airflow/providers/google/cloud/triggers/cloud_composer.py
index 040e35f2a38..7269a4a88a2 100644
--- 
a/providers/google/src/airflow/providers/google/cloud/triggers/cloud_composer.py
+++ 
b/providers/google/src/airflow/providers/google/cloud/triggers/cloud_composer.py
@@ -80,9 +80,12 @@ class CloudComposerExecutionTrigger(BaseTrigger):
         while True:
             operation = await 
self.gcp_hook.get_operation(operation_name=self.operation_name)
             if operation.done:
+                # A long-running operation signals failure as done=True with 
``error`` set; while
+                # it is still running neither ``error`` nor ``response`` is 
populated. Checking
+                # ``error`` only in the not-done branch would therefore never 
see a failure.
+                if operation.error.message:
+                    raise AirflowException(f"Cloud Composer Environment error: 
{operation.error.message}")
                 break
-            elif operation.error.message:
-                raise AirflowException(f"Cloud Composer Environment error: 
{operation.error.message}")
             await asyncio.sleep(self.pooling_period_seconds)
         yield TriggerEvent(
             {
diff --git 
a/providers/google/tests/unit/google/cloud/triggers/test_cloud_composer.py 
b/providers/google/tests/unit/google/cloud/triggers/test_cloud_composer.py
index f19fb6e2ca4..2e016158bf0 100644
--- a/providers/google/tests/unit/google/cloud/triggers/test_cloud_composer.py
+++ b/providers/google/tests/unit/google/cloud/triggers/test_cloud_composer.py
@@ -18,15 +18,18 @@
 from __future__ import annotations
 
 from datetime import datetime
+from types import SimpleNamespace
 from unittest import mock
 
 import pytest
 
 from airflow.exceptions import AirflowProviderDeprecationWarning
 from airflow.models import Connection
+from airflow.providers.common.compat.sdk import AirflowException
 from airflow.providers.google.cloud.triggers.cloud_composer import (
     CloudComposerAirflowCLICommandTrigger,
     CloudComposerDAGRunTrigger,
+    CloudComposerExecutionTrigger,
     CloudComposerExternalTaskTrigger,
 )
 from airflow.triggers.base import TriggerEvent
@@ -59,6 +62,23 @@ TEST_EXEC_RESULT = {
     "output_end": True,
     "exit_info": {"exit_code": 0, "error": ""},
 }
+TEST_OPERATION_NAME = "test_operation_name"
+TEST_POOLING_PERIOD_SECONDS = 30
+TEST_OPERATION_ERROR_MESSAGE = "Environment creation failed: quota exceeded"
+
+
+def make_operation(*, done: bool, error_message: str = ""):
+    """
+    Build a stand-in for a ``google.longrunning.Operation``.
+
+    Per the LRO contract, a finished operation has ``done=True`` and exactly 
one of
+    ``error`` or ``response`` set; while it is still running neither is set.
+    """
+    return SimpleNamespace(
+        name=TEST_OPERATION_NAME,
+        done=done,
+        error=SimpleNamespace(message=error_message),
+    )
 
 
 @pytest.fixture
@@ -127,6 +147,45 @@ def external_task_trigger(mock_conn):
     )
 
 
[email protected]
[email protected](
+    
"airflow.providers.google.common.hooks.base_google.GoogleBaseHook.get_connection",
+    return_value=Connection(conn_id="test_conn"),
+)
+def execution_trigger(mock_conn):
+    return CloudComposerExecutionTrigger(
+        project_id=TEST_PROJECT_ID,
+        region=TEST_LOCATION,
+        operation_name=TEST_OPERATION_NAME,
+        gcp_conn_id=TEST_GCP_CONN_ID,
+        impersonation_chain=TEST_IMPERSONATION_CHAIN,
+        pooling_period_seconds=TEST_POOLING_PERIOD_SECONDS,
+    )
+
+
+class TestCloudComposerExecutionTrigger:
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.hooks.cloud_composer.CloudComposerAsyncHook.get_operation")
+    async def test_run_raises_when_operation_finished_with_error(self, 
mock_get_operation, execution_trigger):
+        mock_get_operation.return_value = make_operation(
+            done=True, error_message=TEST_OPERATION_ERROR_MESSAGE
+        )
+
+        with pytest.raises(AirflowException, 
match=TEST_OPERATION_ERROR_MESSAGE):
+            await execution_trigger.run().asend(None)
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.hooks.cloud_composer.CloudComposerAsyncHook.get_operation")
+    async def test_run_yields_event_when_operation_finished_without_error(
+        self, mock_get_operation, execution_trigger
+    ):
+        mock_get_operation.return_value = make_operation(done=True)
+
+        actual_event = await execution_trigger.run().asend(None)
+
+        assert actual_event == TriggerEvent({"operation_name": 
TEST_OPERATION_NAME, "operation_done": True})
+
+
 class TestCloudComposerAirflowCLICommandTrigger:
     def test_serialize(self, cli_command_trigger):
         actual_data = cli_command_trigger.serialize()

Reply via email to