Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit max pool size for dynamic parallel execution #3206

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ repository on GitHub.
of the former. Please refer to the
<<../user-guide/index.adoc#launcher-api-launcher-interceptors-custom, User Guide>> for
details.
* Support for limiting the `max-pool-size-factor` for parallel execution via a configuration parameter.

[[release-notes-5.10.0-M1-junit-jupiter]]
=== JUnit Jupiter
Expand All @@ -54,7 +55,8 @@ repository on GitHub.

==== Deprecations and Breaking Changes

* ❓
* The `dynamic` parallel execution strategy now allows the thread pool to be saturated by
default.

==== New Features and Improvements

Expand All @@ -68,7 +70,10 @@ repository on GitHub.
`AnnotationConsumer` interfaces.
* New `AnnotationBasedArgumentConverter` that implements both `ArgumentConverter` and
`AnnotationConsumer` interfaces.

* New `junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor` configuration
parameter to set the maximum pool size factor.
* New `junit.jupiter.execution.parallel.config.dynamic.saturate` configuration
parameter to disable pool saturation.

[[release-notes-5.10.0-M1-junit-vintage]]
=== JUnit Vintage
Expand Down
16 changes: 13 additions & 3 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2354,6 +2354,8 @@ configuration parameter to one of the following options.
Computes the desired parallelism based on the number of available processors/cores
multiplied by the `junit.jupiter.execution.parallel.config.dynamic.factor`
configuration parameter (defaults to `1`).
The optional `junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor`
configuration parameter can be used to limit the maximum number of threads.

`fixed`::
Uses the mandatory `junit.jupiter.execution.parallel.config.fixed.parallelism`
Expand All @@ -2377,8 +2379,8 @@ of the synchronization mechanisms described in the next section, the `ForkJoinPo
is used behind the scenes may spawn additional threads to ensure execution continues with
sufficient parallelism.
If you require such guarantees, with Java 9+, it is possible to limit the maximum number
of concurrent threads by controlling the maximum pool size of the `fixed` and `custom`
strategies.
of concurrent threads by controlling the maximum pool size of the `dynamic`, `fixed` and
`custom` strategies.

[[writing-tests-parallel-execution-config-properties]]
===== Relevant properties
Expand Down Expand Up @@ -2425,6 +2427,14 @@ The following table lists relevant properties for configuring parallel execution
| a positive decimal number
| ```1.0```

| ```junit.jupiter.execution.parallel.config.dynamic.max-pool-size.factor```
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
| Factor to be multiplied by the number of available processors/cores and the value of
`junit.jupiter.execution.parallel.config.dynamic.factor` to determine the desired
parallelism for the ```dynamic``` configuration strategy
| a positive decimal number, must be greater than or equal to `1.0`
| 256 + the value of `junit.jupiter.execution.parallel.config.dynamic.factor` multiplied
by the number of available processors/cores

| ```junit.jupiter.execution.parallel.config.fixed.parallelism```
| Desired parallelism for the ```fixed``` configuration strategy
| a positive integer
Expand All @@ -2433,7 +2443,7 @@ The following table lists relevant properties for configuring parallel execution
| ```junit.jupiter.execution.parallel.config.fixed.max-pool-size```
| Desired maximum pool size of the underlying fork-join pool for the ```fixed```
configuration strategy
| a positive integer, must greater than or equal to `junit.jupiter.execution.parallel.config.fixed.parallelism`
| a positive integer, must be greater than or equal to `junit.jupiter.execution.parallel.config.fixed.parallelism`
| 256 + the value of `junit.jupiter.execution.parallel.config.fixed.parallelism`

| ```junit.jupiter.execution.parallel.config.fixed.saturate```
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,20 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
int parallelism = Math.max(1,
factor.multiply(BigDecimal.valueOf(Runtime.getRuntime().availableProcessors())).intValue());

return new DefaultParallelExecutionConfiguration(parallelism, parallelism, 256 + parallelism, parallelism,
KEEP_ALIVE_SECONDS, null);
int maxPoolSize = configurationParameters.get(CONFIG_DYNAMIC_MAX_POOL_SIZE_FACTOR_PROPERTY_NAME,
BigDecimal::new).map(maxPoolSizeFactor -> {
Preconditions.condition(maxPoolSizeFactor.compareTo(BigDecimal.ONE) >= 0,
() -> String.format(
"Factor '%s' specified via configuration parameter '%s' must be greater than or equal to 1",
factor, CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME));
return maxPoolSizeFactor.multiply(BigDecimal.valueOf(parallelism)).intValue();
}).orElseGet(() -> 256 + parallelism);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two ways to calculate the max pool size:

maxPoolSize = dynamic.max-pool-size-factor * dynamic.factor * Runtime.getRuntime().availableProcessors() 
maxPoolSize = dynamic.max-pool-size-factor * Runtime.getRuntime().availableProcessors() 

And the choice is a bit arbitrary. But I think reasoning in terms of "threads per degree of parallelism" is a bit easier so I prefer the former.


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

return new DefaultParallelExecutionConfiguration(parallelism, parallelism, maxPoolSize, parallelism,
KEEP_ALIVE_SECONDS, __ -> saturate);
}
},

Expand Down Expand Up @@ -154,12 +166,46 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
* Property name of the factor used to determine the desired parallelism for the
* {@link #DYNAMIC} configuration strategy.
*
* <p>Value must be a decimal number; defaults to {@code 1}.
* <p>Value must be a non-negative decimal number; defaults to {@code 1}.
*
* @see #DYNAMIC
*/
public static final String CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME = "dynamic.factor";

/**
* Property name of the factor used to determine the maximum pool size of
* the underlying fork-join pool for the {@link #DYNAMIC} configuration
* strategy.
*
* <p>Value must be a decimal number equal and greater than or equal to
* {@code 1}. When set the maximum pool size is calculated as
* {@code dynamic.max-pool-size-factor * dynamic.factor * Runtime.getRuntime().availableProcessors()}
* When not set the maximum pool size is calculated as
* {@code 256 + dynamic.factor * Runtime.getRuntime().availableProcessors()}
* instead.
*
* @since 1.10
* @see #DYNAMIC
*/
@API(status = EXPERIMENTAL, since = "1.10")
public static final String CONFIG_DYNAMIC_MAX_POOL_SIZE_FACTOR_PROPERTY_NAME = "dynamic.max-pool-size-factor";

/**
* Property name used to disable saturation of the underlying fork-join pool
* for the {@link #DYNAMIC} configuration strategy.
*
* <p>When set to {@code false} the underlying fork-join pool will reject
* additional tasks if all available workers are busy and the maximum
* pool-size would be exceeded.
* <p>Value must either {@code true} or {@code false}; defaults to {@code true}.
*
* @since 1.10
* @see #DYNAMIC
* @see #CONFIG_DYNAMIC_FACTOR_PROPERTY_NAME
*/
@API(status = EXPERIMENTAL, since = "1.10")
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 @@ -30,7 +30,7 @@
*/
class DefaultParallelExecutionConfigurationStrategyTests {

private ConfigurationParameters configParams = mock();
final ConfigurationParameters configParams = mock();

@BeforeEach
void setUp() {
Expand Down Expand Up @@ -78,7 +78,25 @@ void dynamicStrategyCreatesValidConfiguration() {
assertThat(configuration.getMinimumRunnable()).isEqualTo(availableProcessors * 2);
assertThat(configuration.getMaxPoolSize()).isEqualTo(256 + (availableProcessors * 2));
assertThat(configuration.getKeepAliveSeconds()).isEqualTo(30);
assertThat(configuration.getSaturatePredicate()).isNull();
assertThat(configuration.getSaturatePredicate().test(null)).isTrue();
}

@Test
void dynamicSaturateStrategyCreatesValidConfiguration() {
when(configParams.get("dynamic.factor")).thenReturn(Optional.of("2.0"));
when(configParams.get("dynamic.max-pool-size-factor")).thenReturn(Optional.of("3.0"));
when(configParams.get("dynamic.saturate")).thenReturn(Optional.of("false"));

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

var availableProcessors = Runtime.getRuntime().availableProcessors();
assertThat(configuration.getParallelism()).isEqualTo(availableProcessors * 2);
assertThat(configuration.getCorePoolSize()).isEqualTo(availableProcessors * 2);
assertThat(configuration.getMinimumRunnable()).isEqualTo(availableProcessors * 2);
assertThat(configuration.getMaxPoolSize()).isEqualTo(availableProcessors * 6);
assertThat(configuration.getKeepAliveSeconds()).isEqualTo(30);
assertThat(configuration.getSaturatePredicate().test(null)).isFalse();
}

@Test
Expand Down