This is an automated email from the ASF dual-hosted git repository.
amoghrajesh 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 168d46aaac8 Make KPO durable execution inert below Airflow 3.3 (#71492)
168d46aaac8 is described below
commit 168d46aaac8b54b2ccb8bb8fa3a46da43b6ad84e
Author: Amogh Desai <[email protected]>
AuthorDate: Fri Aug 14 11:21:16 2026 +0530
Make KPO durable execution inert below Airflow 3.3 (#71492)
---
providers/cncf/kubernetes/docs/operators.rst | 16 +++--
.../providers/cncf/kubernetes/operators/pod.py | 49 +++++++++----
.../unit/cncf/kubernetes/operators/test_pod.py | 80 ++++++++++++++++++++--
3 files changed, 119 insertions(+), 26 deletions(-)
diff --git a/providers/cncf/kubernetes/docs/operators.rst
b/providers/cncf/kubernetes/docs/operators.rst
index 05da87f11dd..a7d1c242b12 100644
--- a/providers/cncf/kubernetes/docs/operators.rst
+++ b/providers/cncf/kubernetes/docs/operators.rst
@@ -216,9 +216,11 @@ To always create a fresh pod on retry rather than
reattaching, set ``durable=Fal
durable=False,
)
-Durable execution requires Airflow 3.3 or newer, since it relies on the task
state store. On
-earlier Airflow versions ``durable=True`` (the default) falls back to the same
label-search
-reattach behavior this operator has always used.
+Durable execution requires Airflow 3.3 or newer, since it relies on the task
state store. Below
+3.3, ``durable`` has no effect at all: setting it explicitly only emits a
warning, and its value is
+ignored either way. The deprecated ``reattach_on_restart`` parameter (default
``True``) is the
+only lever there, and it falls back to the same label-search reattach behavior
this operator has
+always used -- unchanged from before this feature existed.
The pod identity persisted in task state store isn't deleted automatically,
that only happens
when someone runs ``airflow state-store clean``. If a task's ``retry_delay``
is longer than
@@ -229,9 +231,11 @@ search can often still find the same pod, but it loses the
unambiguous reconnect
exposure to ``FoundMoreThanOnePodFailure`` if a genuine duplicate pod exists
by then. Avoid
running cleanup on a schedule shorter than your longest ``retry_delay``.
-``durable`` supersedes the deprecated ``reattach_on_restart`` parameter --
passing
-``reattach_on_restart`` still works but emits
``AirflowProviderDeprecationWarning`` (on Airflow
-3.3+) and maps its value onto ``durable``.
+``durable`` supersedes the deprecated ``reattach_on_restart`` parameter on
Airflow 3.3+, where
+passing ``reattach_on_restart`` still works and maps its value onto
``durable``. Below 3.3,
+``reattach_on_restart`` remains the only working option, since ``durable`` is
a no-op there.
+Either way, passing it emits an ``AirflowProviderDeprecationWarning``, since
the parameter will be
+removed once this provider's minimum supported Airflow version reaches 3.3.
How does XCom work?
^^^^^^^^^^^^^^^^^^^
diff --git
a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.py
b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.py
index f52333d605c..5a9df5645aa 100644
---
a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.py
+++
b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.py
@@ -176,16 +176,17 @@ class KubernetesPodOperator(BaseOperator):
:param in_cluster: run kubernetes client with in_cluster configuration.
:param cluster_context: context that points to kubernetes cluster.
Ignored when in_cluster is True. If None, current-context is used.
(templated)
- :param reattach_on_restart: deprecated for Airflow 3.3+, use ``durable``
instead. If the worker dies
- while the pod is running, reattach and monitor during the next try. If
False, always create a
- new pod for each try.
+ :param reattach_on_restart: deprecated, use ``durable`` instead. If the
worker dies while the pod
+ is running, reattach and monitor during the next try. If False, always
create a new pod for
+ each try. Below Airflow 3.3, this remains the only way to control that
behavior, since
+ ``durable`` has no effect there.
:param durable: if the worker dies while the pod is running, reattach and
monitor during the next
try instead of creating a duplicate pod. If False, always create a new
pod for each try.
- Supersedes ``reattach_on_restart``; on Airflow 3.3+ the reconnection
uses a persisted pod
- identity in task state store instead of a label search, removing the
ambiguity failure a label
- search can hit when more than one matching pod exists. Defaults to
``True``. On Airflow
- versions below 3.3, ``durable`` still works but falls back to the same
label-search reattach
- behavior as ``reattach_on_restart``, since task state store is
unavailable.
+ Supersedes ``reattach_on_restart`` on Airflow 3.3+, where the
reconnection uses a persisted
+ pod identity in task state store instead of a label search. Defaults
to ``True`` on Airflow
+ 3.3+. Below 3.3, ``durable`` has no effect -- setting it explicitly
only emits a warning --
+ and ``reattach_on_restart`` (default ``True``) is the only lever,
using the same
+ label-search reattach mechanism it always has.
:param labels: labels to apply to the Pod. (templated)
:param startup_timeout_seconds: timeout in seconds to startup the pod
after pod was scheduled.
:param startup_check_interval_seconds: interval in seconds to check if the
pod has already started
@@ -340,7 +341,7 @@ class KubernetesPodOperator(BaseOperator):
cluster_context: str | None = None,
labels: dict | None = None,
reattach_on_restart: bool | None = None,
- durable: bool = True,
+ durable: bool | None = None,
startup_timeout_seconds: int = 120,
startup_check_interval_seconds: int = 5,
schedule_timeout_seconds: int | None = None,
@@ -401,14 +402,34 @@ class KubernetesPodOperator(BaseOperator):
) -> None:
if reattach_on_restart is not None:
# Kept as a real named parameter (not **kwargs) so `default_args`
still applies correctly.
- if AIRFLOW_V_3_3_PLUS:
+ warnings.warn(
+ "`reattach_on_restart` is deprecated and will be removed once
this provider's "
+ "minimum supported Airflow version reaches 3.3. "
+ + (
+ "Use `durable` instead."
+ if AIRFLOW_V_3_3_PLUS
+ else "On Airflow 3.3+, use `durable` instead."
+ ),
+ AirflowProviderDeprecationWarning,
+ stacklevel=2,
+ )
+ if AIRFLOW_V_3_3_PLUS:
+ if durable is None:
+ # durable takes precedence when set, even against a conflicting
+ # reattach_on_restart; reattach_on_restart only fills the gap
when durable itself
+ # was never touched.
+ durable = True if reattach_on_restart is None else
reattach_on_restart
+ else:
+ if durable is not None:
+ # durable itself has no effect below 3.3 -- there's no task
state store to persist
+ # a pod identity to, so only reattach_on_restart's
label-search fallback is real
+ # there.
warnings.warn(
- "`reattach_on_restart` is deprecated and will be removed
in a future release. "
- "Use `durable` instead.",
- AirflowProviderDeprecationWarning,
+ "`durable` has no effect on Airflow versions below 3.3.",
+ UserWarning,
stacklevel=2,
)
- durable = reattach_on_restart
+ durable = True if reattach_on_restart is None else
reattach_on_restart
super().__init__(**kwargs)
self.kubernetes_conn_id = kubernetes_conn_id
self.do_xcom_push = do_xcom_push
diff --git
a/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py
b/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py
index 9d070272136..d259d87c623 100644
--- a/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py
+++ b/providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py
@@ -2566,6 +2566,16 @@ class TestKubernetesPodOperator:
assert result.metadata.name == pod_2.metadata.name
+_REATTACH_DEPRECATION_MESSAGE_PREFIX = (
+ "`reattach_on_restart` is deprecated and will be removed once this
provider's "
+ "minimum supported Airflow version reaches 3.3. "
+)
+REATTACH_DEPRECATION_MESSAGE_PRE_3_3 = (
+ _REATTACH_DEPRECATION_MESSAGE_PREFIX + "On Airflow 3.3+, use `durable`
instead."
+)
+REATTACH_DEPRECATION_MESSAGE_3_3_PLUS = _REATTACH_DEPRECATION_MESSAGE_PREFIX +
"Use `durable` instead."
+
+
@pytest.mark.skipif(
not AIRFLOW_V_3_3_PLUS, reason="durable execution (task_state_store)
requires Airflow 3.3+"
)
@@ -2581,6 +2591,12 @@ class TestKubernetesPodOperatorDurableExecution:
patch.stopall()
+ def test_warning_message_recommends_durable_directly_on_3_3_plus(self):
+ with pytest.warns(
+ AirflowProviderDeprecationWarning,
match=f"^{re.escape(REATTACH_DEPRECATION_MESSAGE_3_3_PLUS)}$"
+ ):
+ KubernetesPodOperator(task_id="task", reattach_on_restart=True)
+
def test_durable_fresh_submit_persists_pod_identity(self):
k = KubernetesPodOperator(
image="ubuntu:16.04",
@@ -2779,7 +2795,9 @@ class TestKubernetesPodOperatorDurableExecution:
@pytest.mark.parametrize("reattach_value", [True, False])
def test_reattach_on_restart_deprecation_maps_to_durable(self,
reattach_value):
- with pytest.warns(AirflowProviderDeprecationWarning,
match="reattach_on_restart"):
+ with pytest.warns(
+ AirflowProviderDeprecationWarning,
match=f"^{re.escape(REATTACH_DEPRECATION_MESSAGE_3_3_PLUS)}$"
+ ):
k = KubernetesPodOperator(
task_id="task",
reattach_on_restart=reattach_value,
@@ -2787,18 +2805,22 @@ class TestKubernetesPodOperatorDurableExecution:
assert k.durable is reattach_value
assert k.reattach_on_restart is reattach_value
- def
test_reattach_on_restart_and_durable_conflict_reattach_on_restart_wins(self):
- with pytest.warns(AirflowProviderDeprecationWarning,
match="reattach_on_restart"):
+ def test_durable_wins_over_conflicting_reattach_on_restart(self):
+ with pytest.warns(
+ AirflowProviderDeprecationWarning,
match=f"^{re.escape(REATTACH_DEPRECATION_MESSAGE_3_3_PLUS)}$"
+ ):
k = KubernetesPodOperator(
task_id="task",
durable=False,
reattach_on_restart=True,
)
- assert k.durable is True
- assert k.reattach_on_restart is True
+ assert k.durable is False
+ assert k.reattach_on_restart is False
def test_reattach_on_restart_via_default_args_reaches_durable(self,
dag_maker):
- with pytest.warns(AirflowProviderDeprecationWarning,
match="reattach_on_restart"):
+ with pytest.warns(
+ AirflowProviderDeprecationWarning,
match=f"^{re.escape(REATTACH_DEPRECATION_MESSAGE_3_3_PLUS)}$"
+ ):
with dag_maker(dag_id="test_reattach_default_args",
default_args={"reattach_on_restart": False}):
k = KubernetesPodOperator(task_id="task")
assert k.durable is False
@@ -2808,6 +2830,52 @@ class TestKubernetesPodOperatorDurableExecution:
assert
KubernetesPodOperator._KubernetesPodOperator__supports_durable_execution is True
+class TestKubernetesPodOperatorDurableBelow3_3:
+ @pytest.mark.parametrize("durable_value", [True, False])
+ def test_durable_has_no_effect(self, durable_value):
+ with
mock.patch("airflow.providers.cncf.kubernetes.operators.pod.AIRFLOW_V_3_3_PLUS",
False):
+ with pytest.warns(
+ UserWarning, match=r"^`durable` has no effect on Airflow
versions below 3\.3\.$"
+ ):
+ k = KubernetesPodOperator(task_id="task",
durable=durable_value)
+ # old default (reattach_on_restart's default was True), untouched by
the ignored durable value.
+ assert k.durable is True
+ assert k.reattach_on_restart is True
+
+ @pytest.mark.parametrize("reattach_value", [True, False])
+ def test_reattach_on_restart_still_works(self, reattach_value):
+ with
mock.patch("airflow.providers.cncf.kubernetes.operators.pod.AIRFLOW_V_3_3_PLUS",
False):
+ with pytest.warns(
+ AirflowProviderDeprecationWarning,
+ match=f"^{re.escape(REATTACH_DEPRECATION_MESSAGE_PRE_3_3)}$",
+ ):
+ k = KubernetesPodOperator(task_id="task",
reattach_on_restart=reattach_value)
+ assert k.reattach_on_restart is reattach_value
+
+ def test_reattach_on_restart_wins_over_durable(self):
+ with
mock.patch("airflow.providers.cncf.kubernetes.operators.pod.AIRFLOW_V_3_3_PLUS",
False):
+ with pytest.warns(
+ AirflowProviderDeprecationWarning,
+ match=f"^{re.escape(REATTACH_DEPRECATION_MESSAGE_PRE_3_3)}$",
+ ):
+ k = KubernetesPodOperator(task_id="task", durable=False,
reattach_on_restart=True)
+ assert k.reattach_on_restart is True
+
+ def test_default_rettach_on_restart_is_true(self):
+ with
mock.patch("airflow.providers.cncf.kubernetes.operators.pod.AIRFLOW_V_3_3_PLUS",
False):
+ k = KubernetesPodOperator(task_id="task")
+ assert k.durable is True
+ assert k.reattach_on_restart is True
+
+ def test_warns_on_every_supported_airflow_version(self):
+ with
mock.patch("airflow.providers.cncf.kubernetes.operators.pod.AIRFLOW_V_3_3_PLUS",
False):
+ with pytest.warns(
+ AirflowProviderDeprecationWarning,
+ match=f"^{re.escape(REATTACH_DEPRECATION_MESSAGE_PRE_3_3)}$",
+ ):
+ KubernetesPodOperator(task_id="task", reattach_on_restart=True)
+
+
class TestSuppress:
def test__suppress(self, caplog):
with _optionally_suppress(ValueError):