kestra-io / kestra-io/plugin-kubernetes

Thread Leak: OOM After 24h Operation (1,309 Threads)

Open
#237 4 comments 0 reactions 0 assignees View on GitHub
area/plugin good first issue
Dominant language
Java
Stars
14
Forks
20
Avg merge
1d 20h
Merged PRs (30d)
7

Description

# Thread Leak: OOM After 24h Operation (1,309 Threads)

## Problem

Production workers running Kubernetes plugin tasks experience unbounded thread growth, leading to OOM after ~24 hours.

**Observed symptoms:**
- Thread count: 1,309 (261% over configured 500-thread limit)
- 168 leaked threads in 89 orphaned executor pools (pattern: `-2073792911-pool-*`)
- Threads stuck in TIMED_WAITING, never terminated
- Memory exhaustion despite adequate heap space (1.97GB used of 4GB)

**Thread dump evidence:**
```
"-2073792911-pool-734325-thread-1" daemon TIMED_WAITING
at java.util.concurrent.SynchronousQueue.poll()
at java.util.concurrent.ThreadPoolExecutor.getTask()
at java.util.concurrent.ThreadPoolExecutor.runWorker()
```

## Affected Task

**Only `PodCreate`** - all kubectl tasks (`Apply`, `Get`, `Delete`, `Patch`, `Restart`) are unaffected.

## Environment

- Kestra v1.1.3
- Plugin-kubernetes (current version)
- ~1,000,000 executions daily
- EKS v1.33.5, workers: 4 CPU / 8 GB RAM

## Analysis

### Try-With-Resources Pattern (Appears Correct)

[`PodCreate.java:355-356`](https://github.com/kestra-io/plugin-kubernetes/blob/main/src/main/java/io/kestra/plugin/kubernetes/core/PodCreate.java#L355-L356):
```java
try (KubernetesClient client = PodService.client(runContext, this.getConnection());
PodLogService podLogService = new PodLogService(...)) {
// operations
}
```

Resources close in reverse order: `podLogService.close()`, then `client.close()`.

### Suspect: Async Executor Not Fully Terminated

**Theory (to be confirmed):**

[`PodLogService.java:196-198`](https://github.com/kestra-io/plugin-kubernetes/blob/main/src/main/java/io/kestra/plugin/kubernetes/services/PodLogService.java#L196-L198):
```java
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
// Returns immediately - executor thread may still be running
}
```

The scheduled executor (runs every 30s) captures `KubernetesClient` in its lambda closure:

[`PodLogService.java:64`](https://github.com/kestra-io/plugin-kubernetes/blob/main/src/main/java/io/kestra/plugin/kubernetes/services/PodLogService.java#L64):
```java
scheduledExecutor.scheduleAtFixedRate(
() -> {
PodResource podResource = PodService.podRef(client, pod); // Captures client
// ... creates watchLog() connections
},
0, 30, TimeUnit.SECONDS
);
```

**Suspected race condition:**
1. `podLogService.close()` calls `shutdownNow()` and returns immediately
2. Scheduled thread still running, holds reference to `client`
3. `client.close()` attempts to close internal OkHttp pools
4. Pools cannot fully close (reference still held)
5. OkHttp threads leak

**Missing:** `scheduledExecutor.awaitTermination(timeout, unit)` after `shutdownNow()`

### Why Hard to Detect

- Timing-dependent (race between cleanup and scheduled task)
- Small leak per operation (~2 threads)
- Daemon threads don't prevent JVM exit (unit tests pass)
- Only manifests under sustained load over 24+ hours

## Proposal

I suggest investigating whether:
1. The scheduled executor in `PodLogService` fully terminates before `client.close()` is called
2. Missing `awaitTermination()` is the root cause
3. Other async operations might hold client references

## Related

- [kestra#13129](https://github.com/kestra-io/kestra/issues/13129) - Original OOM report
- [Full analysis](https://gist.github.com/cf-sewe/6c3597cb03749ed77a5c7f36db801269) - Detailed investigation

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with PodLogService.java:64 and 196-198, then inspect PodCreate.java:355-356 to trace executor and client shutdown ordering. Reproduce repeated PodCreate operations under sustained load and observe executor termination, orphaned pools, and thread counts. Done means the root cause is confirmed or ruled out with evidence and the cleanup behavior is verified without leaked threads.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, kubernetes
Domain
devops
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.