From 3c71ff039c0d5750e9f2180ef47cdf88da14e6af Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Wed, 12 Jun 2024 14:29:16 +0200 Subject: [PATCH 1/2] 4.x: Fail HelidonTests by default when pinning jfr event is detected #8857 Signed-off-by: Jorge Bescos Gascon --- .../testing/junit5/HelidonJunitExtension.java | 53 ++++++++++++++++++ .../junit5/src/main/java/module-info.java | 1 + .../testing/testng/HelidonTestNgListener.java | 55 +++++++++++++++++++ .../testng/src/main/java/module-info.java | 1 + .../testing/junit5/TestPinnedThread.java | 40 ++++++++++++++ .../testing/testng/TestPinnedThread.java | 40 ++++++++++++++ 6 files changed, 190 insertions(+) create mode 100644 microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java create mode 100644 microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThread.java diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java index 5599a1bc9fe..8c1f3ef5d75 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java @@ -59,6 +59,9 @@ import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.WebTarget; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedFrame; +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; @@ -77,6 +80,7 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import static org.junit.jupiter.api.Assertions.fail; /** * Junit5 extension to support Helidon CDI container in tests. @@ -100,7 +104,9 @@ class HelidonJunitExtension implements BeforeAllCallback, private final List classLevelExtensions = new ArrayList<>(); private final List classLevelBeans = new ArrayList<>(); + private final List jfrVTPinned = new ArrayList<>(); private final ConfigMeta classLevelConfigMeta = new ConfigMeta(); + private final RecordingStream recordingStream = new RecordingStream(); private boolean classLevelDisableDiscovery = false; private boolean resetPerTest; @@ -113,6 +119,8 @@ class HelidonJunitExtension implements BeforeAllCallback, @SuppressWarnings("unchecked") @Override public void beforeAll(ExtensionContext context) { + startRecordingStream(); + testClass = context.getRequiredTestClass(); AddConfig[] configs = getAnnotations(testClass, AddConfig.class); @@ -383,6 +391,27 @@ public void afterAll(ExtensionContext context) { stopContainer(); releaseConfig(); callAfterStop(); + closeRecordingStream(); + } + + private void startRecordingStream() { + recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace(); + recordingStream.onEvent("jdk.VirtualThreadPinned", event -> { + jfrVTPinned.add(new EventWrapper(event)); + }); + recordingStream.startAsync(); + } + + private void closeRecordingStream() { + try { + // Flush ending events + recordingStream.stop(); + if (!jfrVTPinned.isEmpty()) { + fail("Some pinned virtual threads were detected:\n" + jfrVTPinned); + } + } finally { + recordingStream.close(); + } } @Override @@ -748,4 +777,28 @@ public Class value() { } } + 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/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 de75e0235e5..43b3e57d166 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 @@ -60,6 +60,9 @@ import jakarta.inject.Singleton; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedFrame; +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; @@ -71,6 +74,8 @@ import org.testng.ITestResult; import org.testng.annotations.Test; +import static org.testng.Assert.fail; + /** * TestNG extension to support Helidon CDI container in tests. */ @@ -90,6 +95,8 @@ public class HelidonTestNgListener implements IClassListener, ITestListener { private List classLevelExtensions = new ArrayList<>(); private List classLevelBeans = new ArrayList<>(); private ConfigMeta classLevelConfigMeta = new ConfigMeta(); + private List jfrVTPinned; + private RecordingStream recordingStream; private boolean classLevelDisableDiscovery = false; private boolean resetPerTest; @@ -100,6 +107,7 @@ public class HelidonTestNgListener implements IClassListener, ITestListener { @Override public void onBeforeClass(ITestClass iTestClass) { + startRecordingStream(); testClass = iTestClass.getRealClass(); @@ -162,6 +170,7 @@ public void onAfterClass(ITestClass testClass) { releaseConfig(); stopContainer(); } + closeRecordingStream(); } @Override @@ -398,6 +407,27 @@ private void stopContainer() { } } + private void startRecordingStream() { + jfrVTPinned = new ArrayList<>(); + recordingStream = new RecordingStream(); + recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace(); + recordingStream.onEvent("jdk.VirtualThreadPinned", event -> { + jfrVTPinned.add(new EventWrapper(event)); + }); + recordingStream.startAsync(); + } + + private void closeRecordingStream() { + try { + // Flush ending events + recordingStream.stop(); + if (!jfrVTPinned.isEmpty()) { + fail("Some pinned virtual threads were detected:\n" + jfrVTPinned); + } + } finally { + recordingStream.close(); + } + } @SuppressWarnings("unchecked") private T[] getAnnotations(Class testClass, Class annotClass) { @@ -635,4 +665,29 @@ public Class value() { } } + 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/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..17ee366aff9 --- /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.HelidonTest; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@HelidonTest +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..ff95f24cf11 --- /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.HelidonTest; + +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; + +@HelidonTest +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(); + } +} From 37e3cbf0119a246da67b23e4732581e699ed1b9e Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Wed, 19 Jun 2024 07:28:24 +0200 Subject: [PATCH 2/2] Create PinnedThreadValidation Signed-off-by: Jorge Bescos Gascon --- .../testing/junit5/HelidonJunitExtension.java | 53 ----------- ...nPinnedThreadValidationJunitExtension.java | 94 +++++++++++++++++++ .../junit5/PinnedThreadValidation.java | 34 +++++++ .../testing/testng/HelidonTestNgListener.java | 38 ++++---- .../testng/PinnedThreadValidation.java | 31 ++++++ .../testing/junit5/TestPinnedThread.java | 4 +- .../testing/testng/TestPinnedThread.java | 4 +- ...nPinnedThreadValidationJunitExtension.java | 94 +++++++++++++++++++ .../junit5/PinnedThreadValidation.java | 34 +++++++ .../junit5/src/main/java/module-info.java | 3 +- .../testing/junit5/TestPinnedThread.java | 38 ++++++++ 11 files changed, 353 insertions(+), 74 deletions(-) create mode 100644 microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonPinnedThreadValidationJunitExtension.java create mode 100644 microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/PinnedThreadValidation.java create mode 100644 microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/PinnedThreadValidation.java create mode 100644 webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonPinnedThreadValidationJunitExtension.java create mode 100644 webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/PinnedThreadValidation.java create mode 100644 webserver/testing/junit5/junit5/src/test/java/io/helidon/webserver/testing/junit5/TestPinnedThread.java diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java index 8c1f3ef5d75..5599a1bc9fe 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java @@ -59,9 +59,6 @@ import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.WebTarget; -import jdk.jfr.consumer.RecordedEvent; -import jdk.jfr.consumer.RecordedFrame; -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; @@ -80,7 +77,6 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; -import static org.junit.jupiter.api.Assertions.fail; /** * Junit5 extension to support Helidon CDI container in tests. @@ -104,9 +100,7 @@ class HelidonJunitExtension implements BeforeAllCallback, private final List classLevelExtensions = new ArrayList<>(); private final List classLevelBeans = new ArrayList<>(); - private final List jfrVTPinned = new ArrayList<>(); private final ConfigMeta classLevelConfigMeta = new ConfigMeta(); - private final RecordingStream recordingStream = new RecordingStream(); private boolean classLevelDisableDiscovery = false; private boolean resetPerTest; @@ -119,8 +113,6 @@ class HelidonJunitExtension implements BeforeAllCallback, @SuppressWarnings("unchecked") @Override public void beforeAll(ExtensionContext context) { - startRecordingStream(); - testClass = context.getRequiredTestClass(); AddConfig[] configs = getAnnotations(testClass, AddConfig.class); @@ -391,27 +383,6 @@ public void afterAll(ExtensionContext context) { stopContainer(); releaseConfig(); callAfterStop(); - closeRecordingStream(); - } - - private void startRecordingStream() { - recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace(); - recordingStream.onEvent("jdk.VirtualThreadPinned", event -> { - jfrVTPinned.add(new EventWrapper(event)); - }); - recordingStream.startAsync(); - } - - private void closeRecordingStream() { - try { - // Flush ending events - recordingStream.stop(); - if (!jfrVTPinned.isEmpty()) { - fail("Some pinned virtual threads were detected:\n" + jfrVTPinned); - } - } finally { - recordingStream.close(); - } } @Override @@ -777,28 +748,4 @@ public Class value() { } } - 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/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..4033fef751d --- /dev/null +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/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.microprofile.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/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/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 43b3e57d166..1200793a3b1 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 @@ -99,6 +99,7 @@ public class HelidonTestNgListener implements IClassListener, ITestListener { private RecordingStream recordingStream; private boolean classLevelDisableDiscovery = false; private boolean resetPerTest; + private boolean pinnedThreadValidation; private Class testClass; private ConfigProviderResolver configProviderResolver; @@ -107,10 +108,11 @@ public class HelidonTestNgListener implements IClassListener, ITestListener { @Override public void onBeforeClass(ITestClass iTestClass) { - startRecordingStream(); - testClass = iTestClass.getRealClass(); + pinnedThreadValidation = testClass.getAnnotation(PinnedThreadValidation.class) != null; + startRecordingStream(); + AddConfig[] configs = getAnnotations(testClass, AddConfig.class); classLevelConfigMeta.addConfig(configs); classLevelConfigMeta.configuration(testClass.getAnnotation(Configuration.class)); @@ -408,24 +410,28 @@ private void stopContainer() { } private void startRecordingStream() { - jfrVTPinned = new ArrayList<>(); - recordingStream = new RecordingStream(); - recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace(); - recordingStream.onEvent("jdk.VirtualThreadPinned", event -> { - jfrVTPinned.add(new EventWrapper(event)); - }); - recordingStream.startAsync(); + 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(); + } } private void closeRecordingStream() { - try { - // Flush ending events - recordingStream.stop(); - if (!jfrVTPinned.isEmpty()) { - fail("Some pinned virtual threads were detected:\n" + jfrVTPinned); + if (pinnedThreadValidation) { + try { + // Flush ending events + recordingStream.stop(); + if (!jfrVTPinned.isEmpty()) { + fail("Some pinned virtual threads were detected:\n" + jfrVTPinned); + } + } finally { + recordingStream.close(); } - } finally { - recordingStream.close(); } } 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/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 index 17ee366aff9..2e5ea48e6db 100644 --- 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 @@ -16,12 +16,12 @@ package io.helidon.microprofile.tests.testing.junit5; -import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.microprofile.testing.junit5.PinnedThreadValidation; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -@HelidonTest +@PinnedThreadValidation class TestPinnedThread { @Test 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 index ff95f24cf11..6660b31425a 100644 --- 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 @@ -16,12 +16,12 @@ package io.helidon.microprofile.tests.testing.testng; -import io.helidon.microprofile.testing.testng.HelidonTest; +import io.helidon.microprofile.testing.testng.PinnedThreadValidation; import org.testng.annotations.Ignore; import org.testng.annotations.Test; -@HelidonTest +@PinnedThreadValidation class TestPinnedThread { @Test 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(); + } +}