This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/master by this push:
new 46c16259cd Add virtual thread support for gRPC and HTTP servers on JDK
25+ (#13705)
46c16259cd is described below
commit 46c16259cdba713522a20dc41db2169a54cc1435
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Sun Feb 22 19:33:38 2026 +0800
Add virtual thread support for gRPC and HTTP servers on JDK 25+ (#13705)
Add virtual thread support for gRPC and Armeria HTTP servers on JDK 25+
Enable virtual-thread-per-task executors for all 4 gRPC and 7 HTTP
server handler pools when running on JDK 25+, with automatic fallback
to platform thread pools on older JDKs. JDK 25 is required as the
first LTS with the synchronized pinning fix (JEP 491).
Key changes:
- VirtualThreads: reflection-based detection and executor factory
- VirtualThreadScheduledExecutor: virtual-thread-backed scheduled
executor for Armeria's blockingTaskExecutor
- Default Docker image changed to JDK 25; JDK 11/17/21 variants kept
- Kill switch: SW_VIRTUAL_THREADS_ENABLED=false
On JDK 25+, ~9 ForkJoinPool carrier threads replace up to 800+
platform threads across all handler pools.
---
.github/workflows/publish-docker.yaml | 10 +-
docker/oap/Dockerfile | 2 +-
docs/en/changes/changes.md | 11 +
docs/en/setup/backend/configuration-vocabulary.md | 10 +
.../oap/server/core/CoreModuleProvider.java | 2 +
.../oap/server/library/server/grpc/GRPCServer.java | 26 +-
.../oap/server/library/server/http/HTTPServer.java | 15 ++
.../util/VirtualThreadScheduledExecutor.java | 261 +++++++++++++++++++++
.../oap/server/library/util/VirtualThreads.java | 201 ++++++++++++++++
.../server/library/util/VirtualThreadsTest.java | 164 +++++++++++++
.../skywalking/oap/query/logql/LogQLProvider.java | 1 +
.../oap/query/promql/PromQLProvider.java | 1 +
.../oap/query/zipkin/ZipkinQueryProvider.java | 1 +
.../AWSFirehoseReceiverModuleProvider.java | 1 +
.../envoy/EnvoyMetricReceiverProvider.java | 1 +
.../ebpf/provider/EBPFReceiverProvider.java | 1 +
.../server/SharingServerModuleProvider.java | 2 +
.../receiver/zipkin/ZipkinReceiverProvider.java | 1 +
18 files changed, 700 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/publish-docker.yaml
b/.github/workflows/publish-docker.yaml
index de78893219..3e2a3c757d 100644
--- a/.github/workflows/publish-docker.yaml
+++ b/.github/workflows/publish-docker.yaml
@@ -75,6 +75,11 @@ jobs:
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+ - name: Build and push docker images based on Java 11
+ env:
+ SW_OAP_BASE_IMAGE: eclipse-temurin:11-jre
+ TAG: ${{ env.TAG }}-java11
+ run: make build.all docker.push
- name: Build and push docker images based on Java 17
env:
SW_OAP_BASE_IMAGE: eclipse-temurin:17-jre
@@ -85,11 +90,6 @@ jobs:
SW_OAP_BASE_IMAGE: eclipse-temurin:21-jre
TAG: ${{ env.TAG }}-java21
run: make build.all docker.push
- - name: Build and push docker images based on Java 25
- env:
- SW_OAP_BASE_IMAGE: eclipse-temurin:25-jre
- TAG: ${{ env.TAG }}-java25
- run: make build.all docker.push
- name: Build and push docker images
run: make build.all docker.push
- name: Build and push data-generator image
diff --git a/docker/oap/Dockerfile b/docker/oap/Dockerfile
index b4072c746d..5e346b8023 100644
--- a/docker/oap/Dockerfile
+++ b/docker/oap/Dockerfile
@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-ARG BASE_IMAGE='eclipse-temurin:11-jre'
+ARG BASE_IMAGE='eclipse-temurin:25-jre'
FROM $BASE_IMAGE
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index ec5d97fd4c..9cdeebd7ff 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -12,6 +12,17 @@
* Add `CLAUDE.md` as AI assistant guide for the project.
* Upgrade Groovy to 5.0.3 in OAP backend.
* Bump up nodejs to v24.13.0 for the latest UI(booster-ui) compiling.
+* Add virtual thread support (JDK 25+) for gRPC and Armeria HTTP server
handler threads.
+ Set `SW_VIRTUAL_THREADS_ENABLED=false` to disable.
+
+ | Pool | Threads (JDK < 25) | Threads (JDK 25+) |
+ |---|---|---|
+ | gRPC server handler (`core-grpc`, `receiver-grpc`, `als-grpc`,
`ebpf-grpc`) | Cached platform (unbounded) | Virtual threads |
+ | HTTP blocking (`core-http`, `receiver-http`, `promql-http`, `logql-http`,
`zipkin-query-http`, `zipkin-http`, `firehose-http`) | Cached platform (max
200) | Virtual threads |
+ | VT carrier threads (ForkJoinPool) | N/A | ~9 shared |
+
+ On JDK 25+, all 11 thread pools above share ~9 carrier threads instead of up
to 1,400+ platform threads.
+* Change default Docker base image to JDK 25 (`eclipse-temurin:25-jre`). JDK
11 kept as `-java11` variant.
#### OAP Server
diff --git a/docs/en/setup/backend/configuration-vocabulary.md
b/docs/en/setup/backend/configuration-vocabulary.md
index 93fd86460f..c1f512f468 100644
--- a/docs/en/setup/backend/configuration-vocabulary.md
+++ b/docs/en/setup/backend/configuration-vocabulary.md
@@ -542,6 +542,16 @@ OAP will query the data from the "hot and warm" stage by
default if the "warm" s
| property | - | - | The group
settings of property, such as UI and profiling.
| - | - |
| - | shardNum | - | Shards Number
for property group.
| SW_STORAGE_BANYANDB_PROPERTY_SHARD_NUM | 1 |
| - | replicas | - | Replicas for
property group.
|SW_STORAGE_BANYANDB_PROPERTY_REPLICAS | 0 |
+
+## Standalone Environment Variables
+The following environment variables are **not** backed by `application.yml`.
They are read directly from the
+process environment and take effect across all modules.
+
+| Environment Variable | Value(s) and Explanation
| Default |
+|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
+| SW_OAL_ENGINE_DEBUG | Set to any non-empty value to dump
OAL-generated `.class` files to disk (under the `oal-rt/` directory relative to
the OAP working path). Useful for debugging code generation issues. Leave unset
in production. | (not set, no
files written) |
+| SW_VIRTUAL_THREADS_ENABLED | Set to `false` to disable virtual
threads on JDK 25+. On JDK 25+, gRPC server handler threads and HTTP blocking
task executors are virtual threads by default. Set this variable to `false` to
force traditional platform thread pools. Ignored on JDK versions below 25. |
(not set, virtual threads enabled on JDK 25+) |
+
## Note
¹ System Environment Variable name could be declared and changed in
`application.yml/bydb.yaml`. The names listed here are simply
diff --git
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
index d55dc78853..337208602d 100755
---
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
+++
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModuleProvider.java
@@ -249,6 +249,7 @@ public class CoreModuleProvider extends ModuleProvider {
if (moduleConfig.getGRPCThreadPoolSize() > 0) {
grpcServer.setThreadPoolSize(moduleConfig.getGRPCThreadPoolSize());
}
+ grpcServer.setThreadPoolName("core-grpc");
grpcServer.initialize();
HTTPServerConfig httpServerConfig = HTTPServerConfig.builder()
@@ -264,6 +265,7 @@ public class CoreModuleProvider extends ModuleProvider {
setBootingParameter("oap.external.http.host",
moduleConfig.getRestHost());
setBootingParameter("oap.external.http.port",
moduleConfig.getRestPort());
httpServer = new HTTPServer(httpServerConfig);
+ httpServer.setBlockingTaskName("core-http");
httpServer.initialize();
this.registerServiceImplementation(ConfigService.class, new
ConfigService(moduleConfig, this));
diff --git
a/oap-server/server-library/library-server/src/main/java/org/apache/skywalking/oap/server/library/server/grpc/GRPCServer.java
b/oap-server/server-library/library-server/src/main/java/org/apache/skywalking/oap/server/library/server/grpc/GRPCServer.java
index 068597b166..3c55cbeb0c 100644
---
a/oap-server/server-library/library-server/src/main/java/org/apache/skywalking/oap/server/library/server/grpc/GRPCServer.java
+++
b/oap-server/server-library/library-server/src/main/java/org/apache/skywalking/oap/server/library/server/grpc/GRPCServer.java
@@ -38,6 +38,7 @@ import org.apache.skywalking.oap.server.library.server.Server;
import org.apache.skywalking.oap.server.library.server.ServerException;
import
org.apache.skywalking.oap.server.library.server.grpc.ssl.DynamicSslContext;
import
org.apache.skywalking.oap.server.library.server.pool.CustomThreadFactory;
+import org.apache.skywalking.oap.server.library.util.VirtualThreads;
@Slf4j
public class GRPCServer implements Server {
@@ -53,6 +54,7 @@ public class GRPCServer implements Server {
private String trustedCAsFile;
private DynamicSslContext sslContext;
private int threadPoolSize;
+ private String threadPoolName = "grpcServerPool";
private static final Marker SERVER_START_MARKER =
MarkerFactory.getMarker("Console");
public GRPCServer(String host, int port) {
@@ -72,6 +74,10 @@ public class GRPCServer implements Server {
this.threadPoolSize = threadPoolSize;
}
+ public void setThreadPoolName(String threadPoolName) {
+ this.threadPoolName = threadPoolName;
+ }
+
/**
* Require for `server.crt` and `server.pem` for open ssl at server side.
*
@@ -96,11 +102,21 @@ public class GRPCServer implements Server {
if (maxMessageSize > 0) {
nettyServerBuilder.maxInboundMessageSize(maxMessageSize);
}
- if (threadPoolSize > 0) {
- ExecutorService executor = new ThreadPoolExecutor(
- threadPoolSize, threadPoolSize, 60, TimeUnit.SECONDS, new
SynchronousQueue<>(),
- new CustomThreadFactory("grpcServerPool"), new
CustomRejectedExecutionHandler()
- );
+ final ExecutorService executor = VirtualThreads.createExecutor(
+ threadPoolName,
+ () -> {
+ if (threadPoolSize > 0) {
+ return new ThreadPoolExecutor(
+ threadPoolSize, threadPoolSize, 60, TimeUnit.SECONDS,
+ new SynchronousQueue<>(),
+ new CustomThreadFactory(threadPoolName),
+ new CustomRejectedExecutionHandler()
+ );
+ }
+ return null;
+ }
+ );
+ if (executor != null) {
nettyServerBuilder.executor(executor);
}
diff --git
a/oap-server/server-library/library-server/src/main/java/org/apache/skywalking/oap/server/library/server/http/HTTPServer.java
b/oap-server/server-library/library-server/src/main/java/org/apache/skywalking/oap/server/library/server/http/HTTPServer.java
index 0ab8b8925f..1a352818fa 100644
---
a/oap-server/server-library/library-server/src/main/java/org/apache/skywalking/oap/server/library/server/http/HTTPServer.java
+++
b/oap-server/server-library/library-server/src/main/java/org/apache/skywalking/oap/server/library/server/http/HTTPServer.java
@@ -35,10 +35,12 @@ import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.oap.server.library.server.Server;
import org.apache.skywalking.oap.server.library.server.ssl.PrivateKeyUtil;
+import org.apache.skywalking.oap.server.library.util.VirtualThreads;
import static java.util.Objects.requireNonNull;
@@ -48,11 +50,16 @@ public class HTTPServer implements Server {
protected ServerBuilder sb;
// Health check service, supports HEAD, GET method.
protected final Set<HttpMethod> allowedMethods =
Sets.newHashSet(HttpMethod.HEAD);
+ private String blockingTaskName = "http-blocking";
public HTTPServer(HTTPServerConfig config) {
this.config = config;
}
+ public void setBlockingTaskName(final String blockingTaskName) {
+ this.blockingTaskName = blockingTaskName;
+ }
+
@Override
public void initialize() {
sb = com.linecorp.armeria.server.Server
@@ -93,6 +100,14 @@ public class HTTPServer implements Server {
sb.absoluteUriTransformer(this::transformAbsoluteURI);
}
+ if (VirtualThreads.isSupported()) {
+ final ScheduledExecutorService blockingExecutor =
VirtualThreads.createScheduledExecutor(
+ blockingTaskName, () -> null);
+ if (blockingExecutor != null) {
+ sb.blockingTaskExecutor(blockingExecutor, true);
+ }
+ }
+
log.info("Server root context path: {}", config.getContextPath());
}
diff --git
a/oap-server/server-library/library-util/src/main/java/org/apache/skywalking/oap/server/library/util/VirtualThreadScheduledExecutor.java
b/oap-server/server-library/library-util/src/main/java/org/apache/skywalking/oap/server/library/util/VirtualThreadScheduledExecutor.java
new file mode 100644
index 0000000000..6392283cb9
--- /dev/null
+++
b/oap-server/server-library/library-util/src/main/java/org/apache/skywalking/oap/server/library/util/VirtualThreadScheduledExecutor.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.library.util;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * A {@link ScheduledExecutorService} fully backed by virtual threads.
+ *
+ * <p>All methods — including {@code schedule()}, {@code
scheduleAtFixedRate()},
+ * and {@code scheduleWithFixedDelay()} — delegate to virtual threads.
+ * Scheduling is implemented by sleeping in a virtual thread (which does not
+ * block OS threads), eliminating the need for a platform timer thread.
+ *
+ * <p>This adapter bridges the gap between virtual thread executors (which
return
+ * {@link ExecutorService}) and frameworks like Armeria that require a
+ * {@link ScheduledExecutorService} for their blocking task executor.
+ */
+@Slf4j
+final class VirtualThreadScheduledExecutor implements ScheduledExecutorService
{
+
+ private final ExecutorService vtExecutor;
+
+ VirtualThreadScheduledExecutor(final ExecutorService vtExecutor) {
+ this.vtExecutor = vtExecutor;
+ }
+
+ // --- Core execution: delegate to virtual threads ---
+
+ @Override
+ public void execute(final Runnable command) {
+ vtExecutor.execute(command);
+ }
+
+ @Override
+ public Future<?> submit(final Runnable task) {
+ return vtExecutor.submit(task);
+ }
+
+ @Override
+ public <T> Future<T> submit(final Runnable task, final T result) {
+ return vtExecutor.submit(task, result);
+ }
+
+ @Override
+ public <T> Future<T> submit(final Callable<T> task) {
+ return vtExecutor.submit(task);
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(final Collection<? extends
Callable<T>> tasks)
+ throws InterruptedException {
+ return vtExecutor.invokeAll(tasks);
+ }
+
+ @Override
+ public <T> List<Future<T>> invokeAll(final Collection<? extends
Callable<T>> tasks,
+ final long timeout, final TimeUnit
unit)
+ throws InterruptedException {
+ return vtExecutor.invokeAll(tasks, timeout, unit);
+ }
+
+ @Override
+ public <T> T invokeAny(final Collection<? extends Callable<T>> tasks)
+ throws InterruptedException, ExecutionException {
+ return vtExecutor.invokeAny(tasks);
+ }
+
+ @Override
+ public <T> T invokeAny(final Collection<? extends Callable<T>> tasks,
+ final long timeout, final TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return vtExecutor.invokeAny(tasks, timeout, unit);
+ }
+
+ // --- Scheduling: sleep in virtual thread, then execute ---
+
+ @Override
+ public ScheduledFuture<?> schedule(final Runnable command, final long
delay, final TimeUnit unit) {
+ final long triggerNanos = System.nanoTime() + unit.toNanos(delay);
+ final VirtualScheduledFuture<Void> sf = new
VirtualScheduledFuture<>(triggerNanos);
+ sf.setFuture(vtExecutor.submit(() -> {
+ sleepUntil(triggerNanos);
+ command.run();
+ return null;
+ }));
+ return sf;
+ }
+
+ @Override
+ public <V> ScheduledFuture<V> schedule(final Callable<V> callable, final
long delay,
+ final TimeUnit unit) {
+ final long triggerNanos = System.nanoTime() + unit.toNanos(delay);
+ final VirtualScheduledFuture<V> sf = new
VirtualScheduledFuture<>(triggerNanos);
+ sf.setFuture(vtExecutor.submit(() -> {
+ sleepUntil(triggerNanos);
+ return callable.call();
+ }));
+ return sf;
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleAtFixedRate(final Runnable command,
final long initialDelay,
+ final long period, final
TimeUnit unit) {
+ final long periodNanos = unit.toNanos(period);
+ final long firstTrigger = System.nanoTime() +
unit.toNanos(initialDelay);
+ final VirtualScheduledFuture<Void> sf = new
VirtualScheduledFuture<>(firstTrigger);
+ sf.setFuture(vtExecutor.submit(() -> {
+ long nextTrigger = firstTrigger;
+ sleepUntil(nextTrigger);
+ while (!Thread.currentThread().isInterrupted()) {
+ command.run();
+ nextTrigger += periodNanos;
+ sf.updateTriggerNanos(nextTrigger);
+ sleepUntil(nextTrigger);
+ }
+ return null;
+ }));
+ return sf;
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleWithFixedDelay(final Runnable command,
final long initialDelay,
+ final long delay, final
TimeUnit unit) {
+ final long delayNanos = unit.toNanos(delay);
+ final long firstTrigger = System.nanoTime() +
unit.toNanos(initialDelay);
+ final VirtualScheduledFuture<Void> sf = new
VirtualScheduledFuture<>(firstTrigger);
+ sf.setFuture(vtExecutor.submit(() -> {
+ sleepUntil(firstTrigger);
+ while (!Thread.currentThread().isInterrupted()) {
+ command.run();
+ final long nextTrigger = System.nanoTime() + delayNanos;
+ sf.updateTriggerNanos(nextTrigger);
+ sleepUntil(nextTrigger);
+ }
+ return null;
+ }));
+ return sf;
+ }
+
+ private static void sleepUntil(final long triggerNanos) throws
InterruptedException {
+ long remaining = triggerNanos - System.nanoTime();
+ while (remaining > 0) {
+ TimeUnit.NANOSECONDS.sleep(remaining);
+ remaining = triggerNanos - System.nanoTime();
+ }
+ }
+
+ // --- Lifecycle ---
+
+ @Override
+ public void shutdown() {
+ vtExecutor.shutdown();
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ return vtExecutor.shutdownNow();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return vtExecutor.isShutdown();
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return vtExecutor.isTerminated();
+ }
+
+ @Override
+ public boolean awaitTermination(final long timeout, final TimeUnit unit)
throws InterruptedException {
+ return vtExecutor.awaitTermination(timeout, unit);
+ }
+
+ /**
+ * A {@link ScheduledFuture} backed by a virtual thread {@link Future}.
+ */
+ static final class VirtualScheduledFuture<V> implements ScheduledFuture<V>
{
+ private volatile Future<V> delegate;
+ private volatile long triggerNanos;
+
+ VirtualScheduledFuture(final long triggerNanos) {
+ this.triggerNanos = triggerNanos;
+ }
+
+ void setFuture(final Future<V> delegate) {
+ this.delegate = delegate;
+ }
+
+ void updateTriggerNanos(final long triggerNanos) {
+ this.triggerNanos = triggerNanos;
+ }
+
+ @Override
+ public long getDelay(final TimeUnit unit) {
+ return unit.convert(triggerNanos - System.nanoTime(),
TimeUnit.NANOSECONDS);
+ }
+
+ @Override
+ public int compareTo(final Delayed other) {
+ if (other == this) {
+ return 0;
+ }
+ return Long.compare(getDelay(TimeUnit.NANOSECONDS),
other.getDelay(TimeUnit.NANOSECONDS));
+ }
+
+ @Override
+ public boolean cancel(final boolean mayInterruptIfRunning) {
+ return delegate.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return delegate.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return delegate.isDone();
+ }
+
+ @Override
+ public V get() throws InterruptedException, ExecutionException {
+ return delegate.get();
+ }
+
+ @Override
+ public V get(final long timeout, final TimeUnit unit)
+ throws InterruptedException, ExecutionException,
TimeoutException {
+ return delegate.get(timeout, unit);
+ }
+ }
+}
diff --git
a/oap-server/server-library/library-util/src/main/java/org/apache/skywalking/oap/server/library/util/VirtualThreads.java
b/oap-server/server-library/library-util/src/main/java/org/apache/skywalking/oap/server/library/util/VirtualThreads.java
new file mode 100644
index 0000000000..4f558157a2
--- /dev/null
+++
b/oap-server/server-library/library-util/src/main/java/org/apache/skywalking/oap/server/library/util/VirtualThreads.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.library.util;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.function.Supplier;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Unified executor factory for both virtual threads (JDK 25+) and platform
threads.
+ *
+ * <p>Virtual threads (JEP 444) are available since JDK 21, but JDK 21-23 has
a critical
+ * thread pinning bug where {@code synchronized} blocks prevent virtual
threads from
+ * unmounting from carrier threads (see JEP 491). This was fixed in JDK 24,
but JDK 24
+ * is non-LTS. JDK 25 LTS is the first long-term support release with the fix.
+ *
+ * <p>This utility requires <b>JDK 25+</b> to enable virtual threads, ensuring
both
+ * the pinning fix and LTS support are present. All created threads (virtual
or platform)
+ * are named with the provided prefix for monitoring and debugging.
+ */
+@Slf4j
+public final class VirtualThreads {
+
+ /**
+ * The minimum JDK version required for virtual thread support.
+ * JDK 25 is the first LTS with the synchronized pinning fix (JEP 491).
+ */
+ static final int MINIMUM_JDK_VERSION = 25;
+
+ private static final boolean SUPPORTED;
+
+ /*
+ * Cached reflection handles for JDK 25+ virtual thread APIs:
+ * Thread.ofVirtual() ->
Thread.Builder.OfVirtual
+ * Thread.Builder#name(String prefix, long start) -> Thread.Builder
+ * Thread.Builder#factory() -> ThreadFactory
+ * Executors.newThreadPerTaskExecutor(ThreadFactory) -> ExecutorService
+ */
+ private static final Method OF_VIRTUAL;
+ private static final Method BUILDER_NAME;
+ private static final Method BUILDER_FACTORY;
+ private static final Method NEW_THREAD_PER_TASK_EXECUTOR;
+
+ /**
+ * System environment variable to disable virtual threads on JDK 25+.
+ * Set {@code SW_VIRTUAL_THREADS_ENABLED=false} to force platform threads.
+ */
+ static final String ENV_VIRTUAL_THREADS_ENABLED =
"SW_VIRTUAL_THREADS_ENABLED";
+
+ static {
+ final int jdkVersion = Runtime.version().feature();
+ boolean supported = false;
+ Method ofVirtual = null;
+ Method builderName = null;
+ Method builderFactory = null;
+ Method newThreadPerTaskExecutor = null;
+
+ final String envValue = System.getenv(ENV_VIRTUAL_THREADS_ENABLED);
+ final boolean disabledByEnv = "false".equalsIgnoreCase(envValue);
+
+ if (disabledByEnv) {
+ log.info("Virtual threads disabled by environment variable {}={}",
+ ENV_VIRTUAL_THREADS_ENABLED, envValue);
+ } else if (jdkVersion >= MINIMUM_JDK_VERSION) {
+ try {
+ ofVirtual = Thread.class.getMethod("ofVirtual");
+ final Class<?> builderClass =
Class.forName("java.lang.Thread$Builder");
+ builderName = builderClass.getMethod("name", String.class,
long.class);
+ builderFactory = builderClass.getMethod("factory");
+ newThreadPerTaskExecutor = Executors.class.getMethod(
+ "newThreadPerTaskExecutor", ThreadFactory.class);
+ supported = true;
+ log.info("Virtual threads available (JDK {})", jdkVersion);
+ } catch (final ReflectiveOperationException e) {
+ log.warn("JDK {} meets version requirement but virtual thread
API "
+ + "not found, virtual threads disabled",
jdkVersion, e);
+ }
+ } else {
+ log.info("Virtual threads require JDK {}+, current JDK is {}",
+ MINIMUM_JDK_VERSION, jdkVersion);
+ }
+
+ SUPPORTED = supported;
+ OF_VIRTUAL = ofVirtual;
+ BUILDER_NAME = builderName;
+ BUILDER_FACTORY = builderFactory;
+ NEW_THREAD_PER_TASK_EXECUTOR = newThreadPerTaskExecutor;
+ }
+
+ private VirtualThreads() {
+ }
+
+ /**
+ * @return true if the current JDK version is 25+ and virtual thread API
is available.
+ */
+ public static boolean isSupported() {
+ return SUPPORTED;
+ }
+
+ /**
+ * Create a named executor service with virtual threads enabled by default.
+ * On JDK 25+, creates a virtual-thread-per-task executor with threads
named
+ * {@code {namePrefix}-0}, {@code {namePrefix}-1}, etc.
+ * On older JDKs, delegates to the provided {@code
platformExecutorSupplier}.
+ *
+ * @param namePrefix prefix for virtual thread names
+ * @param platformExecutorSupplier supplies the platform-thread executor
as fallback
+ * @return virtual thread executor on JDK 25+, or the supplier's executor
otherwise
+ */
+ public static ExecutorService createExecutor(final String namePrefix,
+ final
Supplier<ExecutorService> platformExecutorSupplier) {
+ return createExecutor(namePrefix, true, platformExecutorSupplier);
+ }
+
+ /**
+ * Create a named executor service. When {@code enableVirtualThreads} is
true and JDK 25+,
+ * creates a virtual-thread-per-task executor with threads named
+ * {@code {namePrefix}-0}, {@code {namePrefix}-1}, etc.
+ * Otherwise, delegates to the provided {@code platformExecutorSupplier}.
+ *
+ * @param namePrefix prefix for virtual thread names
+ * @param enableVirtualThreads whether to use virtual threads
(requires JDK 25+)
+ * @param platformExecutorSupplier supplies the platform-thread executor
as fallback
+ * @return virtual thread executor or the supplier's executor
+ */
+ public static ExecutorService createExecutor(final String namePrefix,
+ final boolean
enableVirtualThreads,
+ final
Supplier<ExecutorService> platformExecutorSupplier) {
+ if (enableVirtualThreads && SUPPORTED) {
+ try {
+ return createVirtualThreadExecutor(namePrefix);
+ } catch (final ReflectiveOperationException e) {
+ log.warn("Failed to create virtual thread executor [{}], "
+ + "falling back to platform threads", namePrefix,
e);
+ }
+ }
+ return platformExecutorSupplier.get();
+ }
+
+ /**
+ * Create a named scheduled executor service with virtual threads enabled
by default.
+ * On JDK 25+, creates a virtual-thread-backed {@link
ScheduledExecutorService}.
+ * On older JDKs, delegates to the provided {@code
platformExecutorSupplier}.
+ *
+ * <p>This is designed for frameworks (e.g. Armeria) that require a
+ * {@link ScheduledExecutorService} for their blocking task executor.
+ * All methods — including scheduling — are fully backed by virtual
threads.
+ * Scheduling is implemented by sleeping in a virtual thread.
+ *
+ * @param namePrefix prefix for virtual thread names
+ * @param platformExecutorSupplier supplies the platform-thread executor
as fallback
+ * @return virtual thread scheduled executor on JDK 25+, or the supplier's
executor otherwise
+ */
+ public static ScheduledExecutorService createScheduledExecutor(
+ final String namePrefix,
+ final Supplier<ScheduledExecutorService> platformExecutorSupplier)
{
+ if (SUPPORTED) {
+ try {
+ final ExecutorService vtExecutor =
createVirtualThreadExecutor(namePrefix);
+ return new VirtualThreadScheduledExecutor(vtExecutor);
+ } catch (final ReflectiveOperationException e) {
+ log.warn("Failed to create virtual thread scheduled executor
[{}], "
+ + "falling back to platform threads", namePrefix,
e);
+ }
+ }
+ return platformExecutorSupplier.get();
+ }
+
+ private static ExecutorService createVirtualThreadExecutor(
+ final String namePrefix) throws ReflectiveOperationException {
+ // Thread.ofVirtual().name("vt:" + namePrefix + "-", 0).factory()
+ final Object builder = OF_VIRTUAL.invoke(null);
+ final Object namedBuilder = BUILDER_NAME.invoke(builder, "vt:" +
namePrefix + "-", 0L);
+ final ThreadFactory factory = (ThreadFactory)
BUILDER_FACTORY.invoke(namedBuilder);
+ // Executors.newThreadPerTaskExecutor(factory)
+ final ExecutorService executor =
+ (ExecutorService) NEW_THREAD_PER_TASK_EXECUTOR.invoke(null,
factory);
+ log.info("Created virtual-thread-per-task executor [{}]", namePrefix);
+ return executor;
+ }
+}
diff --git
a/oap-server/server-library/library-util/src/test/java/org/apache/skywalking/oap/server/library/util/VirtualThreadsTest.java
b/oap-server/server-library/library-util/src/test/java/org/apache/skywalking/oap/server/library/util/VirtualThreadsTest.java
new file mode 100644
index 0000000000..325b621fb9
--- /dev/null
+++
b/oap-server/server-library/library-util/src/test/java/org/apache/skywalking/oap/server/library/util/VirtualThreadsTest.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.library.util;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class VirtualThreadsTest {
+
+ @Test
+ public void testIsSupportedMatchesJdkVersion() {
+ final int jdkVersion = Runtime.version().feature();
+ final boolean expected = jdkVersion >=
VirtualThreads.MINIMUM_JDK_VERSION;
+ assertEquals(expected, VirtualThreads.isSupported());
+ }
+
+ @Test
+ public void testVirtualThreadExecutor() throws Exception {
+ if (!VirtualThreads.isSupported()) {
+ return;
+ }
+ final ExecutorService executor = VirtualThreads.createExecutor(
+ "vt-check", true, () -> Executors.newSingleThreadExecutor());
+ try {
+ final ThreadCapture capture = submitAndCapture(executor);
+ assertTrue(capture.name.startsWith("vt:vt-check-"),
+ "Virtual thread name should start with 'vt:vt-check-',
but was: " + capture.name);
+ assertTrue(isVirtual(capture.thread),
+ "Thread should be virtual on JDK 25+");
+ } finally {
+ executor.shutdown();
+ }
+ }
+
+ @Test
+ public void testForcePlatformFallback() throws Exception {
+ if (!VirtualThreads.isSupported()) {
+ return;
+ }
+ final AtomicLong counter = new AtomicLong(0);
+ final ExecutorService executor = VirtualThreads.createExecutor(
+ "pt-check", false, () -> new ThreadPoolExecutor(
+ 2, 2, 60, TimeUnit.SECONDS, new SynchronousQueue<>(),
+ r -> new Thread(r, "pt-check-" + counter.getAndIncrement())
+ ));
+ try {
+ final ThreadCapture capture = submitAndCapture(executor);
+ assertTrue(capture.name.startsWith("pt-check-"),
+ "Platform thread name should start with 'pt-check-',
but was: " + capture.name);
+ assertFalse(isVirtual(capture.thread),
+ "Thread should NOT be virtual when
enableVirtualThreads=false");
+ } finally {
+ executor.shutdown();
+ }
+ }
+
+ @Test
+ public void testScheduledExecutorUsesVirtualThreads() throws Exception {
+ if (!VirtualThreads.isSupported()) {
+ return;
+ }
+ final ScheduledExecutorService executor =
VirtualThreads.createScheduledExecutor(
+ "sched-vt", () -> null);
+ assertNotNull(executor);
+ try {
+ // Test execute() runs on virtual threads
+ final ThreadCapture capture = submitAndCapture(executor);
+ assertTrue(capture.name.startsWith("vt:sched-vt-"),
+ "Scheduled executor virtual thread name should start
with 'vt:sched-vt-', but was: "
+ + capture.name);
+ assertTrue(isVirtual(capture.thread),
+ "Scheduled executor should use virtual threads on JDK
25+");
+
+ // Test schedule() also dispatches to virtual threads
+ final AtomicReference<Thread> scheduledRef = new
AtomicReference<>();
+ final CountDownLatch scheduledLatch = new CountDownLatch(1);
+ final ScheduledFuture<?> future = executor.schedule(() -> {
+ scheduledRef.set(Thread.currentThread());
+ scheduledLatch.countDown();
+ }, 10, TimeUnit.MILLISECONDS);
+ assertTrue(scheduledLatch.await(5, TimeUnit.SECONDS), "Scheduled
task did not complete");
+ assertTrue(isVirtual(scheduledRef.get()),
+ "Scheduled task should run on a virtual thread");
+ } finally {
+ executor.shutdown();
+ }
+ }
+
+ @Test
+ public void testFallbackUsedWhenNotSupported() {
+ if (VirtualThreads.isSupported()) {
+ return;
+ }
+ final ExecutorService fallback = Executors.newSingleThreadExecutor();
+ try {
+ final ExecutorService result =
VirtualThreads.createExecutor("test", () -> fallback);
+ assertSame(fallback, result);
+ } finally {
+ fallback.shutdown();
+ }
+ }
+
+ private ThreadCapture submitAndCapture(final ExecutorService executor)
throws InterruptedException {
+ final AtomicReference<Thread> threadRef = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ executor.submit(() -> {
+ threadRef.set(Thread.currentThread());
+ latch.countDown();
+ });
+ assertTrue(latch.await(5, TimeUnit.SECONDS), "Task did not complete in
time");
+ final Thread thread = threadRef.get();
+ assertNotNull(thread);
+ return new ThreadCapture(thread, thread.getName());
+ }
+
+ /**
+ * Check Thread.isVirtual() via reflection (JDK 21+ API, compiled against
JDK 11).
+ */
+ private static boolean isVirtual(final Thread thread) throws Exception {
+ final Method isVirtual = Thread.class.getMethod("isVirtual");
+ return (boolean) isVirtual.invoke(thread);
+ }
+
+ private static class ThreadCapture {
+ final Thread thread;
+ final String name;
+
+ ThreadCapture(final Thread thread, final String name) {
+ this.thread = thread;
+ this.name = name;
+ }
+ }
+}
diff --git
a/oap-server/server-query-plugin/logql-plugin/src/main/java/org/apache/skywalking/oap/query/logql/LogQLProvider.java
b/oap-server/server-query-plugin/logql-plugin/src/main/java/org/apache/skywalking/oap/query/logql/LogQLProvider.java
index b23cd7d84e..7299b35149 100644
---
a/oap-server/server-query-plugin/logql-plugin/src/main/java/org/apache/skywalking/oap/query/logql/LogQLProvider.java
+++
b/oap-server/server-query-plugin/logql-plugin/src/main/java/org/apache/skywalking/oap/query/logql/LogQLProvider.java
@@ -76,6 +76,7 @@ public class LogQLProvider extends ModuleProvider {
.build();
httpServer = new HTTPServer(httpServerConfig);
+ httpServer.setBlockingTaskName("logql-http");
httpServer.initialize();
httpServer.addHandler(
new LogQLApiHandler(getManager()),
diff --git
a/oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/PromQLProvider.java
b/oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/PromQLProvider.java
index a41ce71a89..62c9fc0cb6 100644
---
a/oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/PromQLProvider.java
+++
b/oap-server/server-query-plugin/promql-plugin/src/main/java/org/apache/skywalking/oap/query/promql/PromQLProvider.java
@@ -77,6 +77,7 @@ public class PromQLProvider extends ModuleProvider {
.build();
httpServer = new HTTPServer(httpServerConfig);
+ httpServer.setBlockingTaskName("promql-http");
httpServer.initialize();
httpServer.addHandler(
new PromQLApiHandler(getManager(), config),
diff --git
a/oap-server/server-query-plugin/zipkin-query-plugin/src/main/java/org/apache/skywalking/oap/query/zipkin/ZipkinQueryProvider.java
b/oap-server/server-query-plugin/zipkin-query-plugin/src/main/java/org/apache/skywalking/oap/query/zipkin/ZipkinQueryProvider.java
index d5ab3a6be1..aa5ff0141e 100644
---
a/oap-server/server-query-plugin/zipkin-query-plugin/src/main/java/org/apache/skywalking/oap/query/zipkin/ZipkinQueryProvider.java
+++
b/oap-server/server-query-plugin/zipkin-query-plugin/src/main/java/org/apache/skywalking/oap/query/zipkin/ZipkinQueryProvider.java
@@ -77,6 +77,7 @@ public class ZipkinQueryProvider extends ModuleProvider {
.build();
httpServer = new HTTPServer(httpServerConfig);
+ httpServer.setBlockingTaskName("zipkin-query-http");
httpServer.initialize();
httpServer.addHandler(
new ZipkinQueryHandler(config, getManager()),
diff --git
a/oap-server/server-receiver-plugin/aws-firehose-receiver/src/main/java/org/apache/skywalking/oap/server/receiver/aws/firehose/AWSFirehoseReceiverModuleProvider.java
b/oap-server/server-receiver-plugin/aws-firehose-receiver/src/main/java/org/apache/skywalking/oap/server/receiver/aws/firehose/AWSFirehoseReceiverModuleProvider.java
index 3ab6608e20..41d8ea15ec 100644
---
a/oap-server/server-receiver-plugin/aws-firehose-receiver/src/main/java/org/apache/skywalking/oap/server/receiver/aws/firehose/AWSFirehoseReceiverModuleProvider.java
+++
b/oap-server/server-receiver-plugin/aws-firehose-receiver/src/main/java/org/apache/skywalking/oap/server/receiver/aws/firehose/AWSFirehoseReceiverModuleProvider.java
@@ -81,6 +81,7 @@ public class AWSFirehoseReceiverModuleProvider extends
ModuleProvider {
.tlsCertChainPath(moduleConfig.getTlsCertChainPath())
.build();
httpServer = new HTTPServer(httpServerConfig);
+ httpServer.setBlockingTaskName("firehose-http");
httpServer.initialize();
}
diff --git
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/EnvoyMetricReceiverProvider.java
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/EnvoyMetricReceiverProvider.java
index ac4bcd5c1e..4f7b54928f 100644
---
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/EnvoyMetricReceiverProvider.java
+++
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/EnvoyMetricReceiverProvider.java
@@ -102,6 +102,7 @@ public class EnvoyMetricReceiverProvider extends
ModuleProvider {
if (config.getGRPCThreadPoolSize() > 0) {
grpcServer.setThreadPoolSize(config.getGRPCThreadPoolSize());
}
+ grpcServer.setThreadPoolName("als-grpc");
grpcServer.initialize();
this.receiverGRPCHandlerRegister = new
GRPCHandlerRegisterImpl(grpcServer);
diff --git
a/oap-server/server-receiver-plugin/skywalking-ebpf-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/ebpf/provider/EBPFReceiverProvider.java
b/oap-server/server-receiver-plugin/skywalking-ebpf-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/ebpf/provider/EBPFReceiverProvider.java
index 6041ac865b..f48ee9928f 100644
---
a/oap-server/server-receiver-plugin/skywalking-ebpf-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/ebpf/provider/EBPFReceiverProvider.java
+++
b/oap-server/server-receiver-plugin/skywalking-ebpf-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/ebpf/provider/EBPFReceiverProvider.java
@@ -107,6 +107,7 @@ public class EBPFReceiverProvider extends ModuleProvider {
if (config.getGRPCThreadPoolSize() > 0) {
grpcServer.setThreadPoolSize(config.getGRPCThreadPoolSize());
}
+ grpcServer.setThreadPoolName("ebpf-grpc");
grpcServer.initialize();
this.receiverGRPCHandlerRegister = new
GRPCHandlerRegisterImpl(grpcServer);
diff --git
a/oap-server/server-receiver-plugin/skywalking-sharing-server-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/sharing/server/SharingServerModuleProvider.java
b/oap-server/server-receiver-plugin/skywalking-sharing-server-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/sharing/server/SharingServerModuleProvider.java
index 933e095207..7a67823372 100644
---
a/oap-server/server-receiver-plugin/skywalking-sharing-server-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/sharing/server/SharingServerModuleProvider.java
+++
b/oap-server/server-receiver-plugin/skywalking-sharing-server-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/sharing/server/SharingServerModuleProvider.java
@@ -90,6 +90,7 @@ public class SharingServerModuleProvider extends
ModuleProvider {
setBootingParameter("oap.external.http.port",
config.getRestPort());
httpServer = new HTTPServer(httpServerConfig);
+ httpServer.setBlockingTaskName("receiver-http");
httpServer.initialize();
this.registerServiceImplementation(HTTPHandlerRegister.class, new
HTTPHandlerRegisterImpl(httpServer));
@@ -128,6 +129,7 @@ public class SharingServerModuleProvider extends
ModuleProvider {
if (config.getGRPCThreadPoolSize() > 0) {
grpcServer.setThreadPoolSize(config.getGRPCThreadPoolSize());
}
+ grpcServer.setThreadPoolName("receiver-grpc");
grpcServer.initialize();
GRPCHandlerRegisterImpl grpcHandlerRegister = new
GRPCHandlerRegisterImpl(grpcServer);
diff --git
a/oap-server/server-receiver-plugin/zipkin-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/zipkin/ZipkinReceiverProvider.java
b/oap-server/server-receiver-plugin/zipkin-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/zipkin/ZipkinReceiverProvider.java
index 441f9faaec..3878a6d7c0 100644
---
a/oap-server/server-receiver-plugin/zipkin-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/zipkin/ZipkinReceiverProvider.java
+++
b/oap-server/server-receiver-plugin/zipkin-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/zipkin/ZipkinReceiverProvider.java
@@ -88,6 +88,7 @@ public class ZipkinReceiverProvider extends ModuleProvider {
.build();
httpServer = new HTTPServer(httpServerConfig);
+ httpServer.setBlockingTaskName("zipkin-http");
httpServer.initialize();
httpServer.addHandler(