Skip to content

Commit 324802d

Browse files
authored
Limit max pool size for parallel execution via config param (#3044)
JUnit Jupiter (and The JUnit Platform) now support limiting the maximum number of concurrently executing tests via the `junit.jupiter.execution.parallel.config.fixed.max-pool-size` configuration parameter. With Java 9+ the `ForkJoinPool` used by JUnit can be configured with a maximum pool size. While the number of concurrently executing tests may exceed the configured parallelism when tests become blocked, it will not exceed the maximum pool size. Additionally, because the `ForkJoinPool` will by default reject tasks that would exceed the maximum pool size the `junit.jupiter.execution.parallel.config.fixed.saturate` property has been added and will default to `true`. There appears to be no reason to ever set this `false` but it is there should someone depend on the old behavior. These changes were intentionally not made to the `dynamic` strategy to limit the scope of this pull request. While I can reasonably predict what behavior users of the `fixed` strategy might expect, I can not say the same about the `dynamic` strategy. Fixes #3026. Fixes #2545. Fixes #1858.
1 parent 5e853e3 commit 324802d

File tree

6 files changed

+133
-21
lines changed

6 files changed

+133
-21
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ repository on GitHub.
2626

2727
==== New Features and Improvements
2828

29+
* Support for limiting the `max-pool-size` for parallel execution via a configuration parameter
30+
2931
* All utility methods from `ReflectionSupport` now have counterparts returning `Stream`
3032
instead of `List`.
3133

@@ -38,10 +40,15 @@ repository on GitHub.
3840

3941
==== Deprecations and Breaking Changes
4042

41-
* ❓
43+
* The `fixed` parallel execution strategy now allows the thread pool to be saturated by
44+
default.
4245

4346
==== New Features and Improvements
4447

48+
* New `junit.jupiter.execution.parallel.config.fixed.max-pool-size` configuration
49+
parameter to set the maximum pool size.
50+
* New `junit.jupiter.execution.parallel.config.fixed.saturate` configuration
51+
parameter to disable pool saturation.
4552
* New `ArgumentsAccessor.getInvocationIndex()` method that supplies the index of a
4653
`@ParameterizedTest` invocation.
4754
* `DisplayNameGenerator` methods are now allowed to return `null`, in order to signal

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2347,6 +2347,8 @@ configuration parameter to one of the following options.
23472347
`fixed`::
23482348
Uses the mandatory `junit.jupiter.execution.parallel.config.fixed.parallelism`
23492349
configuration parameter as the desired parallelism.
2350+
The optional `junit.jupiter.execution.parallel.config.fixed.max-pool-size`
2351+
configuration parameter can be used to limit the maximum number of threads.
23502352

23512353
`custom`::
23522354
Allows you to specify a custom `{ParallelExecutionConfigurationStrategy}`
@@ -2357,13 +2359,15 @@ If no configuration strategy is set, JUnit Jupiter uses the `dynamic` configurat
23572359
strategy with a factor of `1`. Consequently, the desired parallelism will be equal to the
23582360
number of available processors/cores.
23592361

2360-
.Parallelism does not imply maximum number of concurrent threads
2361-
NOTE: JUnit Jupiter does not guarantee that the number of concurrently executing tests
2362-
will not exceed the configured parallelism. For example, when using one of the
2363-
synchronization mechanisms described in the next section, the `ForkJoinPool` that is used
2364-
behind the scenes may spawn additional threads to ensure execution continues with
2365-
sufficient parallelism. Thus, if you require such guarantees in a test class, please use
2366-
your own means of controlling concurrency.
2362+
.Parallelism alone does not imply maximum number of concurrent threads
2363+
NOTE: By default JUnit Jupiter does not guarantee that the number of concurrently
2364+
executing tests will not exceed the configured parallelism. For example, when using one
2365+
of the synchronization mechanisms described in the next section, the `ForkJoinPool` that
2366+
is used behind the scenes may spawn additional threads to ensure execution continues with
2367+
sufficient parallelism.
2368+
If you require such guarantees, with Java 9+, it is possible to limit the maximum number
2369+
of concurrent threads by controlling the maximum pool size of the `fixed` and `custom`
2370+
strategies.
23672371

23682372
[[writing-tests-parallel-execution-config-properties]]
23692373
===== Relevant properties
@@ -2415,6 +2419,20 @@ The following table lists relevant properties for configuring parallel execution
24152419
| a positive integer
24162420
| no default value
24172421

2422+
| ```junit.jupiter.execution.parallel.config.fixed.max-pool-size```
2423+
| Desired maximum pool size of the underlying fork-join pool for the ```fixed```
2424+
configuration strategy
2425+
| a positive integer, must greater than or equal to `junit.jupiter.execution.parallel.config.fixed.parallelism`
2426+
| 256 + the value of `junit.jupiter.execution.parallel.config.fixed.parallelism`
2427+
2428+
| ```junit.jupiter.execution.parallel.config.fixed.saturate```
2429+
| Disable saturation of the underlying fork-join pool for the ```fixed``` configuration
2430+
strategy
2431+
|
2432+
* `true`
2433+
* `false`
2434+
| ```true```
2435+
24182436
| ```junit.jupiter.execution.parallel.config.custom.class```
24192437
| Fully qualified class name of the _ParallelExecutionConfigurationStrategy_ to be
24202438
used for the ```custom``` configuration strategy

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import static org.apiguardian.api.API.Status.STABLE;
1616
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_CUSTOM_CLASS_PROPERTY_NAME;
1717
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME;
18+
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME;
1819
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_FIXED_PARALLELISM_PROPERTY_NAME;
20+
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_FIXED_SATURATE_PROPERTY_NAME;
1921
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_STRATEGY_PROPERTY_NAME;
2022

2123
import org.apiguardian.api.API;
@@ -166,6 +168,40 @@ public final class Constants {
166168
public static final String PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
167169
+ CONFIG_FIXED_PARALLELISM_PROPERTY_NAME;
168170

171+
/**
172+
* Property name used to configure the maximum pool size of the underlying
173+
* fork-join pool for the {@code fixed} configuration strategy: {@value}
174+
*
175+
* <p>Value must be an integer and greater than or equal to
176+
* {@value #PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME}; defaults to
177+
* {@code 256 + fixed.parallelism}.
178+
*
179+
* <p>Note: This property only takes affect on Java 9+.
180+
*
181+
* @since 5.10
182+
*/
183+
@API(status = EXPERIMENTAL, since = "5.10")
184+
public static final String PARALLEL_CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
185+
+ CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME;
186+
187+
/**
188+
* Property name used to disable saturation of the underlying fork-join pool
189+
* for the {@code fixed} configuration strategy: {@value}
190+
*
191+
* <p>When set to {@code false} the underlying fork-join pool will reject
192+
* additional tasks if all available workers are busy and the maximum
193+
* pool-size would be exceeded.
194+
195+
* <p>Value must either {@code true} or {@code false}; defaults to {@code true}.
196+
*
197+
* <p>Note: This property only takes affect on Java 9+.
198+
*
199+
* @since 5.10
200+
*/
201+
@API(status = EXPERIMENTAL, since = "5.10")
202+
public static final String PARALLEL_CONFIG_FIXED_SATURATE_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
203+
+ CONFIG_FIXED_SATURATE_PROPERTY_NAME;
204+
169205
/**
170206
* Property name used to set the factor to be multiplied with the number of
171207
* available processors/cores to determine the desired parallelism for the

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfiguration.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
package org.junit.platform.engine.support.hierarchical;
1212

13+
import java.util.concurrent.ForkJoinPool;
14+
import java.util.function.Predicate;
15+
1316
/**
1417
* @since 1.3
1518
*/
@@ -20,14 +23,16 @@ class DefaultParallelExecutionConfiguration implements ParallelExecutionConfigur
2023
private final int maxPoolSize;
2124
private final int corePoolSize;
2225
private final int keepAliveSeconds;
26+
private final Predicate<? super ForkJoinPool> saturate;
2327

2428
DefaultParallelExecutionConfiguration(int parallelism, int minimumRunnable, int maxPoolSize, int corePoolSize,
25-
int keepAliveSeconds) {
29+
int keepAliveSeconds, Predicate<? super ForkJoinPool> saturate) {
2630
this.parallelism = parallelism;
2731
this.minimumRunnable = minimumRunnable;
2832
this.maxPoolSize = maxPoolSize;
2933
this.corePoolSize = corePoolSize;
3034
this.keepAliveSeconds = keepAliveSeconds;
35+
this.saturate = saturate;
3136
}
3237

3338
@Override
@@ -55,4 +60,8 @@ public int getKeepAliveSeconds() {
5560
return keepAliveSeconds;
5661
}
5762

63+
@Override
64+
public Predicate<? super ForkJoinPool> getSaturatePredicate() {
65+
return saturate;
66+
}
5867
}

junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategy.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,14 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
4242
() -> new JUnitException(String.format("Configuration parameter '%s' must be set",
4343
CONFIG_FIXED_PARALLELISM_PROPERTY_NAME)));
4444

45-
return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism,
46-
KEEP_ALIVE_SECONDS);
45+
int maxPoolSize = configurationParameters.get(CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME,
46+
Integer::valueOf).orElse(parallelism + 256);
47+
48+
boolean saturate = configurationParameters.get(CONFIG_FIXED_SATURATE_PROPERTY_NAME,
49+
Boolean::valueOf).orElse(true);
50+
51+
return new DefaultParallelExecutionConfiguration(parallelism, parallelism, maxPoolSize, parallelism,
52+
KEEP_ALIVE_SECONDS, __ -> saturate);
4753
}
4854
},
4955

@@ -66,7 +72,7 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
6672
factor.multiply(BigDecimal.valueOf(Runtime.getRuntime().availableProcessors())).intValue());
6773

6874
return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism,
69-
KEEP_ALIVE_SECONDS);
75+
KEEP_ALIVE_SECONDS, null);
7076
}
7177
},
7278

@@ -114,6 +120,36 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
114120
*/
115121
public static final String CONFIG_FIXED_PARALLELISM_PROPERTY_NAME = "fixed.parallelism";
116122

123+
/**
124+
* Property name used to configure the maximum pool size of the underlying
125+
* fork-join pool for the {@link #FIXED} configuration strategy.
126+
*
127+
* <p>Value must be an integer and greater than or equal to
128+
* {@value #CONFIG_FIXED_PARALLELISM_PROPERTY_NAME}; defaults to
129+
* {@code 256 + fixed.parallelism}.
130+
*
131+
* @since 1.10
132+
* @see #FIXED
133+
*/
134+
@API(status = EXPERIMENTAL, since = "1.10")
135+
public static final String CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME = "fixed.max-pool-size";
136+
137+
/**
138+
* Property name used to disable saturation of the underlying fork-join pool
139+
* for the {@link #FIXED} configuration strategy.
140+
*
141+
* <p>When set to {@code false} the underlying fork-join pool will reject
142+
* additional tasks if all available workers are busy and the maximum
143+
* pool-size would be exceeded.
144+
* <p>Value must either {@code true} or {@code false}; defaults to {@code true}.
145+
*
146+
* @since 1.10
147+
* @see #FIXED
148+
* @see #CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME
149+
*/
150+
@API(status = EXPERIMENTAL, since = "1.10")
151+
public static final String CONFIG_FIXED_SATURATE_PROPERTY_NAME = "fixed.saturate";
152+
117153
/**
118154
* Property name of the factor used to determine the desired parallelism for the
119155
* {@link #DYNAMIC} configuration strategy.

platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategyTests.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
import static org.mockito.Mockito.when;
1818

1919
import java.util.Optional;
20-
import java.util.concurrent.ForkJoinPool;
21-
import java.util.function.Predicate;
2220

2321
import org.junit.jupiter.api.BeforeEach;
2422
import org.junit.jupiter.api.Test;
@@ -51,7 +49,20 @@ void fixedStrategyCreatesValidConfiguration() {
5149
assertThat(configuration.getMinimumRunnable()).isEqualTo(42);
5250
assertThat(configuration.getMaxPoolSize()).isEqualTo(256 + 42);
5351
assertThat(configuration.getKeepAliveSeconds()).isEqualTo(30);
54-
assertThat(configuration.getSaturatePredicate()).isNull();
52+
assertThat(configuration.getSaturatePredicate().test(null)).isTrue();
53+
}
54+
55+
@Test
56+
void fixedSaturateStrategyCreatesValidConfiguration() {
57+
when(configParams.get("fixed.parallelism")).thenReturn(Optional.of("42"));
58+
when(configParams.get("fixed.max-pool-size")).thenReturn(Optional.of("42"));
59+
when(configParams.get("fixed.saturate")).thenReturn(Optional.of("false"));
60+
61+
ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.FIXED;
62+
var configuration = strategy.createConfiguration(configParams);
63+
assertThat(configuration.getParallelism()).isEqualTo(42);
64+
assertThat(configuration.getMaxPoolSize()).isEqualTo(42);
65+
assertThat(configuration.getSaturatePredicate().test(null)).isFalse();
5566
}
5667

5768
@Test
@@ -183,12 +194,7 @@ void customStrategyThrowsExceptionWhenClassDoesNotExist() {
183194
static class CustomParallelExecutionConfigurationStrategy implements ParallelExecutionConfigurationStrategy {
184195
@Override
185196
public ParallelExecutionConfiguration createConfiguration(ConfigurationParameters configurationParameters) {
186-
return new DefaultParallelExecutionConfiguration(1, 2, 3, 4, 5) {
187-
@Override
188-
public Predicate<? super ForkJoinPool> getSaturatePredicate() {
189-
return __ -> true;
190-
}
191-
};
197+
return new DefaultParallelExecutionConfiguration(1, 2, 3, 4, 5, __ -> true);
192198
}
193199
}
194200

0 commit comments

Comments
 (0)