Skip to content

Add info contributor support for JDK 24's VirtualThreadSchedulerMXBean#43594

Closed
therepanic wants to merge 1 commit into
spring-projects:mainfrom
therepanic:add-info-contributor-support-for-VirtualThreadSchedulerMXBean
Closed

Add info contributor support for JDK 24's VirtualThreadSchedulerMXBean#43594
therepanic wants to merge 1 commit into
spring-projects:mainfrom
therepanic:add-info-contributor-support-for-VirtualThreadSchedulerMXBean

Conversation

@therepanic

@therepanic therepanic commented Dec 22, 2024

Copy link
Copy Markdown
Contributor

In this commit, for applications using Spring with JDK 24, I added support for viewing Virtual threads information in ProcessInfo using VirtualThreadSchedulerMXBean. The use of VirtualThreadSchedulerMXBean is done using reflection. If the current JDK version is less than 24, then we will return null, otherwise a class with all the required VirtualThreadsInfo information.

I also added a test checking if null is returned if the JDK version is less than 24.

There is also a point that bothers me, do you think we need an additional test to check the values of the VirtualThreadsInfo fields themselves or not?

Related to #43175

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Dec 22, 2024

@philwebb philwebb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks very much for the PR. I've added some review comments for your consideration.

@philwebb

Copy link
Copy Markdown
Member

do you think we need an additional test to check the values of the VirtualThreadsInfo fields themselves or not?

I think we could add something to the existing test just to check that we have sensible(ish) values.

@philwebb philwebb added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Dec 23, 2024
@philwebb philwebb added this to the 3.5.x milestone Dec 23, 2024
@therepanic therepanic force-pushed the add-info-contributor-support-for-VirtualThreadSchedulerMXBean branch from 6edc540 to d898b44 Compare December 25, 2024 18:57
@therepanic therepanic requested a review from philwebb December 25, 2024 18:57
@therepanic therepanic force-pushed the add-info-contributor-support-for-VirtualThreadSchedulerMXBean branch from d898b44 to 1f1b9b8 Compare December 25, 2024 19:00
@mhalbritter mhalbritter self-assigned this Jan 13, 2025
@mhalbritter

Copy link
Copy Markdown
Contributor

Thank you very much and congratulations on your first contribution 🎉!

@mhalbritter mhalbritter modified the milestones: 3.5.x, 3.5.0-M1 Jan 13, 2025
@nosan

nosan commented Jan 13, 2025

Copy link
Copy Markdown
Contributor

Since reflection is being used here, I wonder whether RuntimeHints should be registered for jdk.management.VirtualThreadSchedulerMXBean.

	static class ProcessInfoContributorRuntimeHints implements RuntimeHintsRegistrar {

   	private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();

   	@Override
   	public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
   		this.bindingRegistrar.registerReflectionHints(hints.reflection(), ProcessInfo.class);
   		hints.reflection()
   			.registerTypeIfPresent(classLoader, "jdk.management.VirtualThreadSchedulerMXBean",
   					MemberCategory.INVOKE_PUBLIC_METHODS);
   	}

   }

@mhalbritter

Copy link
Copy Markdown
Contributor

Yeah, good catch. I'll add that.

@cmdjulian

Copy link
Copy Markdown

For some reason after upgrading to Spring Boot v4, we started to see a lot of errors in our actuator endpoint. It started out first with

2026-01-12T18:08:40.150Z  WARN 7 --- [report-service] [dedElastic-3610] reactor.core.Exceptions                  : throwIfFatal detected a jvm fatal exception, which is thrown and logged below:

org.graalvm.nativeimage.MissingReflectionRegistrationError: Cannot reflectively invoke method 'public abstract long com.sun.management.OperatingSystemMXBean.getProcessCpuTime()'. To allow this operation, add the following to the 'reflection' section of 'reachability-metadata.json' and rebuild the native image:

  {
    "type": "com.sun.management.OperatingSystemMXBean",
    "methods": [
      {
        "name": "getProcessCpuTime",
        "parameterTypes": []
      }
    ]
  }

The 'reachability-metadata.json' file should be located in 'META-INF/native-image/<group-id>/<artifact-id>/' of your project. For further help, see https://www.graalvm.org/latest/reference-manual/native-image/metadata/#reflection
	at org.graalvm.nativeimage.builder/com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.reportInvokedExecutable(MissingReflectionRegistrationUtils.java:110) ~[na:na]
	at java.base@25.0.1/java.lang.reflect.Method.acquireMethodAccessor(Method.java:144) ~[generator:na]
	at java.base@25.0.1/java.lang.reflect.Method.invoke(Method.java:562) ~[generator:na]
	at io.micrometer.core.instrument.binder.system.ProcessorMetrics.invoke(ProcessorMetrics.java:150) ~[generator:1.16.1]
	at io.micrometer.core.instrument.binder.system.ProcessorMetrics.lambda$bindTo$2(ProcessorMetrics.java:140) ~[generator:1.16.1]
	at io.micrometer.core.instrument.cumulative.CumulativeFunctionCounter.count(CumulativeFunctionCounter.java:42) ~[na:na]
	at io.micrometer.prometheusmetrics.PrometheusMeterRegistry.lambda$newFunctionCounter$20(PrometheusMeterRegistry.java:381) ~[generator:1.16.1]
	at io.micrometer.prometheusmetrics.MicrometerCollector.collect(MicrometerCollector.java:84) ~[generator:1.16.1]
	at io.prometheus.metrics.model.registry.PrometheusRegistry.scrape(PrometheusRegistry.java:85) ~[generator:na]
	at io.prometheus.metrics.model.registry.PrometheusRegistry.scrape(PrometheusRegistry.java:67) ~[generator:na]
	at org.springframework.boot.micrometer.metrics.autoconfigure.export.prometheus.PrometheusScrapeEndpoint.scrape(PrometheusScrapeEndpoint.java:75) ~[generator:4.0.1]
	at java.base@25.0.1/java.lang.reflect.Method.invoke(Method.java:565) ~[generator:na]
	at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:281) ~[na:na]
	at org.springframework.boot.actuate.endpoint.invoke.reflect.ReflectiveOperationInvoker.invoke(ReflectiveOperationInvoker.java:76) ~[na:na]
	at org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredOperation.invoke(AbstractDiscoveredOperation.java:62) ~[generator:4.0.1]
	at org.springframework.boot.webflux.actuate.endpoint.web.AbstractWebFluxEndpointHandlerMapping$ElasticSchedulerInvoker.lambda$invoke$0(AbstractWebFluxEndpointHandlerMapping.java:283) ~[generator:4.0.1]
	at reactor.core.publisher.MonoCallable.call(MonoCallable.java:69) ~[na:na]
	at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:230) ~[generator:3.8.1]
	at reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler$SchedulerTask.run(BoundedElasticThreadPerTaskScheduler.java:1016) ~[generator:3.8.1]
	at java.base@25.0.1/java.lang.Thread.runWith(Thread.java:1487) ~[generator:na]
	at java.base@25.0.1/java.lang.VirtualThread.run(VirtualThread.java:456) ~[generator:na]
	at java.base@25.0.1/java.lang.VirtualThread$VThreadContinuation$1.run(VirtualThread.java:248) ~[na:na]	

So I added the required hins via running the tracing agent. I use native image with jdc ce 25.
What I'm now getting is unfortunately the following:

2026-01-12T19:02:58.265+01:00 ERROR 599985 --- [report-service] [ndedElastic-190] b.w.a.e.AbstractErrorWebExceptionHandler : [2e4294a1-99]  500 Server Error for HTTP GET "/actuator/info"

org.springframework.core.codec.EncodingException: JSON encoding error: jdk.management.VirtualThreadSchedulerMXBean is not a platform management interface
	at org.springframework.http.codec.AbstractJacksonEncoder.encodeValue(AbstractJacksonEncoder.java:240) ~[generator:7.0.2]
	Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ Handler Actuator web endpoint 'info' [DispatcherHandler]
	*__checkpoint ⇢ AuthorizationWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ ExceptionTranslationWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ ServerRequestCacheWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ SecurityContextServerWebExchangeWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ AuthenticationWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ ReactorContextWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ HttpHeaderWriterWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy@28ab92a5
	*__checkpoint ⇢ HTTP GET "/actuator/info" [ExceptionHandlingWebHandler]
Original Stack Trace:
		at org.springframework.http.codec.AbstractJacksonEncoder.encodeValue(AbstractJacksonEncoder.java:240) ~[generator:7.0.2]
		at org.springframework.http.codec.AbstractJacksonEncoder.lambda$encode$1(AbstractJacksonEncoder.java:151) ~[generator:7.0.2]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:114) ~[na:na]
		at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2565) ~[generator:3.8.1]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:172) ~[na:na]
		at reactor.core.publisher.MonoSingle$SingleSubscriber.doOnRequest(MonoSingle.java:104) ~[na:na]
		at reactor.core.publisher.Operators$MonoInnerProducerBase.request(Operators.java:2926) ~[generator:3.8.1]
		at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2361) ~[generator:3.8.1]
		at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2235) ~[generator:3.8.1]
		at reactor.core.publisher.MonoSingle$SingleSubscriber.onSubscribe(MonoSingle.java:116) ~[na:na]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96) ~[na:na]
		at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:56) ~[na:na]
		at reactor.core.publisher.FluxFromMonoOperator.subscribe(FluxFromMonoOperator.java:83) ~[generator:3.8.1]
		at reactor.core.publisher.FluxDeferContextual.subscribe(FluxDeferContextual.java:58) ~[na:na]
		at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:75) ~[generator:3.8.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:166) ~[generator:3.8.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondComplete(MonoFlatMap.java:246) ~[generator:3.8.1]
		at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:307) ~[generator:3.8.1]
		at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:123) ~[generator:3.8.1]
		at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:80) ~[na:na]
		at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:130) ~[na:na]
		at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:254) ~[generator:3.8.1]
		at reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler$SchedulerTask.run(BoundedElasticThreadPerTaskScheduler.java:1016) ~[generator:3.8.1]
		at java.base@25/java.lang.Thread.runWith(Thread.java:1487) ~[generator:na]
		at java.base@25/java.lang.VirtualThread.run(VirtualThread.java:456) ~[generator:na]
		at java.base@25/java.lang.VirtualThread$VThreadContinuation$1.run(VirtualThread.java:248) ~[na:na]
Caused by: tools.jackson.databind.DatabindException: jdk.management.VirtualThreadSchedulerMXBean is not a platform management interface
 at [No location information] (through reference chain: org.springframework.boot.actuate.endpoint.OperationResponseBodyMap["process"]->org.springframework.boot.info.ProcessInfo["virtualThreads"])
	at tools.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:275) ~[generator:3.0.3]
	at tools.jackson.databind.ser.UnrolledBeanSerializer.serializeNonFiltered(UnrolledBeanSerializer.java:215) ~[na:na]
	at tools.jackson.databind.ser.UnrolledBeanSerializer.serialize(UnrolledBeanSerializer.java:179) ~[na:na]
	at tools.jackson.databind.ser.jdk.MapSerializer.serializeOptionalFields(MapSerializer.java:718) ~[na:na]
	at tools.jackson.databind.ser.jdk.MapSerializer.serializeWithoutTypeInfo(MapSerializer.java:596) ~[na:na]
	at tools.jackson.databind.ser.jdk.MapSerializer.serialize(MapSerializer.java:556) ~[na:na]
	at tools.jackson.databind.ser.jdk.MapSerializer.serialize(MapSerializer.java:30) ~[na:na]
	at tools.jackson.databind.ser.SerializationContextExt._serialize(SerializationContextExt.java:456) ~[generator:3.0.3]
	at tools.jackson.databind.ser.SerializationContextExt.serializeValue(SerializationContextExt.java:387) ~[generator:3.0.3]
	at tools.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1361) ~[na:na]
	at tools.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:973) ~[generator:3.0.3]
	at org.springframework.http.codec.AbstractJacksonEncoder.encodeValue(AbstractJacksonEncoder.java:233) ~[generator:7.0.2]
	at org.springframework.http.codec.AbstractJacksonEncoder.lambda$encode$1(AbstractJacksonEncoder.java:151) ~[generator:7.0.2]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:114) ~[na:na]
	at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2565) ~[generator:3.8.1]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:172) ~[na:na]
	at reactor.core.publisher.MonoSingle$SingleSubscriber.doOnRequest(MonoSingle.java:104) ~[na:na]
	at reactor.core.publisher.Operators$MonoInnerProducerBase.request(Operators.java:2926) ~[generator:3.8.1]
	at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2361) ~[generator:3.8.1]
	at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2235) ~[generator:3.8.1]
	at reactor.core.publisher.MonoSingle$SingleSubscriber.onSubscribe(MonoSingle.java:116) ~[na:na]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96) ~[na:na]
	at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:56) ~[na:na]
	at reactor.core.publisher.FluxFromMonoOperator.subscribe(FluxFromMonoOperator.java:83) ~[generator:3.8.1]
	at reactor.core.publisher.FluxDeferContextual.subscribe(FluxDeferContextual.java:58) ~[na:na]
	at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:75) ~[generator:3.8.1]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:166) ~[generator:3.8.1]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondComplete(MonoFlatMap.java:246) ~[generator:3.8.1]
	at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:307) ~[generator:3.8.1]
	at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:123) ~[generator:3.8.1]
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:80) ~[na:na]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:130) ~[na:na]
	at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:254) ~[generator:3.8.1]
	at reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler$SchedulerTask.run(BoundedElasticThreadPerTaskScheduler.java:1016) ~[generator:3.8.1]
	at java.base@25/java.lang.Thread.runWith(Thread.java:1487) ~[generator:na]
	at java.base@25/java.lang.VirtualThread.run(VirtualThread.java:456) ~[generator:na]
	at java.base@25/java.lang.VirtualThread$VThreadContinuation$1.run(VirtualThread.java:248) ~[na:na]
Caused by: java.lang.IllegalArgumentException: jdk.management.VirtualThreadSchedulerMXBean is not a platform management interface
	at org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk.management.ManagementSupport.getPlatformMXBean(ManagementSupport.java:142) ~[na:na]
	at java.management@25/java.lang.management.ManagementFactory.getPlatformMXBean(ManagementFactory.java:56) ~[generator:na]
	at org.springframework.boot.info.ProcessInfo.getVirtualThreads(ProcessInfo.java:106) ~[generator:4.0.1]
	at org.graalvm.nativeimage.builder/com.oracle.svm.core.methodhandles.Util_java_lang_invoke_MethodHandle.invokeInternal(Target_java_lang_invoke_MethodHandle.java:221) ~[na:na]
	at java.base@25/java.lang.invoke.MethodHandle.invokeBasic(MethodHandle.java:102) ~[generator:na]
	at org.graalvm.nativeimage.builder/com.oracle.svm.core.methodhandles.MethodHandleIntrinsicImpl.execute(MethodHandleIntrinsicImpl.java:179) ~[na:na]
	at org.graalvm.nativeimage.builder/com.oracle.svm.core.methodhandles.Util_java_lang_invoke_MethodHandle.invokeInternal(Target_java_lang_invoke_MethodHandle.java:186) ~[na:na]
	at java.base@25/java.lang.invoke.MethodHandle.invokeBasic(MethodHandle.java:102) ~[generator:na]
	at java.base@25/java.lang.invoke.LambdaForm$NamedFunction.invokeWithArguments(LambdaForm.java:96) ~[na:na]
	at java.base@25/java.lang.invoke.LambdaForm.interpretName(LambdaForm.java:914) ~[generator:na]
	at java.base@25/java.lang.invoke.LambdaForm.interpretWithArguments(LambdaForm.java:891) ~[generator:na]
	at java.base@25/java.lang.invoke.MethodHandle.invokeBasic(MethodHandle.java:109) ~[generator:na]
	at java.base@25/java.lang.invoke.MethodHandle.invokeBasic(MethodHandle.java:0) ~[generator:na]
	at java.base@25/java.lang.invoke.Invokers$Holder.invokeExact_MT(Invokers$Holder) ~[na:na]
	at tools.jackson.databind.ser.BeanPropertyWriter.get(BeanPropertyWriter.java:750) ~[generator:3.0.3]
	at tools.jackson.databind.ser.BeanPropertyWriter.serializeAsProperty(BeanPropertyWriter.java:564) ~[generator:3.0.3]
	at tools.jackson.databind.ser.UnrolledBeanSerializer.serializeNonFiltered(UnrolledBeanSerializer.java:209) ~[na:na]
	... 35 common frames omitted

For some reason jdk.management.VirtualThreadSchedulerMXBean seems to be not registered as an MX bean with native image?
I could also create a dedicated bug ticket if you like, but I guess it is related to this one.

@philwebb

Copy link
Copy Markdown
Member

@cmdjulian Could you please open a new issue and provide a sample application that replicates the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants