Skip to content

Commit

Permalink
Support saturating the ForkJoinPool via a property.
Browse files Browse the repository at this point in the history
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.

Fixes: junit-team#2545
Fixes: junit-team#1858
Fixes: junit-team#3026
  • Loading branch information
mpkorstanje committed Sep 9, 2022
1 parent e98b714 commit dd16846
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ on GitHub.

==== New Features and Improvements

* ❓

* Support saturating the ForkJoinPool via a property.

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

==== New Features and Improvements

* ❓

* Added `junit.jupiter.execution.parallel.config.dynamic.saturate` and
`junit.jupiter.execution.parallel.config.fixed.saturate` to limit the concurrently
executing tests to the maximum desired parallelism.

[[release-notes-5.9.1-junit-vintage]]
=== JUnit Vintage
Expand Down
25 changes: 22 additions & 3 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2332,12 +2332,14 @@ strategy with a factor of `1`. Consequently, the desired parallelism will be equ
number of available processors/cores.

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

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

| ```junit.jupiter.execution.parallel.config.dynamic.saturate```
| Limit the concurrently executing tests to the maximum parallelism desired parallelism
for the ```dynamic``` configuration strategy.
|
* `true`
* `false`
| ```false```

| ```junit.jupiter.execution.parallel.config.fixed.parallelism```
| Desired parallelism for the ```fixed``` configuration strategy
| a positive integer
| no default value


| ```junit.jupiter.execution.parallel.config.fixed.saturate```
| Limit the concurrently executing tests to the maximum parallelism desired parallelism
for the ```fixed``` configuration strategy.
|
* `true`
* `false`
| ```false```

| ```junit.jupiter.execution.parallel.config.custom.class```
| Fully qualified class name of the _ParallelExecutionConfigurationStrategy_ to be
used for the ```custom``` configuration strategy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import static org.apiguardian.api.API.Status.STABLE;
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_CUSTOM_CLASS_PROPERTY_NAME;
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME;
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME;
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_FIXED_PARALLELISM_PROPERTY_NAME;
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_FIXED_SATURATE_PROPERTY_NAME;
import static org.junit.platform.engine.support.hierarchical.DefaultParallelExecutionConfigurationStrategy.CONFIG_STRATEGY_PROPERTY_NAME;

import org.apiguardian.api.API;
Expand Down Expand Up @@ -166,6 +168,19 @@ public final class Constants {
public static final String PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
+ CONFIG_FIXED_PARALLELISM_PROPERTY_NAME;

/**
* Property name used to limit the concurrently executing tests to the
* maximum parallelism desired parallelism for the {@code fixed}
* configuration strategy: {@value}
*
* <p>Value must either {@code true} or {@code false}; defaults to {@code false}.
*
* @since 5.9.1
*/
@API(status = EXPERIMENTAL, since = "5.9.1")
public static final String PARALLEL_CONFIG_FIXED_SATURATE_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
+ CONFIG_FIXED_SATURATE_PROPERTY_NAME;

/**
* Property name used to set the factor to be multiplied with the number of
* available processors/cores to determine the desired parallelism for the
Expand All @@ -179,6 +194,19 @@ public final class Constants {
public static final String PARALLEL_CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
+ CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME;

/**
* Property name used to limit the concurrently executing tests to the
* maximum parallelism desired parallelism for the {@code dynamic}
* configuration strategy: {@value}
*
* <p>Value must either {@code true} or {@code false}; defaults to {@code false}.
*
* @since 5.9.1
*/
@API(status = EXPERIMENTAL, since = "5.9.1")
public static final String PARALLEL_CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME = PARALLEL_CONFIG_PREFIX
+ CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME;

/**
* Property name used to specify the fully qualified class name of the
* {@link ParallelExecutionConfigurationStrategy} to be used for the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

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

import java.util.concurrent.ForkJoinPool;
import java.util.function.Predicate;

/**
* @since 1.3
*/
Expand All @@ -20,14 +23,16 @@ class DefaultParallelExecutionConfiguration implements ParallelExecutionConfigur
private final int maxPoolSize;
private final int corePoolSize;
private final int keepAliveSeconds;
private final Predicate<? super ForkJoinPool> saturate;

DefaultParallelExecutionConfiguration(int parallelism, int minimumRunnable, int maxPoolSize, int corePoolSize,
int keepAliveSeconds) {
int keepAliveSeconds, boolean saturate) {
this.parallelism = parallelism;
this.minimumRunnable = minimumRunnable;
this.maxPoolSize = maxPoolSize;
this.corePoolSize = corePoolSize;
this.keepAliveSeconds = keepAliveSeconds;
this.saturate = saturate ? __ -> true : null;
}

@Override
Expand Down Expand Up @@ -55,4 +60,8 @@ public int getKeepAliveSeconds() {
return keepAliveSeconds;
}

@Override
public Predicate<? super ForkJoinPool> getSaturatePredicate() {
return saturate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
() -> new JUnitException(String.format("Configuration parameter '%s' must be set",
CONFIG_FIXED_PARALLELISM_PROPERTY_NAME)));

boolean saturate = configurationParameters.get(CONFIG_FIXED_SATURATE_PROPERTY_NAME,
Boolean::valueOf).orElse(false);

return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism,
KEEP_ALIVE_SECONDS);
KEEP_ALIVE_SECONDS, saturate);
}
},

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

boolean saturate = configurationParameters.get(CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME,
Boolean::valueOf).orElse(false);

return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism,
KEEP_ALIVE_SECONDS);
KEEP_ALIVE_SECONDS, saturate);
}
},

Expand Down Expand Up @@ -114,6 +120,20 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
*/
public static final String CONFIG_FIXED_PARALLELISM_PROPERTY_NAME = "fixed.parallelism";

/**
* Property name used to enable saturation of the underlying fork join pool
* for the {@link #FIXED} configuration strategy.
*
* <p>When set to {@code true} the maximum number of concurrent threads will
* not exceed the desired parallelism, even when some threads are blocked.
*
* <p>Value must either {@code true} or {@code false}; defaults to {@code false}.
*
* @see #FIXED
*/
@API(status = EXPERIMENTAL, since = "1.9.1")
public static final String CONFIG_FIXED_SATURATE_PROPERTY_NAME = "fixed.saturate";

/**
* Property name of the factor used to determine the desired parallelism for the
* {@link #DYNAMIC} configuration strategy.
Expand All @@ -124,6 +144,20 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
*/
public static final String CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME = "dynamic.factor";

/**
* Property name used to enable saturation of the underlying fork join pool
* for the {@link #DYNAMIC} configuration strategy.
*
* <p>When set to {@code true} the maximum number of concurrent threads will
* not exceed the desired parallelism, even when some threads are blocked.
*
* <p>Value must either {@code true} or {@code false}; defaults to {@code false}.
*
* @see #DYNAMIC
*/
@API(status = EXPERIMENTAL, since = "1.9.1")
public static final String CONFIG_DYNAMIC_SATURATE_PROPERTY_NAME = "dynamic.saturate";

/**
* Property name used to specify the fully qualified class name of the
* {@link ParallelExecutionConfigurationStrategy} to be used by the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ void fixedStrategyCreatesValidConfiguration() {
assertThat(configuration.getSaturatePredicate()).isNull();
}

@Test
void fixedSaturateStrategyCreatesValidConfiguration() {
when(configParams.get("fixed.parallelism")).thenReturn(Optional.of("42"));
when(configParams.get("fixed.saturate")).thenReturn(Optional.of("true"));

ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.FIXED;
var configuration = strategy.createConfiguration(configParams);
assertThat(configuration.getSaturatePredicate()).isNotNull();
}

@Test
void dynamicStrategyCreatesValidConfiguration() {
when(configParams.get("dynamic.factor")).thenReturn(Optional.of("2.0"));
Expand All @@ -70,6 +80,17 @@ void dynamicStrategyCreatesValidConfiguration() {
assertThat(configuration.getSaturatePredicate()).isNull();
}

@Test
void dynamicSaturateStrategyCreatesValidConfiguration() {
when(configParams.get("dynamic.factor")).thenReturn(Optional.of("2.0"));
when(configParams.get("dynamic.saturate")).thenReturn(Optional.of("true"));

ParallelExecutionConfigurationStrategy strategy = DefaultParallelExecutionConfigurationStrategy.DYNAMIC;
var configuration = strategy.createConfiguration(configParams);

assertThat(configuration.getSaturatePredicate()).isNotNull();
}

@Test
void customStrategyCreatesValidConfiguration() {
when(configParams.get("custom.class")).thenReturn(
Expand Down Expand Up @@ -183,12 +204,7 @@ void customStrategyThrowsExceptionWhenClassDoesNotExist() {
static class CustomParallelExecutionConfigurationStrategy implements ParallelExecutionConfigurationStrategy {
@Override
public ParallelExecutionConfiguration createConfiguration(ConfigurationParameters configurationParameters) {
return new DefaultParallelExecutionConfiguration(1, 2, 3, 4, 5) {
@Override
public Predicate<? super ForkJoinPool> getSaturatePredicate() {
return __ -> true;
}
};
return new DefaultParallelExecutionConfiguration(1, 2, 3, 4, 5, true);
}
}

Expand Down

0 comments on commit dd16846

Please sign in to comment.