-
Notifications
You must be signed in to change notification settings - Fork 295
Description
Java 25: Actions serialize due to ForkJoinPool.commonPool parallelism dropping to 1 in containers
Summary
When running Embabel on Java 25 inside containers (Docker/Kubernetes), all agent actions execute serially instead of in parallel. This is caused by Java 25's improved (more accurate) cgroup CPU detection, which reports availableProcessors() = 1 in CPU-constrained containers. Since ForkJoinPool.commonPool sizes its parallelism based on this value, the effective parallelism drops to 1, serializing all concurrent agent operations.
This is not a bug in Embabel per se — Embabel correctly respects the JVM's reported processor count. However, it represents a significant behavioral regression for users upgrading to Java 25, and warrants documentation and potentially a defensive minimum parallelism floor.
Reported Symptoms
- Two concurrent requests each take ~2x as long to return (serialized execution)
- Agents with multiple actions have each action executed sequentially
- Problem only manifests on Java 25; previous JVM versions work as expected
- Workaround:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=4restores parallel execution
Reported by Jacqui Collier (Discord, 2/26/2026).
Root Cause
Java 25 CPU detection change
Prior to Java 25, the JVM was imprecise when reading container CPU limits — it often saw the host's full CPU count rather than the container's cgroup allocation. Java 25 corrects this to accurately respect cgroup CPU limits (e.g., Kubernetes resources.limits.cpu).
Related OpenJDK ticket: JDK-8362881
Impact on ForkJoinPool
ForkJoinPool.commonPool() sizes itself as availableProcessors() - 1. When the container has no explicit CPU limit or is allocated 1 CPU:
| JVM Version | availableProcessors() | Common pool parallelism | Behavior |
|---|---|---|---|
| Pre-25 | Host CPU count (e.g., 8) | 7 | Actions run in parallel |
| 25 | Container CPU limit (e.g., 1) | 1 (minimum) | Actions serialize |
Embabel's dependency on the common pool
Action item: Investigate and document exactly which Embabel components rely on ForkJoinPool.commonPool vs. custom executors. Candidates to audit:
ExecutorAsyncer/Asyncer— async action dispatchParallelToolLoop— parallel tool executionScatterGather/aggregate— fan-out/fan-in patternsOperationScheduler/ProcessOptionsOperationScheduler— action scheduling- Any use of Kotlin coroutines
Dispatchers.Default(backed byForkJoinPool)
If Embabel delegates entirely to the common pool, this is purely a JVM/container configuration issue. If Embabel configures its own thread pools, those may also need to handle the availableProcessors() = 1 case.
Workarounds
Users can apply any of the following immediately:
Option 1: Set container CPU limits (preferred)
Configure appropriate CPU limits so Java 25 detects them correctly:
# Kubernetes
resources:
limits:
cpu: "4"
requests:
cpu: "2"
# Docker
docker run --cpus=4 ...
Option 2: Override ForkJoinPool parallelism
java -Djava.util.concurrent.ForkJoinPool.common.parallelism=4 -jar agent.jar
Option 3: Override JVM processor count detection
java -XX:ActiveProcessorCount=4 -jar agent.jar
Note: Option 1 is preferred because it provides accurate information to the JVM. Options 2 and 3 are overrides that may mask real resource constraints.
Proposed Actions
1. Documentation (high priority)
Add a section to the deployment/operations guide covering:
- Java 25 container CPU detection behavior change
- Recommended Kubernetes/Docker CPU configuration
- JVM flag workarounds
- How Embabel's parallel execution depends on
availableProcessors()
2. Audit parallelism dependencies (medium priority)
Determine which Embabel components use ForkJoinPool.commonPool vs. custom executors, and document the dependency chain from availableProcessors() through to action execution.
3. Consider a defensive parallelism floor (discussion)
Evaluate whether Embabel should ensure a minimum parallelism level (e.g., 2-4) for its concurrent operations, rather than silently degrading to single-threaded execution. This could be:
- A configuration property:
embabel.agent.parallelism.minimum=2 - A startup warning when
availableProcessors() == 1and parallel features are in use - An auto-tuning default that uses
max(availableProcessors(), 2)
Trade-off: a minimum floor improves resilience against container misconfigurations but could cause resource contention if the container genuinely only has 1 CPU.
Environment
- Java version: 25 (EA or GA)
- Runtime: Containerized (Docker / Kubernetes)
- Container CPU config: Default or
limits.cpu: 1 - Embabel version: 0.3.x+
References
- JDK-8362881 — OpenJDK threading changes
- ForkJoinPool javadoc — common pool parallelism defaults
- Discord report: Jacqui Collier, 2/26/2026
- Community input: vidya333 (Discord, 2/26/2026) — identified root cause as JVM CPU detection
Summary
When running Embabel on Java 25 inside containers (Docker/Kubernetes), all agent actions execute serially instead of in parallel. This is caused by Java 25's improved (more accurate) cgroup CPU detection, which reports availableProcessors() = 1 in CPU-constrained containers. Since ForkJoinPool.commonPool sizes its parallelism based on this value, the effective parallelism drops to 1, serializing all concurrent agent operations.
This is not a bug in Embabel per se — Embabel correctly respects the JVM's reported processor count. However, it represents a significant behavioral regression for users upgrading to Java 25, and warrants documentation and potentially a defensive minimum parallelism floor.
Reported Symptoms
- Two concurrent requests each take ~2x as long to return (serialized execution)
- Agents with multiple actions have each action executed sequentially
- Problem only manifests on Java 25; previous JVM versions work as expected
- Workaround:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=4restores parallel execution
Reported by Jacqui Collier (Discord, 2/26/2026).
Root Cause
Java 25 CPU detection change
Prior to Java 25, the JVM was imprecise when reading container CPU limits — it often saw the host's full CPU count rather than the container's cgroup allocation. Java 25 corrects this to accurately respect cgroup CPU limits (e.g., Kubernetes resources.limits.cpu).
Related OpenJDK ticket: [JDK-8362881](https://bugs.openjdk.org/browse/JDK-8362881)
Impact on ForkJoinPool
ForkJoinPool.commonPool() sizes itself as availableProcessors() - 1. When the container has no explicit CPU limit or is allocated 1 CPU:
| JVM Version | availableProcessors() |
Common pool parallelism | Behavior |
|---|---|---|---|
| Pre-25 | Host CPU count (e.g., 8) | 7 | Actions run in parallel |
| 25 | Container CPU limit (e.g., 1) | 1 (minimum) | Actions serialize |
Embabel's dependency on the common pool
Action item: Investigate and document exactly which Embabel components rely on ForkJoinPool.commonPool vs. custom executors. Candidates to audit:
ExecutorAsyncer/Asyncer— async action dispatchParallelToolLoop— parallel tool executionScatterGather/aggregate— fan-out/fan-in patternsOperationScheduler/ProcessOptionsOperationScheduler— action scheduling- Any use of Kotlin coroutines
Dispatchers.Default(backed byForkJoinPool)
If Embabel delegates entirely to the common pool, this is purely a JVM/container configuration issue. If Embabel configures its own thread pools, those may also need to handle the availableProcessors() = 1 case.
Workarounds
Users can apply any of the following immediately:
Option 1: Set container CPU limits (preferred)
Configure appropriate CPU limits so Java 25 detects them correctly:
# Kubernetes
resources:
limits:
cpu: "4"
requests:
cpu: "2"# Docker
docker run --cpus=4 ...Option 2: Override ForkJoinPool parallelism
java -Djava.util.concurrent.ForkJoinPool.common.parallelism=4 -jar agent.jarOption 3: Override JVM processor count detection
java -XX:ActiveProcessorCount=4 -jar agent.jarNote: Option 1 is preferred because it provides accurate information to the JVM. Options 2 and 3 are overrides that may mask real resource constraints.
Proposed Actions
1. Documentation (high priority)
Add a section to the deployment/operations guide covering:
- Java 25 container CPU detection behavior change
- Recommended Kubernetes/Docker CPU configuration
- JVM flag workarounds
- How Embabel's parallel execution depends on
availableProcessors()
2. Audit parallelism dependencies (medium priority)
Determine which Embabel components use ForkJoinPool.commonPool vs. custom executors, and document the dependency chain from availableProcessors() through to action execution.
3. Consider a defensive parallelism floor (discussion)
Evaluate whether Embabel should ensure a minimum parallelism level (e.g., 2-4) for its concurrent operations, rather than silently degrading to single-threaded execution. This could be:
- A configuration property:
embabel.agent.parallelism.minimum=2 - A startup warning when
availableProcessors() == 1and parallel features are in use - An auto-tuning default that uses
max(availableProcessors(), 2)
Trade-off: a minimum floor improves resilience against container misconfigurations but could cause resource contention if the container genuinely only has 1 CPU.
Environment
- Java version: 25 (EA or GA)
- Runtime: Containerized (Docker / Kubernetes)
- Container CPU config: Default or
limits.cpu: 1 - Embabel version: 0.3.x+
References
- [JDK-8362881](https://bugs.openjdk.org/browse/JDK-8362881) — OpenJDK threading changes
- [ForkJoinPool javadoc](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/ForkJoinPool.html) — common pool parallelism defaults
- Discord report: Jacqui Collier, 2/26/2026
- Community input: vidya333 (Discord, 2/26/2026) — identified root cause as JVM CPU detection