From 2ad60b40f52c329d4f66e7896177fc78f5faea96 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Wed, 12 Mar 2025 00:28:08 +0900 Subject: [PATCH 1/3] Support instrumentation for Executors.newVirtualThreadPerTaskExecutor() Closes gh-5488 Signed-off-by: Johnny Lim --- .../binder/jvm/ExecutorServiceMetrics.java | 42 +++++++++++++++++++ micrometer-java11/build.gradle | 1 + ...ExecutorServiceMetricsReflectiveTests.java | 24 +++++++++++ 3 files changed, 67 insertions(+) diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java index b45952b7d0..7c90987fa8 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java @@ -29,6 +29,7 @@ import io.micrometer.core.instrument.internal.TimedScheduledExecutorService; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.List; import java.util.Set; import java.util.concurrent.*; @@ -56,6 +57,11 @@ @NonNullFields public class ExecutorServiceMetrics implements MeterBinder { + private static final String CLASS_NAME_THREAD_PER_TASK_EXECUTOR = "java.util.concurrent.ThreadPerTaskExecutor"; + + @Nullable + private static final Method METHOD_THREAD_COUNT_FROM_THREAD_PER_TASK_EXECUTOR = getMethodForThreadCountFromThreadPerTaskExecutor(); + private static boolean allowIllegalReflectiveAccess = true; private static final InternalLogger log = InternalLoggerFactory.getInstance(ExecutorServiceMetrics.class); @@ -315,6 +321,9 @@ else if (className.equals("java.util.concurrent.Executors$FinalizableDelegatedEx monitor(registry, unwrapThreadPoolExecutor(executorService, executorService.getClass().getSuperclass())); } + else if (className.equals(CLASS_NAME_THREAD_PER_TASK_EXECUTOR)) { + monitorThreadPerTaskExecutor(registry, executorService); + } else { log.warn("Failed to bind as {} is unsupported.", className); } @@ -439,6 +448,39 @@ private void monitor(MeterRegistry registry, ForkJoinPool fj) { registeredMeterIds.addAll(meters.stream().map(Meter::getId).collect(toSet())); } + private void monitorThreadPerTaskExecutor(MeterRegistry registry, ExecutorService executorService) { + List meters = asList(Gauge + .builder(metricPrefix + "executor.active", executorService, + ExecutorServiceMetrics::getThreadCountFromThreadPerTaskExecutor) + .tags(tags) + .description("The approximate number of threads that are actively executing tasks") + .baseUnit(BaseUnits.THREADS) + .register(registry)); + registeredMeterIds.addAll(meters.stream().map(Meter::getId).collect(toSet())); + } + + private static long getThreadCountFromThreadPerTaskExecutor(ExecutorService executorService) { + try { + return (long) METHOD_THREAD_COUNT_FROM_THREAD_PER_TASK_EXECUTOR.invoke(executorService); + } + catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @Nullable + private static Method getMethodForThreadCountFromThreadPerTaskExecutor() { + try { + Class clazz = Class.forName(CLASS_NAME_THREAD_PER_TASK_EXECUTOR); + Method method = clazz.getMethod("threadCount"); + method.setAccessible(true); + return method; + } + catch (Throwable e) { + return null; + } + } + /** * Disable illegal reflective accesses. * diff --git a/micrometer-java11/build.gradle b/micrometer-java11/build.gradle index 10364ba871..f3603e802b 100644 --- a/micrometer-java11/build.gradle +++ b/micrometer-java11/build.gradle @@ -7,6 +7,7 @@ dependencies { testImplementation 'ru.lanwen.wiremock:wiremock-junit5' testImplementation 'com.github.tomakehurst:wiremock-jre8-standalone' testImplementation project(":micrometer-observation-test") + testImplementation libs.awaitility } java { diff --git a/micrometer-java21/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsReflectiveTests.java b/micrometer-java21/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsReflectiveTests.java index 0b79c6ac4c..4242038983 100644 --- a/micrometer-java21/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsReflectiveTests.java +++ b/micrometer-java21/src/test/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetricsReflectiveTests.java @@ -19,14 +19,17 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import java.time.Duration; import java.util.concurrent.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * Tests for {@link ExecutorServiceMetrics} with reflection enabled. * * @author Tommy Ludwig + * @author Johnny Lim */ @Tag("reflective") class ExecutorServiceMetricsReflectiveTests { @@ -45,4 +48,25 @@ void threadPoolMetricsWith_AutoShutdownDelegatedExecutorService() throws Interru assertThat(registry.get("executor.completed").tag("name", "test").functionCounter().count()).isEqualTo(1L); } + @Test + void monitorWithExecutorsNewVirtualThreadPerTaskExecutor() { + CountDownLatch latch = new CountDownLatch(1); + ExecutorService unmonitored = Executors.newVirtualThreadPerTaskExecutor(); + assertThat(unmonitored.getClass().getName()).isEqualTo("java.util.concurrent.ThreadPerTaskExecutor"); + ExecutorService monitored = ExecutorServiceMetrics.monitor(registry, unmonitored, "test"); + monitored.execute(() -> { + try { + latch.await(1, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + await().atMost(Duration.ofSeconds(1)) + .untilAsserted(() -> assertThat(registry.get("executor.active").gauge().value()).isEqualTo(1)); + latch.countDown(); + await().atMost(Duration.ofSeconds(1)) + .untilAsserted(() -> assertThat(registry.get("executor.active").gauge().value()).isEqualTo(0)); + } + } From d99f98427f1ed978ec80d0ffc980cd6e87a878e4 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Wed, 12 Mar 2025 10:04:54 +0900 Subject: [PATCH 2/3] Remove accidental Awaitility dependency Signed-off-by: Johnny Lim --- micrometer-java11/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/micrometer-java11/build.gradle b/micrometer-java11/build.gradle index f3603e802b..10364ba871 100644 --- a/micrometer-java11/build.gradle +++ b/micrometer-java11/build.gradle @@ -7,7 +7,6 @@ dependencies { testImplementation 'ru.lanwen.wiremock:wiremock-junit5' testImplementation 'com.github.tomakehurst:wiremock-jre8-standalone' testImplementation project(":micrometer-observation-test") - testImplementation libs.awaitility } java { From e5d159bb46185ab1cb078eaa5f22f729ed11c992 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 14 Mar 2025 00:21:20 +0900 Subject: [PATCH 3/3] Replace Method with MethodHandle for ThreadPerTaskExecutor.threadCount() Signed-off-by: Johnny Lim --- .../instrument/binder/jvm/ExecutorServiceMetrics.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java index 7c90987fa8..c63ba85f4d 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java @@ -28,6 +28,8 @@ import io.micrometer.core.instrument.internal.TimedExecutorService; import io.micrometer.core.instrument.internal.TimedScheduledExecutorService; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; @@ -60,7 +62,7 @@ public class ExecutorServiceMetrics implements MeterBinder { private static final String CLASS_NAME_THREAD_PER_TASK_EXECUTOR = "java.util.concurrent.ThreadPerTaskExecutor"; @Nullable - private static final Method METHOD_THREAD_COUNT_FROM_THREAD_PER_TASK_EXECUTOR = getMethodForThreadCountFromThreadPerTaskExecutor(); + private static final MethodHandle METHOD_HANDLE_THREAD_COUNT_FROM_THREAD_PER_TASK_EXECUTOR = getMethodHandleForThreadCountFromThreadPerTaskExecutor(); private static boolean allowIllegalReflectiveAccess = true; @@ -461,7 +463,7 @@ private void monitorThreadPerTaskExecutor(MeterRegistry registry, ExecutorServic private static long getThreadCountFromThreadPerTaskExecutor(ExecutorService executorService) { try { - return (long) METHOD_THREAD_COUNT_FROM_THREAD_PER_TASK_EXECUTOR.invoke(executorService); + return (long) METHOD_HANDLE_THREAD_COUNT_FROM_THREAD_PER_TASK_EXECUTOR.invoke(executorService); } catch (Throwable e) { throw new RuntimeException(e); @@ -469,12 +471,12 @@ private static long getThreadCountFromThreadPerTaskExecutor(ExecutorService exec } @Nullable - private static Method getMethodForThreadCountFromThreadPerTaskExecutor() { + private static MethodHandle getMethodHandleForThreadCountFromThreadPerTaskExecutor() { try { Class clazz = Class.forName(CLASS_NAME_THREAD_PER_TASK_EXECUTOR); Method method = clazz.getMethod("threadCount"); method.setAccessible(true); - return method; + return MethodHandles.lookup().unreflect(method); } catch (Throwable e) { return null;