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 34ffab8e4a4 Fix GKEStartPodOperator ignoring execution_timeout when 
deferrable (#71528)
34ffab8e4a4 is described below

commit 34ffab8e4a4232470cd9d3c06847c6960529f615
Author: Ayush Khati <[email protected]>
AuthorDate: Sun Oct 18 14:18:28 2026 +0530

    Fix GKEStartPodOperator ignoring execution_timeout when deferrable (#71528)
    
    Co-authored-by: Shahar Epstein <[email protected]>
---
 .../google/cloud/operators/kubernetes_engine.py    | 16 ++++++
 .../google/cloud/triggers/kubernetes_engine.py     |  1 +
 .../cloud/operators/test_kubernetes_engine.py      | 61 ++++++++++++++++++++++
 .../cloud/triggers/test_kubernetes_engine.py       |  1 +
 4 files changed, 79 insertions(+)

diff --git 
a/providers/google/src/airflow/providers/google/cloud/operators/kubernetes_engine.py
 
b/providers/google/src/airflow/providers/google/cloud/operators/kubernetes_engine.py
index 506558b1c60..72758b7a7d0 100644
--- 
a/providers/google/src/airflow/providers/google/cloud/operators/kubernetes_engine.py
+++ 
b/providers/google/src/airflow/providers/google/cloud/operators/kubernetes_engine.py
@@ -19,6 +19,7 @@
 
 from __future__ import annotations
 
+import datetime
 import time
 import warnings
 from collections.abc import Sequence
@@ -930,6 +931,19 @@ class GKEStartPodOperator(GKEOperatorMixin, 
KubernetesPodOperator):
         on_finish_action = self.on_finish_action
         if type(on_finish_action) is str and self.on_finish_action not in 
[i.value for i in OnFinishAction]:
             on_finish_action = self.on_finish_action.split(".")[-1].lower()  # 
type: ignore[assignment]
+
+        # Anchor on ti.start_date so the deadline survives re-deferrals.
+        trigger_kwargs = dict(self.trigger_kwargs or {})
+        defer_timeout: datetime.timedelta | None = None
+        if self.execution_timeout is not None and context is not None:
+            ti_start_date = context["ti"].start_date
+            execution_deadline = int(ti_start_date.timestamp() + 
self.execution_timeout.total_seconds())
+            trigger_kwargs["_execution_deadline"] = execution_deadline
+            # Allow the trigger to emit its timeout event before the framework 
backstop fires.
+            remaining = execution_deadline - time.time()
+            poll_buffer = max(60, int(self.poll_interval * 2))
+            defer_timeout = datetime.timedelta(seconds=max(remaining, 0) + 
poll_buffer)
+
         self.defer(
             trigger=GKEStartPodTrigger(
                 pod_name=self.pod.metadata.name,  # type: ignore[union-attr]
@@ -950,8 +964,10 @@ class GKEStartPodOperator(GKEOperatorMixin, 
KubernetesPodOperator):
                 logging_interval=self.logging_interval,
                 last_log_time=last_log_time,
                 use_dns_endpoint=self.use_dns_endpoint,
+                trigger_kwargs=trigger_kwargs,
             ),
             method_name="trigger_reentry",
+            timeout=defer_timeout,
         )
 
 
diff --git 
a/providers/google/src/airflow/providers/google/cloud/triggers/kubernetes_engine.py
 
b/providers/google/src/airflow/providers/google/cloud/triggers/kubernetes_engine.py
index 833a4cfe4a2..b1638b17ffb 100644
--- 
a/providers/google/src/airflow/providers/google/cloud/triggers/kubernetes_engine.py
+++ 
b/providers/google/src/airflow/providers/google/cloud/triggers/kubernetes_engine.py
@@ -157,6 +157,7 @@ class GKEStartPodTrigger(KubernetesPodTrigger):
                 "impersonation_chain": self.impersonation_chain,
                 "logging_interval": self.logging_interval,
                 "last_log_time": self.last_log_time,
+                "trigger_kwargs": self.trigger_kwargs,
             },
         )
 
diff --git 
a/providers/google/tests/unit/google/cloud/operators/test_kubernetes_engine.py 
b/providers/google/tests/unit/google/cloud/operators/test_kubernetes_engine.py
index eb06480c971..857fee86253 100644
--- 
a/providers/google/tests/unit/google/cloud/operators/test_kubernetes_engine.py
+++ 
b/providers/google/tests/unit/google/cloud/operators/test_kubernetes_engine.py
@@ -17,12 +17,14 @@
 # under the License.
 from __future__ import annotations
 
+import datetime
 import runpy
 from copy import deepcopy
 from unittest import mock
 from unittest.mock import PropertyMock, call
 
 import pytest
+import time_machine
 from google.api_core.exceptions import AlreadyExists, FailedPrecondition, 
PermissionDenied
 from google.cloud.container_v1.types import Cluster, NodePool
 from kubernetes.client import models as k8s
@@ -1272,10 +1274,69 @@ class TestGKEStartPodOperator:
             logging_interval=None,
             last_log_time=mock_last_log_time,
             use_dns_endpoint=False,
+            trigger_kwargs={},
         )
         mock_defer.assert_called_once_with(
             trigger=mock_trigger.return_value,
             method_name="trigger_reentry",
+            timeout=None,
+        )
+
+    @pytest.mark.parametrize(
+        (
+            "execution_timeout",
+            "elapsed",
+            "poll_interval",
+            "trigger_kwargs",
+            "expected_timeout",
+            "pass_context",
+        ),
+        [
+            pytest.param(None, 30, 2, {"_redefer_count": 1}, None, True, 
id="no-execution-timeout"),
+            pytest.param(300, 30, 2, None, 270 + 60, True, 
id="remaining-plus-min-buffer"),
+            pytest.param(300, 30, 45, None, 270 + 90, True, 
id="buffer-scales-with-poll-interval"),
+            pytest.param(300, 600, 2, None, 60, True, 
id="deadline-passed-clamps-remaining"),
+            pytest.param(120, 0, 2, {"_redefer_count": 1}, 120 + 60, True, 
id="keeps-user-trigger-kwargs"),
+            pytest.param(300, 30, 2, None, None, False, 
id="no-context-skips-deadline"),
+        ],
+    )
+    @mock.patch(GKE_OPERATORS_PATH.format("GKEStartPodOperator.defer"))
+    
@mock.patch(GKE_OPERATORS_PATH.format("GKEClusterAuthDetails.fetch_cluster_info"))
+    @mock.patch(GKE_OPERATORS_PATH.format("GKEHook"))
+    @mock.patch(GKE_OPERATORS_PATH.format("GKEStartPodTrigger"))
+    def test_invoke_defer_method_execution_timeout(
+        self,
+        mock_trigger,
+        mock_cluster_hook,
+        mock_fetch_cluster_info,
+        mock_defer,
+        execution_timeout,
+        elapsed,
+        poll_interval,
+        trigger_kwargs,
+        expected_timeout,
+        pass_context,
+    ):
+        ti_start = datetime.datetime(2026, 1, 1, 12, 0, 0, tzinfo=datetime.UTC)
+        self.operator.pod = make_pod()
+        self.operator.poll_interval = poll_interval
+        self.operator.trigger_kwargs = deepcopy(trigger_kwargs)
+        if execution_timeout is not None:
+            self.operator.execution_timeout = 
datetime.timedelta(seconds=execution_timeout)
+        mock_fetch_cluster_info.return_value = GKE_CLUSTER_URL, GKE_SSL_CA_CERT
+        ti = mock.MagicMock(spec=["start_date"])
+        ti.start_date = ti_start
+
+        with time_machine.travel(ti_start + 
datetime.timedelta(seconds=elapsed), tick=False):
+            self.operator.invoke_defer_method(context={"ti": ti} if 
pass_context else None)
+
+        expected_trigger_kwargs = dict(trigger_kwargs or {})
+        if execution_timeout is not None and pass_context:
+            expected_trigger_kwargs["_execution_deadline"] = 
int(ti_start.timestamp()) + execution_timeout
+        assert mock_trigger.call_args.kwargs["trigger_kwargs"] == 
expected_trigger_kwargs
+        assert self.operator.trigger_kwargs == trigger_kwargs
+        assert mock_defer.call_args.kwargs["timeout"] == (
+            None if expected_timeout is None else 
datetime.timedelta(seconds=expected_timeout)
         )
 
 
diff --git 
a/providers/google/tests/unit/google/cloud/triggers/test_kubernetes_engine.py 
b/providers/google/tests/unit/google/cloud/triggers/test_kubernetes_engine.py
index a577dccf0db..fbcd0f5c9de 100644
--- 
a/providers/google/tests/unit/google/cloud/triggers/test_kubernetes_engine.py
+++ 
b/providers/google/tests/unit/google/cloud/triggers/test_kubernetes_engine.py
@@ -144,6 +144,7 @@ class TestGKEStartPodTrigger:
             "last_log_time": None,
             "logging_interval": None,
             "use_dns_endpoint": False,
+            "trigger_kwargs": {},
         }
 
     def test_serialize_round_trip_preserves_on_finish_action(self):

Reply via email to