Skip to content

Commit 8f624bf

Browse files
Merge pull request #4955 from yue9944882/fix/util-wait-executor-leak
[util] Shut down scheduled executor in Wait.poll to fix thread leak
2 parents 98927e0 + 97ad1ec commit 8f624bf

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

  • util/src

‎util/src/main/java/io/kubernetes/client/util/wait/Wait.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ public static boolean poll(
6868
}
6969
} catch (Exception e) {
7070
return result.get();
71+
} finally {
72+
future.cancel(true);
73+
executorService.shutdownNow();
7174
}
72-
future.cancel(true);
7375
return result.get();
7476
}
7577
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.util.wait;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
import java.time.Duration;
18+
import java.util.concurrent.atomic.AtomicReference;
19+
import org.junit.jupiter.api.Test;
20+
21+
class WaitTest {
22+
23+
/**
24+
* Regression test: {@link Wait#poll} must shut down the internal scheduled executor before
25+
* returning. Prior to the fix the executor was never shut down, so its (non-daemon) worker thread
26+
* stayed alive after the call, leaking a thread on every invocation.
27+
*/
28+
@Test
29+
void pollShutsDownExecutorThreadAfterReturning() throws InterruptedException {
30+
AtomicReference<Thread> workerThread = new AtomicReference<>();
31+
32+
boolean finished =
33+
Wait.poll(
34+
Duration.ofMillis(1),
35+
Duration.ofMillis(1),
36+
Duration.ofSeconds(5),
37+
() -> {
38+
workerThread.compareAndSet(null, Thread.currentThread());
39+
return true;
40+
});
41+
42+
assertThat(finished).isTrue();
43+
44+
Thread worker = workerThread.get();
45+
assertThat(worker).as("condition should have executed on an executor thread").isNotNull();
46+
47+
// After poll returns the executor must have been shut down, so its worker thread terminates.
48+
// Allow a short grace period for the interrupt/shutdown to take effect.
49+
long deadline = System.currentTimeMillis() + 2000;
50+
while (worker.isAlive() && System.currentTimeMillis() < deadline) {
51+
Thread.sleep(10);
52+
}
53+
54+
assertThat(worker.isAlive())
55+
.as("Wait.poll must shut down its executor so the worker thread is not leaked")
56+
.isFalse();
57+
}
58+
}

0 commit comments

Comments
 (0)