diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonPinnedThreadValidationJunitExtension.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonPinnedThreadValidationJunitExtension.java new file mode 100644 index 00000000000..0a0b42593df --- /dev/null +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonPinnedThreadValidationJunitExtension.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.microprofile.testing.junit5; + +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingStream; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * JUnit5 extension to support pinned threads validation. + */ +class HelidonPinnedThreadValidationJunitExtension implements BeforeAllCallback, AfterAllCallback { + + private RecordingStream recordingStream; + private boolean pinnedThreadValidation; + private PinningException pinningException; + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + Class testClass = context.getRequiredTestClass(); + pinnedThreadValidation = testClass.getAnnotation(PinnedThreadValidation.class) != null; + if (pinnedThreadValidation) { + recordingStream = new RecordingStream(); + recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace(); + recordingStream.onEvent("jdk.VirtualThreadPinned", this::record); + recordingStream.startAsync(); + } + } + + void record(RecordedEvent event) { + PinningException e = new PinningException(event); + if (pinningException == null) { + pinningException = e; + } else { + pinningException.addSuppressed(e); + } + } + + @Override + public void afterAll(ExtensionContext context) { + if (pinnedThreadValidation) { + try { + // Flush ending events + recordingStream.stop(); + if (pinningException != null) { + throw pinningException; + } + } finally { + recordingStream.close(); + } + } + } + + private static class PinningException extends AssertionError { + private final RecordedEvent recordedEvent; + + PinningException(RecordedEvent recordedEvent) { + this.recordedEvent = recordedEvent; + if (recordedEvent.getStackTrace() != null) { + StackTraceElement[] stackTraceElements = recordedEvent.getStackTrace().getFrames().stream() + .map(f -> new StackTraceElement(f.getMethod().getType().getName(), + f.getMethod().getName(), + f.getMethod().getType().getName() + ".java", + f.getLineNumber())) + .toArray(StackTraceElement[]::new); + super.setStackTrace(stackTraceElements); + } + } + + @Override + public String getMessage() { + return "Pinned virtual threads were detected:\n" + + recordedEvent.toString(); + } + } +} diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/PinnedThreadValidation.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/PinnedThreadValidation.java new file mode 100644 index 00000000000..49f3a2f6fde --- /dev/null +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/PinnedThreadValidation.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.microprofile.testing.junit5; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * An annotation making this test class to fail at the end if a pinned virtual thread was detected. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ExtendWith(HelidonPinnedThreadValidationJunitExtension.class) +@Inherited +public @interface PinnedThreadValidation { +} diff --git a/microprofile/testing/junit5/src/main/java/module-info.java b/microprofile/testing/junit5/src/main/java/module-info.java index f9514942be0..e20192d5d8a 100644 --- a/microprofile/testing/junit5/src/main/java/module-info.java +++ b/microprofile/testing/junit5/src/main/java/module-info.java @@ -24,6 +24,7 @@ requires io.helidon.microprofile.cdi; requires jakarta.inject; requires org.junit.jupiter.api; + requires jdk.jfr; requires transitive jakarta.cdi; requires transitive jakarta.ws.rs; diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java index a90b6b38492..66248244198 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java @@ -59,6 +59,8 @@ import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.ws.rs.client.ClientBuilder; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingStream; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigBuilder; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; @@ -88,23 +90,29 @@ public class HelidonTestNgListener implements IClassListener, ITestListener { private List classLevelExtensions = new ArrayList<>(); private List classLevelBeans = new ArrayList<>(); private ConfigMeta classLevelConfigMeta = new ConfigMeta(); + private RecordingStream recordingStream; private boolean classLevelDisableDiscovery = false; private boolean resetPerTest; + private boolean pinnedThreadValidation; private Class testClass; private Object testInstance; private ConfigProviderResolver configProviderResolver; private Config config; private SeContainer container; + private PinningException pinningException; @Override public void onBeforeClass(ITestClass iTestClass) { - testClass = iTestClass.getRealClass(); List metaAnnotations = extractMetaAnnotations(testClass); AddConfig[] configs = getAnnotations(testClass, AddConfig.class, metaAnnotations); + pinnedThreadValidation = testClass.getAnnotation(PinnedThreadValidation.class) != null; + startRecordingStream(); + + AddConfig[] configs = getAnnotations(testClass, AddConfig.class); classLevelConfigMeta.addConfig(configs); classLevelConfigMeta.configuration(getAnnotation(testClass, Configuration.class, metaAnnotations)); classLevelConfigMeta.addConfigBlock(getAnnotation(testClass, AddConfigBlock.class, metaAnnotations)); @@ -161,6 +169,7 @@ public void onAfterClass(ITestClass testClass) { releaseConfig(); stopContainer(); } + closeRecordingStream(); } @Override @@ -358,6 +367,30 @@ private T getAnnotation(Class testClass, Class anno return annotation; } + private void startRecordingStream() { + if (pinnedThreadValidation) { + pinningException = null; + recordingStream = new RecordingStream(); + recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace(); + recordingStream.onEvent("jdk.VirtualThreadPinned", this::record); + recordingStream.startAsync(); + } + } + + private void closeRecordingStream() { + if (pinnedThreadValidation) { + try { + // Flush ending events + recordingStream.stop(); + if (pinningException != null) { + throw pinningException; + } + } finally { + recordingStream.close(); + } + } + } + @SuppressWarnings("unchecked") private T[] getAnnotations(Class testClass, Class annotClass, List metaAnnotations) { @@ -431,6 +464,15 @@ private static boolean hasAnnotation(AnnotatedElement element, Set testClass) implements Extension { @@ -645,4 +687,28 @@ public Class value() { private static final class SingletonLiteral extends AnnotationLiteral implements Singleton { static final SingletonLiteral INSTANCE = new SingletonLiteral(); } + + private static class PinningException extends AssertionError { + private final RecordedEvent recordedEvent; + + PinningException(RecordedEvent recordedEvent) { + this.recordedEvent = recordedEvent; + if (recordedEvent.getStackTrace() != null) { + StackTraceElement[] stackTraceElements = recordedEvent.getStackTrace().getFrames().stream() + .map(f -> new StackTraceElement(f.getMethod().getType().getName(), + f.getMethod().getName(), + f.getMethod().getType().getName() + ".java", + f.getLineNumber())) + .toArray(StackTraceElement[]::new); + super.setStackTrace(stackTraceElements); + } + } + + @Override + public String getMessage() { + return "Pinned virtual threads were detected:\n" + + recordedEvent.toString(); + } + } + } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/PinnedThreadValidation.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/PinnedThreadValidation.java new file mode 100644 index 00000000000..e3be0b51783 --- /dev/null +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/PinnedThreadValidation.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.microprofile.testing.testng; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation making this test class to fail at the end if a pinned virtual thread was detected. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface PinnedThreadValidation { +} diff --git a/microprofile/testing/testng/src/main/java/module-info.java b/microprofile/testing/testng/src/main/java/module-info.java index f5a4aa1feb3..6a5d76347fe 100644 --- a/microprofile/testing/testng/src/main/java/module-info.java +++ b/microprofile/testing/testng/src/main/java/module-info.java @@ -27,6 +27,7 @@ requires jakarta.cdi; requires jakarta.inject; requires jakarta.ws.rs; + requires jdk.jfr; requires microprofile.config.api; requires org.testng; diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java new file mode 100644 index 00000000000..2e5ea48e6db --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.microprofile.tests.testing.junit5; + +import io.helidon.microprofile.testing.junit5.PinnedThreadValidation; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@PinnedThreadValidation +class TestPinnedThread { + + @Test + @Disabled("Enable to verify pinned threads fails") + void test() throws InterruptedException { + Thread.ofVirtual().start(() -> { + synchronized (this) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } + }).join(); + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThread.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThread.java new file mode 100644 index 00000000000..6660b31425a --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThread.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.microprofile.tests.testing.testng; + +import io.helidon.microprofile.testing.testng.PinnedThreadValidation; + +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; + +@PinnedThreadValidation +class TestPinnedThread { + + @Test + @Ignore("Enable to verify pinned threads fails") + void test() throws InterruptedException { + Thread.ofVirtual().start(() -> { + synchronized (this) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } + }).join(); + } +} diff --git a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonPinnedThreadValidationJunitExtension.java b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonPinnedThreadValidationJunitExtension.java new file mode 100644 index 00000000000..a45e9de5656 --- /dev/null +++ b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonPinnedThreadValidationJunitExtension.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.testing.junit5; + +import java.util.ArrayList; +import java.util.List; + +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedFrame; +import jdk.jfr.consumer.RecordingStream; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * JUnit5 extension to support pinned threads validation. + */ +class HelidonPinnedThreadValidationJunitExtension implements BeforeAllCallback, AfterAllCallback { + + private List jfrVTPinned; + private RecordingStream recordingStream; + private boolean pinnedThreadValidation; + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + Class testClass = context.getRequiredTestClass(); + pinnedThreadValidation = testClass.getAnnotation(PinnedThreadValidation.class) != null; + if (pinnedThreadValidation) { + jfrVTPinned = new ArrayList<>(); + recordingStream = new RecordingStream(); + recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace(); + recordingStream.onEvent("jdk.VirtualThreadPinned", event -> { + jfrVTPinned.add(new EventWrapper(event)); + }); + recordingStream.startAsync(); + } + } + + @Override + public void afterAll(ExtensionContext context) { + if (pinnedThreadValidation) { + try { + // Flush ending events + recordingStream.stop(); + if (!jfrVTPinned.isEmpty()) { + fail("Some pinned virtual threads were detected:\n" + jfrVTPinned); + } + } finally { + recordingStream.close(); + } + } + } + + private static class EventWrapper { + + private final RecordedEvent recordedEvent; + + private EventWrapper(RecordedEvent recordedEvent) { + this.recordedEvent = recordedEvent; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(recordedEvent.toString()); + if (recordedEvent.getStackTrace() != null) { + builder.append("full-stackTrace = ["); + List frames = recordedEvent.getStackTrace().getFrames(); + for (RecordedFrame frame : frames) { + builder.append("\n\t").append(frame.getMethod().getType().getName()) + .append("#").append(frame.getMethod().getName()) + .append("(").append(frame.getLineNumber()).append(")"); + } + builder.append("\n]"); + } + return builder.toString(); + } + } +} diff --git a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/PinnedThreadValidation.java b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/PinnedThreadValidation.java new file mode 100644 index 00000000000..3c2f343f5f6 --- /dev/null +++ b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/PinnedThreadValidation.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.testing.junit5; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * An annotation making this test class to fail at the end if a pinned virtual thread was detected. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ExtendWith(HelidonPinnedThreadValidationJunitExtension.class) +@Inherited +public @interface PinnedThreadValidation { +} diff --git a/webserver/testing/junit5/junit5/src/main/java/module-info.java b/webserver/testing/junit5/junit5/src/main/java/module-info.java index 3f3ad38967b..f2ec443a9f7 100644 --- a/webserver/testing/junit5/junit5/src/main/java/module-info.java +++ b/webserver/testing/junit5/junit5/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ module io.helidon.webserver.testing.junit5 { requires io.helidon.logging.common; + requires jdk.jfr; requires transitive hamcrest.all; requires transitive io.helidon.common.testing.http.junit5; diff --git a/webserver/testing/junit5/junit5/src/test/java/io/helidon/webserver/testing/junit5/TestPinnedThread.java b/webserver/testing/junit5/junit5/src/test/java/io/helidon/webserver/testing/junit5/TestPinnedThread.java new file mode 100644 index 00000000000..a39937d0b31 --- /dev/null +++ b/webserver/testing/junit5/junit5/src/test/java/io/helidon/webserver/testing/junit5/TestPinnedThread.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.webserver.testing.junit5; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@PinnedThreadValidation +class TestPinnedThread { + + @Test + @Disabled("Enable to verify pinned threads fails") + void test() throws InterruptedException { + Thread.ofVirtual().start(() -> { + synchronized (this) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } + }).join(); + } +}