Skip to content

Commit e48bd29

Browse files
authored
Limit max pool size for dynamic parallel execution (#3206)
This is a followup of #3044 for the `dynamic` strategy. Fixes #3205.
1 parent 918f097 commit e48bd29

File tree

4 files changed

+97
-9
lines changed

4 files changed

+97
-9
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ repository on GitHub.
4242
methods of the former. Please refer to the
4343
<<../user-guide/index.adoc#launcher-api-launcher-interceptors-custom, User Guide>> for
4444
details.
45+
* Support for limiting the `max-pool-size-factor` for parallel execution via a configuration parameter.
4546
* The new `testfeed` details mode for `ConsoleLauncher` prints test execution events as
4647
they occur in a concise format.
4748

@@ -55,7 +56,8 @@ repository on GitHub.
5556

5657
==== Deprecations and Breaking Changes
5758

58-
* ❓
59+
* The `dynamic` parallel execution strategy now allows the thread pool to be saturated by
60+
default.
5961

6062
==== New Features and Improvements
6163

@@ -76,6 +78,10 @@ repository on GitHub.
7678
`ArgumentsProvider` and `AnnotationConsumer`.
7779
* New `AnnotationBasedArgumentConverter` convenience base class which implements both
7880
`ArgumentConverter` and `AnnotationConsumer`.
81+
* New `junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor` configuration
82+
parameter to set the maximum pool size factor.
83+
* New `junit.jupiter.execution.parallel.config.dynamic.saturate` configuration
84+
parameter to disable pool saturation.
7985

8086

8187
[[release-notes-5.10.0-M1-junit-vintage]]

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2395,6 +2395,8 @@ configuration parameter to one of the following options.
23952395
Computes the desired parallelism based on the number of available processors/cores
23962396
multiplied by the `junit.jupiter.execution.parallel.config.dynamic.factor`
23972397
configuration parameter (defaults to `1`).
2398+
The optional `junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor`
2399+
configuration parameter can be used to limit the maximum number of threads.
23982400

23992401
`fixed`::
24002402
Uses the mandatory `junit.jupiter.execution.parallel.config.fixed.parallelism`
@@ -2418,8 +2420,8 @@ of the synchronization mechanisms described in the next section, the `ForkJoinPo
24182420
is used behind the scenes may spawn additional threads to ensure execution continues with
24192421
sufficient parallelism.
24202422
If you require such guarantees, with Java 9+, it is possible to limit the maximum number
2421-
of concurrent threads by controlling the maximum pool size of the `fixed` and `custom`
2422-
strategies.
2423+
of concurrent threads by controlling the maximum pool size of the `dynamic`, `fixed` and
2424+
`custom` strategies.
24232425

24242426
[[writing-tests-parallel-execution-config-properties]]
24252427
===== Relevant properties
@@ -2466,6 +2468,22 @@ The following table lists relevant properties for configuring parallel execution
24662468
| a positive decimal number
24672469
| ```1.0```
24682470

2471+
| ```junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor```
2472+
| Factor to be multiplied by the number of available processors/cores and the value of
2473+
`junit.jupiter.execution.parallel.config.dynamic.factor` to determine the desired
2474+
parallelism for the ```dynamic``` configuration strategy
2475+
| a positive decimal number, must be greater than or equal to `1.0`
2476+
| 256 + the value of `junit.jupiter.execution.parallel.config.dynamic.factor` multiplied
2477+
by the number of available processors/cores
2478+
2479+
| ```junit.jupiter.execution.parallel.config.dynamic.saturate```
2480+
| Disable saturation of the underlying fork-join pool for the ```dynamic``` configuration
2481+
strategy
2482+
|
2483+
* `true`
2484+
* `false`
2485+
| ```true```
2486+
24692487
| ```junit.jupiter.execution.parallel.config.fixed.parallelism```
24702488
| Desired parallelism for the ```fixed``` configuration strategy
24712489
| a positive integer
@@ -2474,7 +2492,7 @@ The following table lists relevant properties for configuring parallel execution
24742492
| ```junit.jupiter.execution.parallel.config.fixed.max-pool-size```
24752493
| Desired maximum pool size of the underlying fork-join pool for the ```fixed```
24762494
configuration strategy
2477-
| a positive integer, must greater than or equal to `junit.jupiter.execution.parallel.config.fixed.parallelism`
2495+
| a positive integer, must be greater than or equal to `junit.jupiter.execution.parallel.config.fixed.parallelism`
24782496
| 256 + the value of `junit.jupiter.execution.parallel.config.fixed.parallelism`
24792497

24802498
| ```junit.jupiter.execution.parallel.config.fixed.saturate```

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

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,20 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
7272
int parallelism = Math.max(1,
7373
factor.multiply(BigDecimal.valueOf(Runtime.getRuntime().availableProcessors())).intValue());
7474

75-
return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism,
76-
KEEP_ALIVE_SECONDS, null);
75+
int maxPoolSize = configurationParameters.get(CONFIG_DYNAMIC_MAX_POOL_SIZE_FACTOR_PROPERTY_NAME,
76+
BigDecimal::new).map(maxPoolSizeFactor -> {
77+
Preconditions.condition(maxPoolSizeFactor.compareTo(BigDecimal.ONE) >= 0,
78+
() -> String.format(
79+
"Factor '%s' specified via configuration parameter '%s' must be greater than or equal to 1",
80+
factor, CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME));
81+
return maxPoolSizeFactor.multiply(BigDecimal.valueOf(parallelism)).intValue();
82+
}).orElseGet(() -> 256 + parallelism);
83+
84+
boolean saturate = configurationParameters.get(CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME,
85+
Boolean::valueOf).orElse(true);
86+
87+
return new DefaultParallelExecutionConfiguration(parallelism, parallelism, maxPoolSize, parallelism,
88+
KEEP_ALIVE_SECONDS, __ -> saturate);
7789
}
7890
},
7991

@@ -155,12 +167,46 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
155167
* Property name of the factor used to determine the desired parallelism for the
156168
* {@link #DYNAMIC} configuration strategy.
157169
*
158-
* <p>Value must be a decimal number; defaults to {@code 1}.
170+
* <p>Value must be a non-negative decimal number; defaults to {@code 1}.
159171
*
160172
* @see #DYNAMIC
161173
*/
162174
public static final String CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME = "dynamic.factor";
163175

176+
/**
177+
* Property name of the factor used to determine the maximum pool size of
178+
* the underlying fork-join pool for the {@link #DYNAMIC} configuration
179+
* strategy.
180+
*
181+
* <p>Value must be a decimal number equal and greater than or equal to
182+
* {@code 1}. When set the maximum pool size is calculated as
183+
* {@code dynamic.max-pool-size-factor * dynamic.factor * Runtime.getRuntime().availableProcessors()}
184+
* When not set the maximum pool size is calculated as
185+
* {@code 256 + dynamic.factor * Runtime.getRuntime().availableProcessors()}
186+
* instead.
187+
*
188+
* @since 1.10
189+
* @see #DYNAMIC
190+
*/
191+
@API(status = EXPERIMENTAL, since = "1.10")
192+
public static final String CONFIG_DYNAMIC_MAX_POOL_SIZE_FACTOR_PROPERTY_NAME = "dynamic.max-pool-size-factor";
193+
194+
/**
195+
* Property name used to disable saturation of the underlying fork-join pool
196+
* for the {@link #DYNAMIC} configuration strategy.
197+
*
198+
* <p>When set to {@code false} the underlying fork-join pool will reject
199+
* additional tasks if all available workers are busy and the maximum
200+
* pool-size would be exceeded.
201+
* <p>Value must either {@code true} or {@code false}; defaults to {@code true}.
202+
*
203+
* @since 1.10
204+
* @see #DYNAMIC
205+
* @see #CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME
206+
*/
207+
@API(status = EXPERIMENTAL, since = "1.10")
208+
public static final String CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME = "dynamic.saturate";
209+
164210
/**
165211
* Property name used to specify the fully qualified class name of the
166212
* {@link ParallelExecutionConfigurationStrategy} to be used by the

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
*/
3131
class DefaultParallelExecutionConfigurationStrategyTests {
3232

33-
private ConfigurationParameters configParams = mock();
33+
final ConfigurationParameters configParams = mock();
3434

3535
@BeforeEach
3636
void setUp() {
@@ -78,7 +78,25 @@ void dynamicStrategyCreatesValidConfiguration() {
7878
assertThat(configuration.getMinimumRunnable()).isEqualTo(availableProcessors * 2);
7979
assertThat(configuration.getMaxPoolSize()).isEqualTo(256 + (availableProcessors * 2));
8080
assertThat(configuration.getKeepAliveSeconds()).isEqualTo(30);
81-
assertThat(configuration.getSaturatePredicate()).isNull();
81+
assertThat(configuration.getSaturatePredicate().test(null)).isTrue();
82+
}
83+
84+
@Test
85+
void dynamicSaturateStrategyCreatesValidConfiguration() {
86+
when(configParams.get("dynamic.factor")).thenReturn(Optional.of("2.0"));
87+
when(configParams.get("dynamic.max-pool-size-factor")).thenReturn(Optional.of("3.0"));
88+
when(configParams.get("dynamic.saturate")).thenReturn(Optional.of("false"));
89+
90+
ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC;
91+
var configuration = strategy.createConfiguration(configParams);
92+
93+
var availableProcessors = Runtime.getRuntime().availableProcessors();
94+
assertThat(configuration.getParallelism()).isEqualTo(availableProcessors * 2);
95+
assertThat(configuration.getCorePoolSize()).isEqualTo(availableProcessors * 2);
96+
assertThat(configuration.getMinimumRunnable()).isEqualTo(availableProcessors * 2);
97+
assertThat(configuration.getMaxPoolSize()).isEqualTo(availableProcessors * 6);
98+
assertThat(configuration.getKeepAliveSeconds()).isEqualTo(30);
99+
assertThat(configuration.getSaturatePredicate().test(null)).isFalse();
82100
}
83101

84102
@Test

0 commit comments

Comments
 (0)