Skip to content

Java 25 Parallelism #1454

@alexheifetz

Description

@alexheifetz

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=4 restores 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 dispatch
  • ParallelToolLoop — parallel tool execution
  • ScatterGather / aggregate — fan-out/fan-in patterns
  • OperationScheduler / ProcessOptionsOperationScheduler — action scheduling
  • Any use of Kotlin coroutines Dispatchers.Default (backed by ForkJoinPool)

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() == 1 and 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
# 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=4 restores 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 dispatch
  • ParallelToolLoop — parallel tool execution
  • ScatterGather / aggregate — fan-out/fan-in patterns
  • OperationScheduler / ProcessOptionsOperationScheduler — action scheduling
  • Any use of Kotlin coroutines Dispatchers.Default (backed by ForkJoinPool)

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() == 1 and 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

Metadata

Metadata

Labels

designdocumentationImprovements or additions to documentationenhancementNew feature or request

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions