forked from projectnessie/nessie
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
"Thread per test class" via
MultiEnvTestEngine
See projectnessie#9441 for a complete description of the underlying problem. TL;DR is: `ThreadLocal`s from various 3rd party libraries leak into the single `Test worker` thread that runs all the tests, resulting in TL objects/suppliers from the various Quarkus test class loaders, eventually leading to nasty OOMs. This change updates the `MultiEnvTestEngine` by using the new `ThreadPerTestClassExecutionExecutorService` and also "assimilate" really all tests, even the non-multi-env tests, so that those also run on a thread per test-class. The logic to distinguish multi-env from non-multi-env tests via `MultiEnvExtensionRegistry.registerExtension()` via test discovery is not perfect (but good enough), it can add multi-env tests to the non-multi-env tests, so an additional check is needed there. Since each test class runs on "its own thread", the `ThreadLocal`s are registered on that thread. Once the test class finishes, the thread is disposed and its thread locals become eligible for garbage collection, which is what is needed. The bump of the max-heap size for test workers is also reduced back to 2g (was changed in projectnessie#9433). Fixes projectnessie#9441
- Loading branch information
Showing
6 changed files
with
301 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
.../main/java/org/projectnessie/junit/engine/ThreadPerTestClassExecutionExecutorService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/* | ||
* Copyright (C) 2024 Dremio | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.projectnessie.junit.engine; | ||
|
||
import static java.util.concurrent.CompletableFuture.completedFuture; | ||
|
||
import java.lang.reflect.Field; | ||
import java.util.List; | ||
import java.util.concurrent.Future; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
import org.junit.platform.engine.TestDescriptor; | ||
import org.junit.platform.engine.UniqueId; | ||
import org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutorService; | ||
|
||
/** | ||
* Implements a JUnit test executor that provides thread-per-test-class behavior. | ||
* | ||
* <p>"Thread-per-test-class behavior" is needed to prevent the class/class-loader leak via {@link | ||
* ThreadLocal}s as described in <a | ||
* href="https://github.com/projectnessie/nessie/issues/9441">#9441</a>. | ||
*/ | ||
public class ThreadPerTestClassExecutionExecutorService implements HierarchicalTestExecutorService { | ||
|
||
private static final Class<?> CLASS_NODE_TEST_TASK; | ||
private static final Field FIELD_TEST_DESCRIPTOR; | ||
|
||
static { | ||
try { | ||
CLASS_NODE_TEST_TASK = | ||
Class.forName("org.junit.platform.engine.support.hierarchical.NodeTestTask"); | ||
FIELD_TEST_DESCRIPTOR = CLASS_NODE_TEST_TASK.getDeclaredField("testDescriptor"); | ||
FIELD_TEST_DESCRIPTOR.setAccessible(true); | ||
} catch (Exception e) { | ||
throw new RuntimeException( | ||
"ThreadPerExecutionExecutorService is probably not compatible with the current JUnit version", | ||
e); | ||
} | ||
} | ||
|
||
protected TestDescriptor getTestDescriptor(TestTask testTask) { | ||
if (!CLASS_NODE_TEST_TASK.isAssignableFrom(testTask.getClass())) { | ||
throw new IllegalArgumentException( | ||
testTask.getClass().getName() + " is not of type " + CLASS_NODE_TEST_TASK.getName()); | ||
} | ||
try { | ||
return (TestDescriptor) FIELD_TEST_DESCRIPTOR.get(testTask); | ||
} catch (IllegalAccessException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public ThreadPerTestClassExecutionExecutorService() {} | ||
|
||
@Override | ||
public Future<Void> submit(TestTask testTask) { | ||
executeTask(testTask); | ||
return completedFuture(null); | ||
} | ||
|
||
@Override | ||
public void invokeAll(List<? extends TestTask> tasks) { | ||
tasks.forEach(this::executeTask); | ||
} | ||
|
||
protected void executeTask(TestTask testTask) { | ||
TestDescriptor testDescriptor = getTestDescriptor(testTask); | ||
UniqueId.Segment lastSegment = testDescriptor.getUniqueId().getLastSegment(); | ||
String type = lastSegment.getType(); | ||
if ("class".equals(type)) { | ||
AtomicReference<Exception> failure = new AtomicReference<>(); | ||
Thread threadPerClass = | ||
new Thread( | ||
() -> { | ||
try { | ||
testTask.execute(); | ||
} catch (Exception e) { | ||
failure.set(e); | ||
} | ||
}, | ||
"TEST THREAD FOR " + lastSegment.getValue()); | ||
threadPerClass.setDaemon(true); | ||
threadPerClass.start(); | ||
try { | ||
threadPerClass.join(); | ||
} catch (InterruptedException e) { | ||
// delegate a thread-interrupt | ||
threadPerClass.interrupt(); | ||
} | ||
Exception ex = failure.get(); | ||
if (ex instanceof RuntimeException) { | ||
throw (RuntimeException) ex; | ||
} else if (ex != null) { | ||
throw new RuntimeException(ex); | ||
} | ||
} else { | ||
testTask.execute(); | ||
} | ||
} | ||
|
||
@Override | ||
public void close() { | ||
// nothing to do | ||
} | ||
} |
Oops, something went wrong.