Skip to content

Commit 9aaf314

Browse files
committed
Support saturating the ForkJoinPool via a property.
The JUnit Platform now supports saturating the ForkJoinPool via a property. Enabling this property will limit the number of concurrently executing tests will to the configured parallelism even when some are blocked. For JUnit Jupiter this can be enabled by through the `junit.jupiter.execution.parallel.config.dynamic.saturate` and `junit.jupiter.execution.parallel.config.fixed.saturate` configuration properties.
1 parent e98b714 commit 9aaf314

File tree

6 files changed

+124
-16
lines changed

6 files changed

+124
-16
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.9.1.adoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ on GitHub.
2323

2424
==== New Features and Improvements
2525

26-
* ❓
27-
26+
* Support saturating the ForkJoinPool via a property.
2827

2928
[[release-notes-5.9.1-junit-jupiter]]
3029
=== JUnit Jupiter
@@ -47,8 +46,9 @@ on GitHub.
4746

4847
==== New Features and Improvements
4948

50-
* ❓
51-
49+
* Added `junit.jupiter.execution.parallel.config.dynamic.saturate` and
50+
`junit.jupiter.execution.parallel.config.fixed.saturate` to limit the
51+
concurrently executing tests to the maximum desired parallelism.
5252

5353
[[release-notes-5.9.1-junit-vintage]]
5454
=== JUnit Vintage

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2332,12 +2332,14 @@ strategy with a factor of `1`. Consequently, the desired parallelism will be equ
23322332
number of available processors/cores.
23332333

23342334
.Parallelism does not imply maximum number of concurrent threads
2335-
NOTE: JUnit Jupiter does not guarantee that the number of concurrently executing tests
2335+
NOTE: By default JUnit Jupiter does not guarantee that the number of concurrently executing tests
23362336
will not exceed the configured parallelism. For example, when using one of the
23372337
synchronization mechanisms described in the next section, the `ForkJoinPool` that is used
23382338
behind the scenes may spawn additional threads to ensure execution continues with
2339-
sufficient parallelism. Thus, if you require such guarantees in a test class, please use
2340-
your own means of controlling concurrency.
2339+
sufficient parallelism.
2340+
By setting the optional configuration parameters `junit.jupiter.execution.parallel.config.dynamic.saturate`
2341+
or `junit.jupiter.execution.parallel.config.fixed.saturate` to `true` the number
2342+
of concurrently executing tests will not exceed the configured parallelism.
23412343

23422344
[[writing-tests-parallel-execution-config-properties]]
23432345
===== Relevant properties
@@ -2384,11 +2386,28 @@ The following table lists relevant properties for configuring parallel execution
23842386
| a positive decimal number
23852387
| ```1.0```
23862388

2389+
| ```junit.jupiter.execution.parallel.config.dynamic.saturate```
2390+
| Limit the concurrently executing tests to the maximum parallelism desired
2391+
parallelism for the ```dynamic``` configuration strategy.
2392+
|
2393+
* `true`
2394+
* `false`
2395+
| ```false```
2396+
23872397
| ```junit.jupiter.execution.parallel.config.fixed.parallelism```
23882398
| Desired parallelism for the ```fixed``` configuration strategy
23892399
| a positive integer
23902400
| no default value
23912401

2402+
2403+
| ```junit.jupiter.execution.parallel.config.fixed.saturate```
2404+
| Limit the concurrently executing tests to the maximum parallelism desired
2405+
parallelism for the ```fixed``` configuration strategy.
2406+
|
2407+
* `true`
2408+
* `false`
2409+
| ```false```
2410+
23922411
| ```junit.jupiter.execution.parallel.config.custom.class```
23932412
| Fully qualified class name of the _ParallelExecutionConfigurationStrategy_ to be
23942413
used for the ```custom``` configuration strategy

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

Lines changed: 28 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_DYNAMIC_SATURATE_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,19 @@ 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 limit the concurrently executing tests to the
173+
* maximum parallelism desired parallelism for the {@code fixed}
174+
* configuration strategy: {@value}
175+
*
176+
* <p>Value must either {@code true} or {@code false}; defaults to {@code false}.
177+
*
178+
* @since 5.9.1
179+
*/
180+
@API(status = EXPERIMENTAL, since = "5.9.1")
181+
public static final String PARALLEL_CONFIG_FIXED_SATURATE_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
182+
+ CONFIG_FIXED_SATURATE_PROPERTY_NAME;
183+
169184
/**
170185
* Property name used to set the factor to be multiplied with the number of
171186
* available processors/cores to determine the desired parallelism for the
@@ -179,6 +194,19 @@ public final class Constants {
179194
public static final String PARALLEL_CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
180195
+ CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME;
181196

197+
/**
198+
* Property name used to limit the concurrently executing tests to the
199+
* maximum parallelism desired parallelism for the {@code dynamic}
200+
* configuration strategy: {@value}
201+
*
202+
* <p>Value must either {@code true} or {@code false}; defaults to {@code false}.
203+
*
204+
* @since 5.9.1
205+
*/
206+
@API(status = EXPERIMENTAL, since = "5.9.1")
207+
public static final String PARALLEL_CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
208+
+ CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME;
209+
182210
/**
183211
* Property name used to specify the fully qualified class name of the
184212
* {@link ParallelExecutionConfigurationStrategy} to be used for the

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

Lines changed: 11 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
*/
@@ -21,13 +24,16 @@ class DefaultParallelExecutionConfiguration implements ParallelExecutionConfigur
2124
private final int corePoolSize;
2225
private final int keepAliveSeconds;
2326

27+
private final Predicate<? super ForkJoinPool> saturate;
28+
2429
DefaultParallelExecutionConfiguration(int parallelism, int minimumRunnable, int maxPoolSize, int corePoolSize,
25-
int keepAliveSeconds) {
30+
int keepAliveSeconds, boolean saturate) {
2631
this.parallelism = parallelism;
2732
this.minimumRunnable = minimumRunnable;
2833
this.maxPoolSize = maxPoolSize;
2934
this.corePoolSize = corePoolSize;
3035
this.keepAliveSeconds = keepAliveSeconds;
36+
this.saturate = saturate ? __ -> true : null;
3137
}
3238

3339
@Override
@@ -55,4 +61,8 @@ public int getKeepAliveSeconds() {
5561
return keepAliveSeconds;
5662
}
5763

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

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import java.math.BigDecimal;
1616
import java.util.Locale;
17+
import java.util.concurrent.ForkJoinPool;
18+
import java.util.function.Predicate;
1719

1820
import org.apiguardian.api.API;
1921
import org.junit.platform.commons.JUnitException;
@@ -42,8 +44,11 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
4244
() -> new JUnitException(String.format("Configuration parameter '%s' must be set",
4345
CONFIG_FIXED_PARALLELISM_PROPERTY_NAME)));
4446

47+
boolean saturate = configurationParameters.get(CONFIG_FIXED_SATURATE_PROPERTY_NAME,
48+
Boolean::valueOf).orElse(false);
49+
4550
return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism,
46-
KEEP_ALIVE_SECONDS);
51+
KEEP_ALIVE_SECONDS, saturate);
4752
}
4853
},
4954

@@ -65,8 +70,11 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
6570
int parallelism = Math.max(1,
6671
factor.multiply(BigDecimal.valueOf(Runtime.getRuntime().availableProcessors())).intValue());
6772

73+
boolean saturate = configurationParameters.get(CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME,
74+
Boolean::valueOf).orElse(false);
75+
6876
return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism,
69-
KEEP_ALIVE_SECONDS);
77+
KEEP_ALIVE_SECONDS, saturate);
7078
}
7179
},
7280

@@ -114,6 +122,20 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
114122
*/
115123
public static final String CONFIG_FIXED_PARALLELISM_PROPERTY_NAME = "fixed.parallelism";
116124

125+
/**
126+
* Property name used to enable saturation of the underlying fork join pool
127+
* for the {@link #FIXED} configuration strategy.
128+
*
129+
* <p>When set to {@code true} the maximum number of concurrent threads will
130+
* not exceed the desired parallelism, even when some threads are blocked.
131+
*
132+
* <p>Value must either {@code true} or {@code false}; defaults to {@code false}.
133+
*
134+
* @see #FIXED
135+
*/
136+
@API(status = EXPERIMENTAL, since = "1.9.1")
137+
public static final String CONFIG_FIXED_SATURATE_PROPERTY_NAME = "fixed.saturate";
138+
117139
/**
118140
* Property name of the factor used to determine the desired parallelism for the
119141
* {@link #DYNAMIC} configuration strategy.
@@ -124,6 +146,19 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
124146
*/
125147
public static final String CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME = "dynamic.factor";
126148

149+
/**
150+
* Property name used to enable saturation of the underlying fork join pool
151+
* for the {@link #DYNAMIC} configuration strategy.
152+
*
153+
* <p>When set to {@code true} the maximum number of concurrent threads will
154+
* not exceed the desired parallelism, even when some threads are blocked.
155+
*
156+
* <p>Value must either {@code true} or {@code false}; defaults to {@code false}.
157+
*
158+
* @see #DYNAMIC
159+
*/
160+
public static final String CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME = "dynamic.saturate";
161+
127162
/**
128163
* Property name used to specify the fully qualified class name of the
129164
* {@link ParallelExecutionConfigurationStrategy} to be used by the

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ void fixedStrategyCreatesValidConfiguration() {
5454
assertThat(configuration.getSaturatePredicate()).isNull();
5555
}
5656

57+
@Test
58+
void fixedSaturateStrategyCreatesValidConfiguration() {
59+
when(configParams.get("fixed.parallelism")).thenReturn(Optional.of("42"));
60+
when(configParams.get("fixed.saturate")).thenReturn(Optional.of("true"));
61+
62+
ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.FIXED;
63+
var configuration = strategy.createConfiguration(configParams);
64+
assertThat(configuration.getSaturatePredicate()).isNotNull();
65+
}
66+
5767
@Test
5868
void dynamicStrategyCreatesValidConfiguration() {
5969
when(configParams.get("dynamic.factor")).thenReturn(Optional.of("2.0"));
@@ -70,6 +80,17 @@ void dynamicStrategyCreatesValidConfiguration() {
7080
assertThat(configuration.getSaturatePredicate()).isNull();
7181
}
7282

83+
@Test
84+
void dynamicSaturateStrategyCreatesValidConfiguration() {
85+
when(configParams.get("dynamic.factor")).thenReturn(Optional.of("2.0"));
86+
when(configParams.get("dynamic.saturate")).thenReturn(Optional.of("true"));
87+
88+
ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC;
89+
var configuration = strategy.createConfiguration(configParams);
90+
91+
assertThat(configuration.getSaturatePredicate()).isNotNull();
92+
}
93+
7394
@Test
7495
void customStrategyCreatesValidConfiguration() {
7596
when(configParams.get("custom.class")).thenReturn(
@@ -183,12 +204,7 @@ void customStrategyThrowsExceptionWhenClassDoesNotExist() {
183204
static class CustomParallelExecutionConfigurationStrategy implements ParallelExecutionConfigurationStrategy {
184205
@Override
185206
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-
};
207+
return new DefaultParallelExecutionConfiguration(1, 2, 3, 4, 5, true);
192208
}
193209
}
194210

0 commit comments

Comments
 (0)