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 parallel execution via config param #3044

Merged
merged 13 commits into from
Dec 30, 2022

Conversation

mpkorstanje
Copy link
Contributor

@mpkorstanje mpkorstanje commented Sep 24, 2022

Overview

With Java 9+ the ForkJoinPool used by JUnit can be configured with a maximum pool size via the junit.jupiter.execution.parallel.config.fixed.max-pool-size property.

While the number of concurrently executing tests may exceed the configured parallelism when tests become blocked, it will not exceed the maximum pool size in these circumstances.

With the following configuration:

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.

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.

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 behavior.

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: #2545
Fixes: #1858
Fixes: #3026

This PR replaces #3027.


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

@mpkorstanje mpkorstanje force-pushed the saturate-via-property branch 3 times, most recently from e8c4b30 to 6474b71 Compare September 24, 2022 13:19
@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Sep 24, 2022

@marcphilipp To update the changelog I'd need to know which milestone would go into.

@sbrannen
Copy link
Member

To update the changelog I'd need to know which milestone would go into.

I just created 5.10 M1. So feel free to use that.

If it ends up landing in a different milestone, we'll move the change log entry accordingly.

@sbrannen
Copy link
Member

Note, however, that there is currently no file for the 5.10 M1 release notes.

So, you'll need to follow the instructions in release-notes-TEMPLATE.adoc.

If that seems too cumbersome, a core committer can create the new file for you.

@mpkorstanje mpkorstanje marked this pull request as ready for review September 24, 2022 13:36
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#3026
Fixes: junit-team#2545
Fixes: junit-team#1858
@mpkorstanje mpkorstanje force-pushed the saturate-via-property branch from c977a28 to dccc0c7 Compare October 3, 2022 11:03
@see-quick
Copy link

Thanks for implementing this! :)

@mpkorstanje mpkorstanje changed the title Limit the number of concurrently executing tests via a property Limit the maximum number of concurrently executing tests via a property Oct 15, 2022
@mpkorstanje mpkorstanje force-pushed the saturate-via-property branch from 16a3926 to d35e44c Compare October 16, 2022 09:18
@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Oct 16, 2022

#3027 (comment)

Fixes: #2545
Fixes: #1858
Fixes: #3026

@mpkorstanje Could you please briefly explain why all three issues would be fixed by merging this PR?

@marcphilipp

For #2545:

While the proposed solution involves a ForkJoinWorkerThreadFactory the underlying root cause follows from the inability to saturate the fork join pool. This was fixed by #2792 and can be achieved through a custom parallel strategy. This PR simplifies the process and should for this specific use case remove the need for more complicated solutions.

If more complicated solutions are needed I expect people will open a new issue to describe their exact problem in more detail.

For #1858:

The root cause was explained in #1858 (comment). When a test puts its executing thread to sleep, JUnit will start another test to keep up the desired parallism. However as explained in #1858 (comment) when using Selenium threads are commonly put to sleep and so it is desirable to limit the maximum pool size to limit the maximum number of web drivers created. This wasn't possible without saturating the pool.

This too was fixed by #2792 and can be achieved through a custom parallel strategy. This PR simplifies the process and should for this specific use case remove the need for more complicated solutions.

For #3026:

Essentially this issues requests a configuration parameter to limit the maximum pool size and allow the pool to be saturated. Removing the need to create a custom class for this very common scenario,

@OPeyrusse
Copy link

OPeyrusse commented Oct 17, 2022

⚠️ Even though I am not a member of this repository, I would like to warn you that what you did will not work with people also using ForkJoinPool. ⚠️
I explained in this comment on #1858 how a single thread can end up running 40+ tests "concurrently". By concurrently, I mean that 40+ tests can report as having start, before any can complete.
If this is a use case that you plan to support with this PR, I would gladly assist you in designing a test case that reproduce the issue. Don't hesitate to ping me if you want.

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Oct 17, 2022

Thanks for the heads-up! I didn't see that specific problem explicitly mentioned in the issues before so I left it out of scope.

I reckon the current PR can still be salvaged by fixing the wording. Limiting the max poolsize via a property is still useful.

Replacing the Fork Join Pool might be a bit more involved. 😅

@mpkorstanje mpkorstanje reopened this Oct 17, 2022
@OPeyrusse
Copy link

@mpkorstanje Do you want me to create a special test and open an issue for that particular problem?

@marcphilipp marcphilipp self-requested a review October 21, 2022 06:48
Copy link
Member

@marcphilipp marcphilipp left a comment

Choose a reason for hiding this comment

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

Looks good except for a few minor documentation things.

@mpkorstanje
Copy link
Contributor Author

@OPeyrusse yes, please do. I can't promise or commit to solving it. But perhaps someone can.

@mpkorstanje mpkorstanje changed the title Limit the maximum number of concurrently executing tests via a property Limit the max-pool-size for parallel execution via a property Oct 25, 2022
@mpkorstanje
Copy link
Contributor Author

I think this is what @OPeyrusse had in mind. The code below will report between 2-6 active tests as the .join() call will end up starting other tests. The number of active threads however does not exceed 2.

I've updated the documentation to say concurrent "threads" instead of "tests".

class ExampleTest {
    private static final Runnable sleep = () -> {
        try {
            Thread.sleep(600);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    };

    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) {
        ForkJoinPool.commonPool()
                .submit(sleep)
                .join();
    }

    @ParameterizedTest
    @MethodSource("numbers")
    void test2(int i) {
        ForkJoinPool.commonPool()
                .submit(sleep)
                .join();
    }
}

@mpkorstanje
Copy link
Contributor Author

@marcphilipp good to go.

@mpkorstanje
Copy link
Contributor Author

Changes applied!

For fixing small mistakes and typos please do feel free to push directly to my branch.

@marcphilipp marcphilipp changed the title Limit the max-pool-size for parallel execution via a property Limit max pool size for parallel execution via config param Dec 30, 2022
@marcphilipp marcphilipp merged commit 324802d into junit-team:main Dec 30, 2022
@marcphilipp marcphilipp modified the milestone: 5.9.2 Dec 30, 2022
@mpkorstanje mpkorstanje deleted the saturate-via-property branch December 30, 2022 15:35
marcphilipp added a commit that referenced this pull request Jan 6, 2023
marcphilipp pushed a commit that referenced this pull request Jan 6, 2023
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`
configuration parameter.

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.

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
behavior.

These changes were intentionally not made to the `dynamic` strategy to
limit the scope of this pull request. While I can reasonably predict
what behavior users of the `fixed` strategy might expect, I can not say
the same about the `dynamic` strategy.

Fixes #3026.
Fixes #2545.
Fixes #1858.
marcphilipp added a commit that referenced this pull request Jan 6, 2023
mpkorstanje added a commit to cucumber/cucumber-jvm that referenced this pull request Jan 12, 2023
With JUnit 5.9.2 we pull in junit-team/junit5#3044 which makes it
possible to limit the maximum number of threads used while
executing in parallel.

Closes: #2677
mpkorstanje added a commit to cucumber/cucumber-jvm that referenced this pull request Jan 12, 2023
With JUnit 5.9.2 we pull in junit-team/junit5#3044 which makes it
possible to limit the maximum number of threads used while
executing in parallel.

Closes: #2677
mpkorstanje added a commit to cucumber/cucumber-jvm that referenced this pull request Jan 12, 2023
With JUnit 5.9.2 we pull in junit-team/junit5#3044 which makes it
possible to limit the maximum number of threads used while
executing in parallel.

Closes: #2677
mpkorstanje added a commit to mpkorstanje/junit5 that referenced this pull request Mar 24, 2023
This is a followup of junit-team#3044 for the `dynamic` strategy.

Fixes: junit-team#3205
mpkorstanje added a commit to mpkorstanje/junit5 that referenced this pull request Mar 24, 2023
This is a followup of junit-team#3044 for the `dynamic` strategy.

Fixes: junit-team#3205
marcphilipp pushed a commit that referenced this pull request Apr 23, 2023
This is a followup of #3044 for the `dynamic` strategy.

Fixes #3205.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants