Add --min-completed-minutes to cleanup-pods to prevent KPO race condition - #70595
Merged
eladkal merged 12 commits intoJul 30, 2026
Merged
Conversation
…tion
The ``airflow kubernetes cleanup-pods`` command currently deletes
Succeeded/Failed/Evicted pods immediately, with no minimum-age guard
for terminal states. ``KubernetesPodOperator`` in synchronous mode
polls pod status every ~2 seconds via ``await_pod_completion``. If
the cleanup job fires in the window between the pod reaching
``Succeeded`` and KPO's next poll, KPO receives a 404 and fails the
task -- even though the pod completed successfully (exit code 0).
A ``--min-pending-minutes`` guard already exists for Pending pods
(default 30 m, floor 5 m). No equivalent exists for terminal states.
This commit adds ``--min-completed-minutes`` (default ``0``, which
preserves the existing behaviour). When set to any positive value,
Succeeded/Failed/Evicted pods are skipped unless their completion time
is older than the threshold. Completion time is derived from the
latest ``containerStatuses[*].state.terminated.finishedAt`` timestamp
(falls back to ``metadata.creationTimestamp`` for pods that were
evicted before any container started).
Root-cause investigation
------------------------
This was confirmed via Kubernetes API server audit logs on a production
EKS cluster. Timeline for an affected KPO task:
13:15:12Z KPO polls pod → phase Running
13:15:14Z Container exits with code 0 (pod transitions to Succeeded)
13:15:17Z cleanup-pods CronJob deletes the pod (3 s after completion)
13:15:18Z KPO polls pod → 404 Not Found → task marked FAILED
The pod had succeeded; the task failure was a false positive caused
entirely by the race. Multiple production DAGs exhibited the same
pattern with the cleanup CronJob set to run every 5 minutes.
Reducing the CronJob frequency is a partial mitigation (lowers the
probability) but does not eliminate the race. Setting
``--min-completed-minutes=5`` gives KPO a 5-minute window to observe
the terminal phase -- 150x wider than the 2 s poll interval -- closing
the race completely in practice.
Changes
-------
* ``definition.py`` – add ``ARG_MIN_COMPLETED_MINUTES``; wire into
cleanup-pods args tuple
* ``kubernetes_command.py`` – add ``_get_pod_completion_time()`` helper;
gate terminal-pod deletion by age when
``min_completed_minutes > 0``
* ``test_kubernetes_command.py`` – 4 new unit tests covering:
- Succeeded pod too young → not deleted
- Succeeded pod old enough → deleted
- Default (0) preserves immediate-deletion behaviour
- Failed/Never pod too young → not deleted
* ``changelog.rst`` – entry under 10.21.0
CLI docs (``cli-ref.rst``) are auto-generated via ``.. argparse::``
and will pick up the new flag automatically.
noamst-monday
requested review from
hussein-awala,
jedcunningham and
jscheffl
as code owners
July 28, 2026 11:48
|
Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
|
noamst-monday
marked this pull request as draft
July 28, 2026 11:52
noamst-monday
marked this pull request as ready for review
July 28, 2026 13:24
eladkal
reviewed
Jul 28, 2026
noamst-monday
commented
Jul 28, 2026
noamst-monday
commented
Jul 28, 2026
jedcunningham
requested changes
Jul 28, 2026
- Set default to 1 minute (no reason to keep the race by default) - Shorten --min-completed-minutes help text - Fix _get_pod_completion_time: scan init_container_statuses too; fall back to max(conditions.last_transition_time) instead of creation_timestamp (which predates actual completion) - Add TestGetPodCompletionTime with real k8s model objects covering main-only, init-only, both, conditions fallback, and creation_timestamp last-resort cases - Fix test_cleanup_min_completed_zero_deletes_immediately to pass --min-completed-minutes=0 explicitly now that default is 1
noamst-monday
marked this pull request as draft
July 29, 2026 10:09
The guard must not fall through to creation_timestamp for pods whose containers never reached a terminated state — that path reports an inflated age and deletes immediately, the unsafe direction. Freezing time keeps every case at a realistic completion offset instead of a timestamp in the future.
_get_pod_completion_time guards three levels of the container state, but the fallback cases only reached the first two, so a terminated status carrying no finishedAt went unexercised. Modelling the non-terminated case as a waiting container also matches what k8s reports for a pod evicted before its containers started.
noamst-monday
marked this pull request as ready for review
July 29, 2026 10:42
jedcunningham
approved these changes
Jul 30, 2026
eladkal
approved these changes
Jul 30, 2026
|
Awesome work, congrats on your first merged pull request! You are invited to check our Issue Tracker for additional contributions. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
KubernetesPodOperatorin synchronous (deferrable=False) mode callsPodManager.await_pod_completionto confirm the pod reached a terminal phase. This method pollsread_podevery 2 seconds (pod_manager.py:839, hardcodedtime.sleep(2)).When a pod transitions to
Succeeded, there is up to a 2-second window beforePodManager.await_pod_completionobserves the terminal phase. Theairflow kubernetes cleanup-podscommand currently deletes Succeeded/Failed/Evicted pods immediately — there is no minimum-age guard for terminal states.If the cleanup job fires during that window,
PodManager.await_pod_completion's nextread_podcall returns 404 and the task is marked FAILED, even though the pod completed successfully (exit code 0).A
--min-pending-minutesguard already exists for Pending pods (default 30 m, minimum 5 m). No equivalent exists for terminal states.Real-world reproduction
Confirmed via Kubernetes API server audit logs on a production EKS cluster running
apache-airflow-providers-cncf-kubernetes==10.19.0on Airflow 3.2.2. The cleanup CronJob was set to run every 5 minutes.Timeline of a representative failure:
Airflow task log excerpt:
Audit log at deletion time confirmed
phase: Succeeded,containerStatuses[0].state.terminated.exitCode: 0,finishedAt: 13:15:14Z.Related
PR #69269 fixed a similar 404 race in
is_istio_enabledfor the deferrabletrigger_reentrypath (10.20.0). This PR addresses the complementary gap: preventing the race at source by not deleting recently-completed pods.Solution
Add
--min-completed-minutes(default1) to thecleanup-podscommand. When set, Succeeded/Failed/Evicted pods are skipped unless their completion time exceeds the threshold. Set to0to restore the previous immediate-deletion behaviour.Completion time is derived from the latest
finished_atacross bothcontainer_statusesandinit_container_statuses. Falls back to the latestconditions[*].last_transition_time(updated at the terminal transition), then tocreation_timestampas a last resort.Changes
cli/definition.pyARG_MIN_COMPLETED_MINUTES(default1,allow_zero=True)cli/kubernetes_command.py_get_pod_completion_time(); gate terminal-pod deletion by agetests/.../test_kubernetes_command.pyTestGetPodCompletionTimewith real k8s model objects; 4 integration-style tests for the new flagdocs/changelog.rstCLI reference docs (
cli-ref.rst) use.. argparse::and pick up the new flag automatically.Testing
pytest providers/cncf/kubernetes/tests/unit/cncf/kubernetes/cli/test_kubernetes_command.py -q # 19 passedUsage
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Sonnet 4.6 following the guidelines