[SPARK-58725][K8S] Resolve the pod before deciding whether --kill found it - #57948
Open
LuciferYang wants to merge 2 commits into
Open
[SPARK-58725][K8S] Resolve the pod before deciding whether --kill found it#57948LuciferYang wants to merge 2 commits into
LuciferYang wants to merge 2 commits into
Conversation
`KillApplication.executeOnPod` null-checks the request handle returned by
`getPod` rather than the pod it resolves to. `getPod` hands back
`client.pods.inNamespace(ns).withName(name)`, a fabric8 `PodResource`, and
`withName` never returns null: it either throws for a null or empty name or
constructs a new handle. So `Option(podToDelete).isDefined` is always true and
the `printMessage("Application not found.")` branch cannot run.
`PodResource.delete()` on a name the API server does not have sends a real
DELETE, catches the 404 inside `BaseOperation.deleteAll()`, and returns an empty
list, which the caller discards. `spark-submit --kill <ns>:<wrong-name>` therefore
prints only the "Submitting a request to kill submission ..." banner and exits 0,
the same output a successful kill produces. `--status` on the same submission ID
does report "Application not found.", because `ListStatus.executeOnPod` calls
`get()` before its null check.
Resolve the handle the same way and reuse it for the delete, so the fix costs one
extra GET and the two subcommands agree. A 403 or 5xx still propagates a
`KubernetesClientException`; only the 404 case was silent.
The added test needs a real `PodResource` mock whose `get` is stubbed to null. An
unstubbed name would make Mockito's `withName` return null, which sends the buggy
check down the false branch for the wrong reason and passes without the fix.
…test stub The commit was authored while issues.apache.org was unreachable, so it landed as [MINOR] with no ticket. Add SPARK-58725 to the test name. Also remove `when(missingPodOperations.delete()).thenReturn(emptyList)` from the `before` block. No test can reach it: the only test using that mock asserts `verify(missingPodOperations, never()).delete()`. Leaving it in suggests the delete result is what reports absence, which is the approach this change deliberately did not take, since `Deletable.delete()` documents that the returned list is not guaranteed to be complete while `Gettable.get()` contracts null for a missing item.
uros-b
approved these changes
Aug 12, 2026
Member
|
Thank you @LuciferYang! |
Contributor
Author
|
Thank you @uros-b and cc @dongjoon-hyun FYI |
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.
What changes were proposed in this pull request?
Resolve the pod before deciding whether
spark-submit --killfound it.KillApplication.executeOnPodnull-checks the request handle returned bygetPodrather than the pod it resolves to. Callget()on the handle and check that instead, reusing the same handle for the delete.Why are the changes needed?
getPodreturnsclient.pods.inNamespace(ns).withName(name), a fabric8PodResourcerequest handle.BaseOperation.withNamenever returns null: it throwsIllegalArgumentException("Name must be provided.")for a null or empty name and otherwise constructs a new handle. SoOption(podToDelete).isDefinedis always true and theprintMessage("Application not found.")branch is unreachable.PodResource.delete()on a name the API server does not have sends a real DELETE.BaseOperation.deleteAll()catches the resulting 404 when a name is set and returnsCollections.emptyList(), and the caller discards that list.So
spark-submit --kill <ns>:<wrong-name> --master k8s://...prints only the "Submitting a request to kill submission ..." banner and exits 0, which is the same output a successful kill produces.--statuson the same submission ID does report "Application not found.", becauseListStatus.executeOnPodcallsget()before its null check. The two subcommands in the same file disagree about the same nonexistent pod.The check has been unreachable since the feature was added in 2019. fabric8 4.x had the same never-null
withNameand also swallowed the 404, returningBoolean.FALSEinstead of an empty list, and that value was discarded too.Three points a reviewer may want to weigh, all deliberate:
Scope. The silence is specific to the API server answering 404. A 403 or 5xx still propagates a
KubernetesClientExceptionand a nonzero exit, so this is a wrong-name or wrong-namespace defect rather than "kill never reports failure". Standalone mode does report a nonexistent driver ("Driver ... has already finished or does not exist"), so reporting is the convention this path was missing.The GET and DELETE are not atomic. If the pod disappears between them, fabric8 swallows the 404 and the command exits 0 having printed only the banner, which is what the old code did for a name that never existed. The race lands in the previous behavior rather than in a new error, so no retry loop is needed.
Gating on
delete()'s returnedList[StatusDetails]would avoid the extra GET and close that window, butDeletable.delete()documents that "It is not guaranteed that the returned list will contain all values marked for deletion", whileGettable.get()contracts "the item or null if the item doesn't exist". Depending on the first would let this bug reappear silently on a future client bump, with no unit test able to catch it.Also note that
--kill <name>without anns:prefix, against a kube config that supplies no namespace, now throws fromget()where it previously issued a silent no-op DELETE against a URL that could never match a pod.ListStatus.executeOnPodalready behaves that way on the same input, so the two subcommands end up equally exposed rather than--killbecoming worse than--status.Does this PR introduce any user-facing change?
Yes, on one path.
spark-submit --killagainst a driver pod that does not exist now prints "Application not found." instead of printing nothing. It previously exited 0 after issuing a DELETE whose 404 was swallowed. The exit code is unchanged, matching standalone mode, which also exits 0 for a nonexistent driver. No API or configuration change.How was this patch tested?
A test added to
K8sSubmitOpSuite, asserting both that the message is printed and that no delete is issued.The missing pod needs a real
PodResourcemock whosegetis stubbed to null. An unstubbed name would make Mockito'swithNamereturn null, which sends the buggy check down the false branch for the wrong reason and passes without the fix.Confirmed to fail against the unfixed tree:
(That run predated the JIRA id, so the test name in the captured output lacked the
SPARK-58725:prefix it carries now.)build/sbt -Pkubernetes 'kubernetes/testOnly org.apache.spark.deploy.k8s.submit.K8sSubmitOpSuite':kubernetes/scalastyleandkubernetes/Test/scalastylereport 0 errors.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)