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 b95498cd566 Restore the on_operator_resuming callback in
KubernetesPodOperator (#76035)
b95498cd566 is described below
commit b95498cd566e0a08a408f0eb1c89db5ddba520e3
Author: Henry Chen <[email protected]>
AuthorDate: Sun Nov 1 03:21:35 2026 +0800
Restore the on_operator_resuming callback in KubernetesPodOperator (#76035)
This callback is documented as supported, but it has had no call site since
the triggerer took over reading pod logs: the invocation lived inside the
`event["status"] != "running"` guard that the same change removed. Users who
override it lost the only hook that receives the raw trigger event, and
because the base implementation is a no-op there is nothing to indicate it
stopped running.
The unit test meant to cover it was left asserting `on_pod_cleanup` instead,
so nothing detected the gap.
Invoking it ahead of the re-defer branch keeps every re-entry observable,
including the `_redefer_count` retries taken when the trigger reports an
error for a pod that is still alive -- the case where the trigger event is
the only record of what went wrong.
---
providers/cncf/kubernetes/docs/operators.rst | 7 +-
.../airflow/providers/cncf/kubernetes/callbacks.py | 7 +-
.../providers/cncf/kubernetes/operators/pod.py | 13 ++++
.../unit/cncf/kubernetes/operators/test_pod.py | 84 ++++++++++++++++++----
4 files changed, 97 insertions(+), 14 deletions(-)
diff --git a/providers/cncf/kubernetes/docs/operators.rst
b/providers/cncf/kubernetes/docs/operators.rst
index 9c491a275ca..70a8996379e 100644
--- a/providers/cncf/kubernetes/docs/operators.rst
+++ b/providers/cncf/kubernetes/docs/operators.rst
@@ -375,14 +375,19 @@ parameter.
The following callbacks are supported:
* on_sync_client_creation: called after creating the sync client
+* on_pod_manifest_created: called after the pod manifest is built but before
the pod is created
* on_pod_creation: called after creating the pod
* on_pod_starting: called after the pod starts
* on_pod_completion: called when the pod completes
+* on_pod_teardown: called after the completion callbacks but before the pod is
deleted
* on_pod_cleanup: called after cleaning/deleting the pod
* on_operator_resuming: when resuming the task from deferred state
* progress_callback: called on each line of containers logs
-Currently, the callbacks methods are not called in the async mode, this
support will be added in the future.
+In deferrable mode the operator invokes ``on_pod_manifest_created``,
``on_pod_creation``,
+``on_operator_resuming``, ``on_pod_completion``, ``on_pod_teardown`` and
``on_pod_cleanup``.
+``on_pod_starting`` is not invoked, because the triggerer -- not the operator
-- observes pod
+startup. The ``mode`` argument is currently always ``sync``.
Example:
~~~~~~~~
diff --git
a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/callbacks.py
b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/callbacks.py
index f78e83fabd4..fe77c8c0e03 100644
---
a/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/callbacks.py
+++
b/providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/callbacks.py
@@ -181,7 +181,12 @@ class KubernetesPodOperatorCallback:
**kwargs,
) -> None:
"""
- Invoke this callback when resuming the `KubernetesPodOperator` from
deferred state.
+ Invoke this callback when the operator processes a trigger event.
+
+ This fires on every re-entry, including re-entries that defer again,
and
+ including the synchronous short-circuit taken when the pod already
reached
+ a terminal state before deferral. It does not fire when the pod was
garbage
+ collected before re-entry, since there is no pod to pass.
:param pod: the current state of the pod.
:param event: the returned event from the Trigger.
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 44a5d09d359..f9716532765 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
@@ -1181,6 +1181,19 @@ class KubernetesPodOperator(BaseOperator):
# keep this for any subclass override that returns None instead.
raise PodNotFoundException("Could not find pod after resuming from
deferral")
+ # Must stay ahead of the re-defer branch below so every resume is
observable,
+ # including the ones that defer again (``_redefer_count``) -- those
carry the
+ # only record of why the trigger reported an error for a still-live
pod.
+ for callback in self.callbacks:
+ callback.on_operator_resuming(
+ pod=self.pod,
+ event=event,
+ client=self.client,
+ mode=ExecutionMode.SYNC,
+ context=context,
+ operator=self,
+ )
+
follow = self.logging_interval is None
last_log_time = event.get("last_log_time")
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 3c75746df89..5f3219001b4 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
@@ -2497,24 +2497,28 @@ class TestKubernetesPodOperator:
)
context = create_context(k)
- k.trigger_reentry(
- context=context,
- event={
- "status": "success",
- "message": TEST_SUCCESS_MESSAGE,
- "name": TEST_NAME,
- "namespace": TEST_NAMESPACE,
- },
- )
+ # ``on_operator_resuming`` sees the pod re-fetched on re-entry, which
is a
+ # different object from the one ``await_pod_completion`` later returns.
+ resumed_pod_mock = MagicMock()
+ k.hook.get_pod.return_value = resumed_pod_mock
+
+ callback_event = {
+ "status": "success",
+ "message": TEST_SUCCESS_MESSAGE,
+ "name": TEST_NAME,
+ "namespace": TEST_NAMESPACE,
+ }
+ k.trigger_reentry(context=context, event=callback_event)
# check on_operator_resuming callback
- mock_callbacks.on_pod_cleanup.assert_called_once()
- assert mock_callbacks.on_pod_cleanup.call_args.kwargs == {
+ mock_callbacks.on_operator_resuming.assert_called_once()
+ assert mock_callbacks.on_operator_resuming.call_args.kwargs == {
"client": k.client,
"mode": ExecutionMode.SYNC,
- "pod": remote_pod_mock,
+ "pod": resumed_pod_mock,
"operator": k,
"context": context,
+ "event": callback_event,
}
# check on_pod_cleanup callback
@@ -3979,6 +3983,62 @@ class TestKubernetesPodOperatorAsync:
mock_invoke_defer.assert_called_once()
assert op.trigger_kwargs["_redefer_count"] == 1
+ @patch(KUB_OP_PATH.format("invoke_defer_method"))
+ @patch(HOOK_CLASS)
+ def test_on_operator_resuming_fires_before_redefer(self, mocked_hook,
mock_invoke_defer):
+ """The resuming callback fires on re-entries that defer again, not
only terminal ones."""
+ from airflow.providers.cncf.kubernetes.callbacks import ExecutionMode
+
+ from unit.cncf.kubernetes.test_callbacks import (
+ MockKubernetesPodOperatorCallback,
+ MockWrapper,
+ )
+
+ MockWrapper.reset()
+ mock_callbacks = MockWrapper.mock_callbacks
+ mock_invoke_defer.side_effect = TaskDeferred(trigger=MagicMock(),
method_name="trigger_reentry")
+
+ base_container = MagicMock()
+ base_container.name = "base"
+ base_container.state.running = MagicMock()
+ base_container.state.terminated = None
+ base_container.state.waiting = None
+
+ remote_pod = MagicMock()
+ remote_pod.status.phase = "Running"
+ remote_pod.status.container_statuses = [base_container]
+ mocked_hook.return_value.get_pod.return_value = remote_pod
+
+ op = KubernetesPodOperator(
+ task_id="test_task",
+ name="test-pod",
+ deferrable=True,
+ callbacks=MockKubernetesPodOperatorCallback,
+ )
+ context = create_context(op)
+ event = {
+ "status": "error",
+ "message": "some transient error",
+ "name": TEST_NAME,
+ "namespace": TEST_NAMESPACE,
+ }
+
+ with pytest.raises(TaskDeferred):
+ op.trigger_reentry(context, event)
+
+ mock_callbacks.on_operator_resuming.assert_called_once()
+ assert mock_callbacks.on_operator_resuming.call_args.kwargs == {
+ "client": op.client,
+ "mode": ExecutionMode.SYNC,
+ "pod": remote_pod,
+ "operator": op,
+ "context": context,
+ "event": event,
+ }
+ # The task has not finished yet, so the terminal callbacks must stay
untouched.
+ mock_callbacks.on_pod_completion.assert_not_called()
+ mock_callbacks.on_pod_cleanup.assert_not_called()
+
@patch(KUB_OP_PATH.format("invoke_defer_method"))
@patch(HOOK_CLASS)
def test_redefer_when_pod_pending(self, mocked_hook, mock_invoke_defer):