Skip to content

Make OtlpMeterRegistry virtual thread aware #42407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.core.task.VirtualThreadTaskExecutor;

/**
* {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to OTLP.
Expand Down Expand Up @@ -72,10 +75,19 @@ OtlpConfig otlpConfig(OpenTelemetryProperties openTelemetryProperties,

@Bean
@ConditionalOnMissingBean
public OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock) {
@ConditionalOnThreading(Threading.PLATFORM)
public OtlpMeterRegistry otlpMeterRegistryPlatformThreads(OtlpConfig otlpConfig, Clock clock) {
return new OtlpMeterRegistry(otlpConfig, clock);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnThreading(Threading.VIRTUAL)
public OtlpMeterRegistry otlpMeterRegistryVirtualThreads(OtlpConfig otlpConfig, Clock clock) {
VirtualThreadTaskExecutor taskExecutor = new VirtualThreadTaskExecutor("otlp-meter-registry");
return new OtlpMeterRegistry(otlpConfig, clock, taskExecutor.getVirtualThreadFactory());
}

/**
* Adapts {@link OtlpProperties} to {@link OtlpMetricsConnectionDetails}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@

package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp;

import java.util.concurrent.ScheduledExecutorService;

import io.micrometer.core.instrument.Clock;
import io.micrometer.registry.otlp.OtlpConfig;
import io.micrometer.registry.otlp.OtlpMeterRegistry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;

import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpMetricsConnectionDetails;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.assertj.ScheduledExecutorServiceAssert;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
Expand Down Expand Up @@ -76,6 +81,32 @@ void allowsCustomConfigToBeUsed() {
.hasBean("customConfig"));
}

@Test
void allowsPlatformThreadsToBeUsed() {
this.contextRunner.withUserConfiguration(BaseConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(OtlpMeterRegistry.class);
OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class);
assertThat(registry).extracting("scheduledExecutorService")
.satisfies((executor) -> ScheduledExecutorServiceAssert.assertThat((ScheduledExecutorService) executor)
.usesPlatformThreads());
});
}

@Test
@EnabledForJreRange(min = JRE.JAVA_21)
void allowsVirtualThreadsToBeUsed() {
this.contextRunner.withUserConfiguration(BaseConfiguration.class)
.withPropertyValues("spring.threads.virtual.enabled=true")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpMeterRegistry.class);
OtlpMeterRegistry registry = context.getBean(OtlpMeterRegistry.class);
assertThat(registry).extracting("scheduledExecutorService")
.satisfies(
(executor) -> ScheduledExecutorServiceAssert.assertThat((ScheduledExecutorService) executor)
.usesVirtualThreads());
});
}

@Test
void allowsRegistryToBeCustomized() {
this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* 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
*
* https://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.springframework.boot.testsupport.assertj;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assert;

/**
* AssertJ {@link Assert} for {@link ScheduledThreadPoolExecutor}.
*
* @author Mike Turbe
* @author Moritz Halbritter
*/
public final class ScheduledExecutorServiceAssert
extends AbstractAssert<ScheduledExecutorServiceAssert, ScheduledExecutorService> {

private ScheduledExecutorServiceAssert(ScheduledExecutorService actual) {
super(actual, ScheduledExecutorServiceAssert.class);
}

/**
* Verifies that the actual executor uses platform threads.
* @return {@code this} assertion object
* @throws AssertionError if the actual executor doesn't use platform threads
*/
public ScheduledExecutorServiceAssert usesPlatformThreads() {
isNotNull();
if (producesVirtualThreads()) {
failWithMessage("Expected executor to use platform threads, but it uses virtual threads");
}
return this;
}

/**
* Verifies that the actual executor uses virtual threads.
* @return {@code this} assertion object
* @throws AssertionError if the actual executor doesn't use virtual threads
*/
public ScheduledExecutorServiceAssert usesVirtualThreads() {
isNotNull();
if (!producesVirtualThreads()) {
failWithMessage("Expected executor to use virtual threads, but it uses platform threads");
}
return this;
}

private boolean producesVirtualThreads() {
try {
return this.actual.schedule(() -> {
// https://openjdk.org/jeps/444
// jep 444 specifies that virtual threads will belong to
// a special thread group given the name "VirtualThreads"
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
String threadGroupName = (threadGroup != null) ? threadGroup.getName() : "";
return threadGroupName.equalsIgnoreCase("VirtualThreads");
}, 0, TimeUnit.SECONDS).get();
}
catch (InterruptedException | ExecutionException ex) {
throw new AssertionError(ex);
}
}

/**
* Creates a new assertion class with the given {@link ScheduledExecutorService}.
* @param actual the {@link ScheduledExecutorService}
* @return the assertion class
*/
public static ScheduledExecutorServiceAssert assertThat(ScheduledExecutorService actual) {
return new ScheduledExecutorServiceAssert(actual);
}

}
Loading