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 ece8ebfe53f Fix Dataflow deferrable trigger handling of transient 503 
(#66293)
ece8ebfe53f is described below

commit ece8ebfe53f6e4a6982173f150fc92afcbd8902e
Author: Aaron Chen <[email protected]>
AuthorDate: Tue May 5 09:43:51 2026 -0700

    Fix Dataflow deferrable trigger handling of transient 503 (#66293)
---
 .../providers/google/cloud/triggers/dataflow.py    | 17 ++--
 .../unit/google/cloud/triggers/test_dataflow.py    | 98 ++++++++++++++++++++--
 2 files changed, 103 insertions(+), 12 deletions(-)

diff --git 
a/providers/google/src/airflow/providers/google/cloud/triggers/dataflow.py 
b/providers/google/src/airflow/providers/google/cloud/triggers/dataflow.py
index 857e9bea3f8..4b6889d7325 100644
--- a/providers/google/src/airflow/providers/google/cloud/triggers/dataflow.py
+++ b/providers/google/src/airflow/providers/google/cloud/triggers/dataflow.py
@@ -22,6 +22,7 @@ from collections.abc import Sequence
 from functools import cached_property
 from typing import TYPE_CHECKING, Any
 
+from google.api_core.exceptions import ServiceUnavailable
 from google.cloud.dataflow_v1beta3 import JobState
 from google.cloud.dataflow_v1beta3.types import (
     AutoscalingEvent,
@@ -110,11 +111,17 @@ class TemplateJobStartTrigger(BaseTrigger):
         hook = self._get_async_hook()
         try:
             while True:
-                status = await hook.get_job_status(
-                    project_id=self.project_id,
-                    job_id=self.job_id,
-                    location=self.location,
-                )
+                try:
+                    status = await hook.get_job_status(
+                        project_id=self.project_id,
+                        job_id=self.job_id,
+                        location=self.location,
+                    )
+                except ServiceUnavailable as e:
+                    self.log.warning("Dataflow API is temporarily unavailable. 
Retrying... (%s)", e)
+                    await asyncio.sleep(self.poll_sleep)
+                    continue
+
                 if status == JobState.JOB_STATE_DONE:
                     yield TriggerEvent(
                         {
diff --git a/providers/google/tests/unit/google/cloud/triggers/test_dataflow.py 
b/providers/google/tests/unit/google/cloud/triggers/test_dataflow.py
index 6babdd11d24..bc2eb646e81 100644
--- a/providers/google/tests/unit/google/cloud/triggers/test_dataflow.py
+++ b/providers/google/tests/unit/google/cloud/triggers/test_dataflow.py
@@ -19,17 +19,58 @@ from __future__ import annotations
 
 import asyncio
 import logging
+import sys
+import types
 from unittest import mock
 
 import pytest
-
-# TODO: Remove below skip once beam provider changed to ready state
-pytest.importorskip("apache-beam", reason="apache-beam package suspended due 
to grpcio limitation")
-
+from google.api_core.exceptions import ServiceUnavailable
 from google.cloud.dataflow_v1beta3 import Job, JobState, JobType
 
-from airflow.providers.google.cloud.hooks.dataflow import DataflowJobStatus
-from airflow.providers.google.cloud.triggers.dataflow import (
+
+# While the apache-beam provider is suspended (#61926),
+# `airflow.providers.google.cloud.hooks.dataflow` cannot be imported because it
+# does `from airflow.providers.apache.beam.hooks.beam import ...` at module
+# scope. Probe-import the real module first (which also triggers Airflow's
+# provider-entry-point discovery while the real `airflow.providers.apache`
+# namespace package is still intact) and only fall back to stubs when the
+# probe fails. The stubs deliberately cover only the missing beam-specific
+# subtree — we never overwrite `airflow.providers.apache` itself, otherwise
+# sibling providers such as `airflow.providers.apache.pig` would be unreachable
+# via the namespace package's `__path__`.
+def _beam_module_stubs() -> dict[str, types.ModuleType]:
+    beam_package = types.ModuleType("airflow.providers.apache.beam")
+    beam_package.__path__ = []
+    hooks_package = types.ModuleType("airflow.providers.apache.beam.hooks")
+    hooks_package.__path__ = []
+
+    class BeamRunnerType:
+        DataflowRunner = "DataflowRunner"
+
+    class BeamModule(types.ModuleType):
+        BeamHook: object
+        BeamRunnerType: object
+        beam_options_to_args: object
+
+    beam_module = BeamModule("airflow.providers.apache.beam.hooks.beam")
+    beam_module.BeamHook = mock.MagicMock()
+    beam_module.BeamRunnerType = BeamRunnerType
+    beam_module.beam_options_to_args = mock.MagicMock(return_value=[])
+
+    return {
+        "airflow.providers.apache.beam": beam_package,
+        "airflow.providers.apache.beam.hooks": hooks_package,
+        "airflow.providers.apache.beam.hooks.beam": beam_module,
+    }
+
+
+try:
+    import airflow.providers.apache.beam.hooks.beam  # noqa: F401
+except ImportError:
+    sys.modules.update(_beam_module_stubs())
+
+from airflow.providers.google.cloud.hooks.dataflow import DataflowJobStatus  # 
noqa: E402
+from airflow.providers.google.cloud.triggers.dataflow import (  # noqa: E402
     DataflowJobAutoScalingEventTrigger,
     DataflowJobMessagesTrigger,
     DataflowJobMetricsTrigger,
@@ -38,7 +79,7 @@ from airflow.providers.google.cloud.triggers.dataflow import (
     DataflowStartYamlJobTrigger,
     TemplateJobStartTrigger,
 )
-from airflow.triggers.base import TriggerEvent
+from airflow.triggers.base import TriggerEvent  # noqa: E402
 
 PROJECT_ID = "test-project-id"
 JOB_ID = "test_job_id_2012-12-23-10:00"
@@ -238,6 +279,49 @@ class TestTemplateJobStartTrigger:
         # cancel the task to suppress test warnings
         task.cancel()
 
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.triggers.dataflow.asyncio.sleep", 
new_callable=mock.AsyncMock)
+    
@mock.patch("airflow.providers.google.cloud.hooks.dataflow.AsyncDataflowHook.get_job_status")
+    async def test_run_continues_polling_after_retryable_service_unavailable(
+        self, mock_job_status, mock_sleep, template_job_start_trigger
+    ):
+        mock_job_status.side_effect = [
+            ServiceUnavailable(
+                "Visibility check was unavailable. Please retry the request 
and contact support if the problem persists"
+            ),
+            JobState.JOB_STATE_DONE,
+        ]
+        expected_event = TriggerEvent(
+            {
+                "job_id": JOB_ID,
+                "status": "success",
+                "message": "Job completed",
+            }
+        )
+        actual_event = await template_job_start_trigger.run().asend(None)
+
+        assert actual_event == expected_event
+        assert mock_job_status.await_count == 2
+        mock_sleep.assert_awaited_once_with(POLL_SLEEP)
+
+    @pytest.mark.asyncio
+    
@mock.patch("airflow.providers.google.cloud.hooks.dataflow.AsyncDataflowHook.get_job_status")
+    async def 
test_run_returns_error_event_after_unexpected_status_polling_exception(
+        self, mock_job_status, template_job_start_trigger
+    ):
+        mock_job_status.side_effect = Exception("Test exception")
+
+        expected_event = TriggerEvent(
+            {
+                "status": "error",
+                "message": "Test exception",
+            }
+        )
+        actual_event = await template_job_start_trigger.run().asend(None)
+
+        assert actual_event == expected_event
+        mock_job_status.assert_awaited_once_with(project_id=PROJECT_ID, 
job_id=JOB_ID, location=LOCATION)
+
 
 class TestDataflowJobAutoScalingEventTrigger:
     def test_serialize(self, dataflow_job_autoscaling_event_trigger):

Reply via email to