Skip to content

Fix race condition in KubernetesTaskRunner between shutdown and getKnownTasks#14030

Merged
clintropolis merged 12 commits intoapache:masterfrom
georgew5656:fixRaceCondition
Apr 10, 2023
Merged

Fix race condition in KubernetesTaskRunner between shutdown and getKnownTasks#14030
clintropolis merged 12 commits intoapache:masterfrom
georgew5656:fixRaceCondition

Conversation

@georgew5656
Copy link
Copy Markdown
Contributor

@georgew5656 georgew5656 commented Apr 5, 2023

Description

Discovered a race condition in the shutdown code while doing some additional testing around f60f377

If a task is shutdown, has its k8s job deleted and then subsequently removed from the tasks map on https://github.com/apache/druid/blob/master/extensions-contrib/kubernetes-overlord-extensions/src/main/java/org/apache/druid/k8s/overlord/KubernetesTaskRunner.java#L218,
it is possible for the pod to still be running in a terminating state for a little while after. this is because k8s deletes the controller (the job) and then lets the pod clean itself up.

If getKnownTasks (https://github.com/apache/druid/blob/master/extensions-contrib/kubernetes-overlord-extensions/src/main/java/org/apache/druid/k8s/overlord/KubernetesTaskRunner.java#L338) is called by the TaskQueue main loop between when the tasks table is cleaned up and the peon pod finishes deleting, another run() will be called by getKnownTasks.

Normally this sees the existing future in the tasks table and returns it, but since the task id will already have been deleted from the tasks table, the task runner will actually start another job with the same name as the original one that was deleted.

This job will quickly be deleted again (because getKnownTasks will return it, the taskQueue will see the task that the task runner returned is not in TaskStorage and submit another shutdown request to delete the job), but this is a poor user experience.

Additionally, there may be DuplicateKeyError's thrown because there are multiple pods with the same job name running, this can cause the ingestion to be briefly nonresponsive.

It is also possible to get these DuplicateKeyErrors if a pod (not a job) is manually deleted in K8s. Normally, since we set backoffLimit to 0, if a pod fails, the job doesn't try to retry creating the pod and just fails out. But it seems like k8s treats manual termination differently from a pod failing, so when a pod is manually terminated, the job actually starts another pod.

IMO it makes more sense for getKnownTasks to try to rehydrate state from Kubernetes via the list of jobs, rather than the list of Pods. This solves both of the above problems because we can be sure the K8s job has been deleted before deleting the task future from the tasks map, and there will never be duplicate jobs with the same name.

Release note

Bugfixes to the kubernetes overlord extension

Key changed/added classes in this PR
  • The change here to have toTask take in the Job object rather than the Pod object and then use this change to have getKnownTasks and getRunningTasks list jobs rather than pods when trying to read the k8s state.

I explored some other options for fixing this issue, such as excluding terminating pods from getKNownTasks and getRunningTasks or having the main run loop wait for pods to delete if the job has been deleted, but this seemed like the cleanest solution.

This PR has:

  • [ X been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • a release note entry in the PR description.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • added integration tests.
  • been tested in a test Druid cluster.
    I have tested more thoroughly with the PodTemplateTaskAdapter and done some smoke tests with the MultiContainerTaskAdapter.

George Wu and others added 9 commits March 31, 2023 16:36
List<TaskRunnerWorkItem> result = new ArrayList<>();
for (Pod existingTask : client.listPeonPods(Sets.newHashSet(PeonPhase.RUNNING))) {
for (Job existingTask : client.listAllPeonJobs().stream()
.filter(job -> job.getStatus() != null && job.getStatus().getActive() != null && job.getStatus().getActive() > 0).collect(Collectors.toSet())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of defining logic for whether the job is active, succeeded or failed in various places. Can you create a class with a method for each state. I.E

JobStatus.isActive(Job job)
JobStatus.isSucceeded(Job job)
JobStatus.isFailed(Job job)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i added a class to do this, I think it may change slightly once #14028 (review) is merged so I'll have to publish a new version once thats merged

{
List<TaskRunnerWorkItem> result = new ArrayList<>();
for (Pod existingTask : client.listPeonPods()) {
for (Job existingTask : client.listAllPeonJobs()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listPeonPods() and listPeonPods(Set<PeonPhase> phases) methods will be unused if you make this change right? If so please remove them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is done

Copy link
Copy Markdown
Contributor

@nlippis nlippis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, let's merge after #14028 is in.

@nlippis
Copy link
Copy Markdown
Contributor

nlippis commented Apr 5, 2023

Looks good, let's merge after #14028 is in.

#14028 has been merged, please pull in those changes.

@georgew5656
Copy link
Copy Markdown
Contributor Author

georgew5656 commented Apr 5, 2023

Looks good, let's merge after #14028 is in.

just resolved merge conflicts, gonna do some more smoke testing with the new fabric8 clients once I can figure out how to get the changes from #14028 deployed to my environment but this should be good to go

client.pods().inNamespace("test").create(pod);
PodList podList = client.pods().inNamespace("test").list();
assertEquals(1, podList.getItems().size());
client.batch().v1().jobs().inNamespace("test").create(jobFromSpec);

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation

Invoking [ItemWritableOperation.create](1) should be avoided because it has been deprecated.
@georgew5656
Copy link
Copy Markdown
Contributor Author

Looks good, let's merge after #14028 is in.

just resolved merge conflicts, gonna do some more smoke testing with the new fabric8 clients once I can figure out how to get the changes from #14028 deployed to my environment but this should be good to go

@nlippis i got this deployed into my testing environment and it looks good to go with the new fabric8 client, other than the missing dependency from #14052

@clintropolis clintropolis merged commit 00d777d into apache:master Apr 10, 2023
@clintropolis clintropolis added this to the 26.0 milestone Apr 10, 2023
clintropolis pushed a commit to clintropolis/druid that referenced this pull request Apr 10, 2023
…ownTasks (apache#14030)

* Fix issues with null pointers on jobResponse

* fix unit tests

* Update extensions-contrib/kubernetes-overlord-extensions/src/main/java/org/apache/druid/k8s/overlord/common/DruidKubernetesPeonClient.java

Co-authored-by: Abhishek Agarwal <1477457+abhishekagarwal87@users.noreply.github.com>

* nullable

* fix error message

* Use jobs for known tasks instead of pods

* Remove log lines

* remove log lines

* PR change requests

* revert wait change

---------

Co-authored-by: Abhishek Agarwal <1477457+abhishekagarwal87@users.noreply.github.com>
clintropolis added a commit that referenced this pull request Apr 13, 2023
…ownTasks (#14030) (#14057)

* Fix issues with null pointers on jobResponse

* fix unit tests

* Update extensions-contrib/kubernetes-overlord-extensions/src/main/java/org/apache/druid/k8s/overlord/common/DruidKubernetesPeonClient.java



* nullable

* fix error message

* Use jobs for known tasks instead of pods

* Remove log lines

* remove log lines

* PR change requests

* revert wait change

---------

Co-authored-by: George Shiqi Wu <george.wu@imply.io>
Co-authored-by: Abhishek Agarwal <1477457+abhishekagarwal87@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants