Skip to content

Commit 21af089

Browse files
committed
Limit the number of concurrently executing tests via a property
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` property. 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. With the following configuration: ```properties junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.config.strategy=fixed junit.jupiter.execution.parallel.mode.default=concurrent junit.jupiter.execution.parallel.config.fixed.parallelism=2 ``` This example will report between 2-5 tests running concurrently. ```java class ExampleTest { private static final AtomicInteger running = new AtomicInteger(); @beforeeach void increment() { System.out.println("Running " + running.incrementAndGet()); } @AfterEach void decrement() { running.decrementAndGet(); } static IntStream numbers() { return IntStream.range(0, 1000); } @ParameterizedTest @MethodSource("numbers") void test(int i) throws ExecutionException, InterruptedException { Runnable sleep = () -> { try { Thread.sleep(600); } catch (InterruptedException e) { throw new RuntimeException(e); } }; ForkJoinPool.commonPool().submit(sleep).get(); } } ``` By also configuring the `max-pool-size` we can ensure the concurrently executing test does not exceed the configured 2. ```properties junit.jupiter.execution.parallel.config.fixed.max-pool-size=2 ``` 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 behaviour. These changes were intentionally not made to the `dynamic` strategy to limit the scope of this pull request. While I can reasonably predict what behaviour users of the `fixed` strategy might expect, I can not say the same about the `dynamic` strategy. Fixes: junit-team#2545 Fixes: junit-team#1858 Fixes: junit-team#3026
1 parent 517bfd5 commit 21af089

File tree

5 files changed

+123
-20
lines changed

5 files changed

+123
-20
lines changed

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,6 +2336,8 @@ configuration parameter to one of the following options.
23362336
`fixed`::
23372337
Uses the mandatory `junit.jupiter.execution.parallel.config.fixed.parallelism`
23382338
configuration parameter as the desired parallelism.
2339+
The optional `junit.jupiter.execution.parallel.config.fixed.max-pool-size` can
2340+
be used to limit the maximum number of concurrent tests.
23392341

23402342
`custom`::
23412343
Allows you to specify a custom `{ParallelExecutionConfigurationStrategy}`
@@ -2346,13 +2348,15 @@ If no configuration strategy is set, JUnit Jupiter uses the `dynamic` configurat
23462348
strategy with a factor of `1`. Consequently, the desired parallelism will be equal to the
23472349
number of available processors/cores.
23482350

2349-
.Parallelism does not imply maximum number of concurrent threads
2350-
NOTE: JUnit Jupiter does not guarantee that the number of concurrently executing tests
2351-
will not exceed the configured parallelism. For example, when using one of the
2352-
synchronization mechanisms described in the next section, the `ForkJoinPool` that is used
2353-
behind the scenes may spawn additional threads to ensure execution continues with
2354-
sufficient parallelism. Thus, if you require such guarantees in a test class, please use
2355-
your own means of controlling concurrency.
2351+
.Parallelism alone does not imply maximum number of concurrent threads
2352+
NOTE: By default JUnit Jupiter does not guarantee that the number of concurrently
2353+
executing tests will not exceed the configured parallelism. For example, when using one
2354+
of the synchronization mechanisms described in the next section, the `ForkJoinPool` that
2355+
is used behind the scenes may spawn additional threads to ensure execution continues with
2356+
sufficient parallelism.
2357+
If you require such guarantees, with Java 9+, it is possible to limit the maximum number
2358+
of concurrent tests by controlling the maximum pool size of the `fixed` and `custom`
2359+
strategies.
23562360

23572361
[[writing-tests-parallel-execution-config-properties]]
23582362
===== Relevant properties
@@ -2404,6 +2408,20 @@ The following table lists relevant properties for configuring parallel execution
24042408
| a positive integer
24052409
| no default value
24062410

2411+
| ```junit.jupiter.execution.parallel.config.fixed.max-pool-size```
2412+
| Desired maximum pool size of the underlying fork join pool for the ```fixed```
2413+
configuration strategy
2414+
| a positive integer, must not be smaller then `junit.jupiter.execution.parallel.config.fixed.parallelism`
2415+
| 256 + `junit.jupiter.execution.parallel.config.fixed.parallelism`
2416+
2417+
| ```junit.jupiter.execution.parallel.config.fixed.saturate```
2418+
| Enable saturation of the underlying fork join pool for the ```fixed``` configuration
2419+
strategy
2420+
|
2421+
* `true`
2422+
* `false`
2423+
| ```true```
2424+
24072425
| ```junit.jupiter.execution.parallel.config.custom.class```
24082426
| Fully qualified class name of the _ParallelExecutionConfigurationStrategy_ to be
24092427
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 larger 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 enable saturation of the underlying fork join pool
189+
* for the {@code ffixed} configuration strategy: {@value}
190+
*
191+
* <p>When set to {@code false} the underlying fork join pool will reject
192+
* additional tasks when 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: 37 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+
boolean saturate = configurationParameters.get(CONFIG_FIXED_SATURATE_PROPERTY_NAME,
46+
Boolean::valueOf).orElse(true);
47+
48+
int maxPoolSize = configurationParameters.get(CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME,
49+
Integer::valueOf).orElse(parallelism + 256);
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,34 @@ 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+
* <p>Value must be an integer and larger or equal to
127+
* {@value #CONFIG_FIXED_PARALLELISM_PROPERTY_NAME}; defaults to
128+
* {@code 256 + fixed.parallelism}.
129+
*
130+
* @since 1.10
131+
* @see #FIXED
132+
*/
133+
@API(status = EXPERIMENTAL, since = "1.10")
134+
public static final String CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME = "fixed.max-pool-size";
135+
136+
/**
137+
* Property name used to enable saturation of the underlying fork join pool
138+
* for the {@link #FIXED} configuration strategy.
139+
* <p>When set to {@code false} the underlying fork join pool will reject
140+
* additional tasks when all available workers are busy and the maximum
141+
* pool-size would be exceeded.
142+
* <p>Value must either {@code true} or {@code false}; defaults to {@code true}.
143+
*
144+
* @since 1.10
145+
* @see #FIXED
146+
* @see #CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME
147+
*/
148+
@API(status = EXPERIMENTAL, since = "1.10")
149+
public static final String CONFIG_FIXED_SATURATE_PROPERTY_NAME = "fixed.saturate";
150+
117151
/**
118152
* Property name of the factor used to determine the desired parallelism for the
119153
* {@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)