This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-clients.git
The following commit(s) were added to refs/heads/master by this push:
new 9abb7f39 [ISSUE #1318] [Java] Support opt-in virtual threads (#1319)
9abb7f39 is described below
commit 9abb7f39ce9f74b41873a4f5253ab82af052d7a7
Author: qianye <[email protected]>
AuthorDate: Thu Aug 6 20:57:28 2026 +0800
[ISSUE #1318] [Java] Support opt-in virtual threads (#1319)
---
.github/workflows/java_build.yml | 7 +-
.../rocketmq/client/apis/ClientConfiguration.java | 11 +-
.../client/apis/ClientConfigurationBuilder.java | 22 +++-
.../apache/rocketmq/client/java/impl/Client.java | 9 ++
.../rocketmq/client/java/impl/ClientImpl.java | 37 +++---
.../client/java/impl/ClientManagerImpl.java | 15 +--
.../client/java/impl/consumer/ConsumeService.java | 6 +-
.../java/impl/consumer/FifoConsumeService.java | 4 +-
.../java/impl/consumer/LiteFifoConsumeService.java | 4 +-
.../impl/consumer/LiteStandardConsumeService.java | 4 +-
.../java/impl/consumer/PushConsumerImpl.java | 20 +--
.../java/impl/consumer/StandardConsumeService.java | 4 +-
.../client/java/misc/ExecutorServices.java | 135 +++++++++++++++++++++
.../client/apis/ClientConfigurationTest.java} | 28 +++--
.../client/java/misc/ExecutorServicesTest.java | 99 +++++++++++++++
15 files changed, 351 insertions(+), 54 deletions(-)
diff --git a/.github/workflows/java_build.yml b/.github/workflows/java_build.yml
index 7a77aa75..21851d4c 100644
--- a/.github/workflows/java_build.yml
+++ b/.github/workflows/java_build.yml
@@ -9,7 +9,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-22.04, macos-latest, windows-2022 ]
- jdk: [11, 17]
+ jdk: ["11", "17", "21"]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -22,8 +22,13 @@ jobs:
distribution: "adopt"
cache: maven
- name: Build with Maven
+ if: matrix.jdk != '21'
working-directory: ./java
run: mvn -B package --file pom.xml
+ - name: Test virtual threads with JDK 21
+ if: matrix.jdk == '21'
+ working-directory: ./java
+ run: mvn -B -pl client -am '-Dspotbugs.skip=true'
'-Dtest=ClientConfigurationTest,ExecutorServicesTest' '-DfailIfNoTests=false'
'-Dsurefire.failIfNoSpecifiedTests=false' test
opentelemetry-instrumentation-compatibility:
runs-on: ubuntu-latest
diff --git
a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/ClientConfiguration.java
b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/ClientConfiguration.java
index fa8c45ea..15cec038 100644
---
a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/ClientConfiguration.java
+++
b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/ClientConfiguration.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.client.apis;
+import com.google.common.annotations.Beta;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -31,6 +32,7 @@ public class ClientConfiguration {
private final SessionCredentialsProvider sessionCredentialsProvider;
private final Duration requestTimeout;
private final boolean sslEnabled;
+ private final boolean virtualThreadsEnabled;
private final String namespace;
private final int maxStartupAttempts;
private final Map<String, String> clientProperties;
@@ -40,12 +42,14 @@ public class ClientConfiguration {
* logging warnings already, so we avoid repeating args check here.
*/
ClientConfiguration(String endpoints, SessionCredentialsProvider
sessionCredentialsProvider,
- Duration requestTimeout, boolean sslEnabled, String namespace, int
maxStartupAttempts,
+ Duration requestTimeout, boolean sslEnabled, boolean
virtualThreadsEnabled, String namespace,
+ int maxStartupAttempts,
Map<String, String> clientProperties) {
this.endpoints = endpoints;
this.sessionCredentialsProvider = sessionCredentialsProvider;
this.requestTimeout = requestTimeout;
this.sslEnabled = sslEnabled;
+ this.virtualThreadsEnabled = virtualThreadsEnabled;
this.namespace = namespace;
this.maxStartupAttempts = maxStartupAttempts;
this.clientProperties = Collections.unmodifiableMap(new
LinkedHashMap<>(clientProperties));
@@ -71,6 +75,11 @@ public class ClientConfiguration {
return sslEnabled;
}
+ @Beta
+ public boolean isVirtualThreadsEnabled() {
+ return virtualThreadsEnabled;
+ }
+
public String getNamespace() {
return namespace;
}
diff --git
a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/ClientConfigurationBuilder.java
b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/ClientConfigurationBuilder.java
index 4688e500..0a23b0df 100644
---
a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/ClientConfigurationBuilder.java
+++
b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/ClientConfigurationBuilder.java
@@ -20,6 +20,7 @@ package org.apache.rocketmq.client.apis;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.common.annotations.Beta;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.LinkedHashMap;
@@ -43,6 +44,7 @@ public class ClientConfigurationBuilder {
private SessionCredentialsProvider sessionCredentialsProvider = null;
private Duration requestTimeout = Duration.ofSeconds(3);
private boolean sslEnabled = true;
+ private boolean virtualThreadsEnabled = false;
private String namespace = "";
private int maxStartupAttempts = 3;
private final Map<String, String> clientProperties = new LinkedHashMap<>();
@@ -97,6 +99,22 @@ public class ClientConfigurationBuilder {
return this;
}
+ /**
+ * Enable or disable virtual threads for client task execution.
+ *
+ * <p>Virtual threads are disabled by default. When enabled on JDK 21 or
later, the client uses virtual threads for
+ * asynchronous RPCs, callbacks, telemetry commands and message
consumption. On earlier JDK versions, the client
+ * falls back to its platform-thread executors.
+ *
+ * @param virtualThreadsEnabled whether virtual threads should be enabled.
+ * @return The {@link ClientConfigurationBuilder} instance, to allow for
method chaining.
+ */
+ @Beta
+ public ClientConfigurationBuilder enableVirtualThreads(boolean
virtualThreadsEnabled) {
+ this.virtualThreadsEnabled = virtualThreadsEnabled;
+ return this;
+ }
+
/**
* Configure namespace for client
* @param namespace namespace
@@ -183,8 +201,8 @@ public class ClientConfigurationBuilder {
checkNotNull(requestTimeout, "requestTimeout should not be null");
// Keep build() defensive for maps supplied through
setClientProperties or future builder paths.
validateClientProperties(clientProperties);
- return new ClientConfiguration(endpoints, sessionCredentialsProvider,
requestTimeout, sslEnabled, namespace,
- maxStartupAttempts, clientProperties);
+ return new ClientConfiguration(endpoints, sessionCredentialsProvider,
requestTimeout, sslEnabled,
+ virtualThreadsEnabled, namespace, maxStartupAttempts,
clientProperties);
}
private static void validateClientProperties(Map<String, String>
properties) {
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/Client.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/Client.java
index 83510113..deb5d40b 100644
--- a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/Client.java
+++ b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/Client.java
@@ -55,6 +55,15 @@ public interface Client {
*/
boolean isSslEnabled();
+ /**
+ * Check whether virtual threads are enabled for client task execution.
+ *
+ * @return a boolean value indicating whether virtual threads are enabled
or not.
+ */
+ default boolean isVirtualThreadsEnabled() {
+ return false;
+ }
+
/**
* Reconnect telemetry to the specified endpoints.
*
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientImpl.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientImpl.java
index b523af73..e487fcc0 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientImpl.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientImpl.java
@@ -114,7 +114,7 @@ public abstract class ClientImpl extends
AbstractIdleService implements Client,
/**
* Telemetry command executor, which aims to execute commands from the
remote.
*/
- protected final ThreadPoolExecutor telemetryCommandExecutor;
+ protected final ExecutorService telemetryCommandExecutor;
protected final ClientId clientId;
private final ClientManager clientManager;
@@ -151,13 +151,14 @@ public abstract class ClientImpl extends
AbstractIdleService implements Client,
this.clientManager = new ClientManagerImpl(this);
final long clientIdIndex = clientId.getIndex();
- this.clientCallbackExecutor = new ThreadPoolExecutor(
- Runtime.getRuntime().availableProcessors(),
- Runtime.getRuntime().availableProcessors(),
- 60,
- TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(),
- new ThreadFactoryImpl("ClientCallbackWorker", clientIdIndex));
+ this.clientCallbackExecutor = ExecutorServices.newExecutorService(
+ clientConfiguration.isVirtualThreadsEnabled(), () -> new
ThreadPoolExecutor(
+ Runtime.getRuntime().availableProcessors(),
+ Runtime.getRuntime().availableProcessors(),
+ 60,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(),
+ new ThreadFactoryImpl("ClientCallbackWorker", clientIdIndex)));
this.clientMeterManager = new ClientMeterManager(clientId,
clientConfiguration);
@@ -165,13 +166,14 @@ public abstract class ClientImpl extends
AbstractIdleService implements Client,
new CompositedMessageInterceptor(Collections.singletonList(new
MessageMeterInterceptor(this,
clientMeterManager)));
- this.telemetryCommandExecutor = new ThreadPoolExecutor(
- 1,
- 1,
- 60,
- TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(),
- new ThreadFactoryImpl("CommandExecutor", clientIdIndex));
+ this.telemetryCommandExecutor = ExecutorServices.newExecutorService(
+ clientConfiguration.isVirtualThreadsEnabled(), () -> new
ThreadPoolExecutor(
+ 1,
+ 1,
+ 60,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(),
+ new ThreadFactoryImpl("CommandExecutor", clientIdIndex)));
}
/**
@@ -588,6 +590,11 @@ public abstract class ClientImpl extends
AbstractIdleService implements Client,
return clientConfiguration.isSslEnabled();
}
+ @Override
+ public boolean isVirtualThreadsEnabled() {
+ return clientConfiguration.isVirtualThreadsEnabled();
+ }
+
/**
* Send heartbeat data to the appointed endpoint
*
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManagerImpl.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManagerImpl.java
index c7b3e1bb..d26798a8 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManagerImpl.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManagerImpl.java
@@ -137,13 +137,14 @@ public class ClientManagerImpl extends ClientManager {
Runtime.getRuntime().availableProcessors(),
new ThreadFactoryImpl("ClientScheduler", clientIndex));
- this.asyncWorker = new ThreadPoolExecutor(
- Runtime.getRuntime().availableProcessors(),
- Runtime.getRuntime().availableProcessors(),
- 60,
- TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(50000),
- new ThreadFactoryImpl("ClientAsyncWorker", clientIndex));
+ this.asyncWorker =
ExecutorServices.newExecutorService(client.isVirtualThreadsEnabled(),
+ () -> new ThreadPoolExecutor(
+ Runtime.getRuntime().availableProcessors(),
+ Runtime.getRuntime().availableProcessors(),
+ 60,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(50000),
+ new ThreadFactoryImpl("ClientAsyncWorker", clientIndex)));
}
/**
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/ConsumeService.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/ConsumeService.java
index 11075c1e..4bd2745e 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/ConsumeService.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/ConsumeService.java
@@ -25,8 +25,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.time.Duration;
import java.util.List;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.MessageListener;
@@ -43,12 +43,12 @@ public abstract class ConsumeService {
protected final ClientId clientId;
protected final String consumerGroup;
private final MessageListener messageListener;
- private final ThreadPoolExecutor consumptionExecutor;
+ private final ExecutorService consumptionExecutor;
private final MessageInterceptor messageInterceptor;
private final ScheduledExecutorService scheduler;
public ConsumeService(ClientId clientId, String consumerGroup,
- MessageListener messageListener, ThreadPoolExecutor
consumptionExecutor,
+ MessageListener messageListener, ExecutorService consumptionExecutor,
MessageInterceptor messageInterceptor, ScheduledExecutorService
scheduler) {
this.clientId = clientId;
this.consumerGroup = consumerGroup;
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/FifoConsumeService.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/FifoConsumeService.java
index 81bea5a6..9d4fb8d5 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/FifoConsumeService.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/FifoConsumeService.java
@@ -25,8 +25,8 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.MessageListener;
import org.apache.rocketmq.client.java.hook.MessageInterceptor;
@@ -40,7 +40,7 @@ class FifoConsumeService extends ConsumeService {
private final boolean enableFifoConsumeAccelerator;
public FifoConsumeService(ClientId clientId, String consumerGroup,
MessageListener messageListener,
- ThreadPoolExecutor consumptionExecutor, MessageInterceptor
messageInterceptor,
+ ExecutorService consumptionExecutor, MessageInterceptor
messageInterceptor,
ScheduledExecutorService scheduler, boolean
enableFifoConsumeAccelerator) {
super(clientId, consumerGroup, messageListener, consumptionExecutor,
messageInterceptor, scheduler);
this.enableFifoConsumeAccelerator = enableFifoConsumeAccelerator;
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/LiteFifoConsumeService.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/LiteFifoConsumeService.java
index 5e79850a..5abeace9 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/LiteFifoConsumeService.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/LiteFifoConsumeService.java
@@ -23,8 +23,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.ConsumeResultSuspend;
@@ -36,7 +36,7 @@ import org.apache.rocketmq.client.java.misc.ClientId;
public class LiteFifoConsumeService extends FifoConsumeService {
public LiteFifoConsumeService(ClientId clientId, String consumerGroup,
MessageListener messageListener,
- ThreadPoolExecutor consumptionExecutor, MessageInterceptor
messageInterceptor,
+ ExecutorService consumptionExecutor, MessageInterceptor
messageInterceptor,
ScheduledExecutorService scheduler, boolean
enableFifoConsumeAccelerator) {
super(clientId, consumerGroup, messageListener, consumptionExecutor,
messageInterceptor, scheduler, enableFifoConsumeAccelerator);
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/LiteStandardConsumeService.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/LiteStandardConsumeService.java
index 6044aac3..6869be92 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/LiteStandardConsumeService.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/LiteStandardConsumeService.java
@@ -22,8 +22,8 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.MessageListener;
import org.apache.rocketmq.client.java.hook.MessageInterceptor;
@@ -36,7 +36,7 @@ public class LiteStandardConsumeService extends
ConsumeService {
private static final Logger log =
LoggerFactory.getLogger(LiteStandardConsumeService.class);
public LiteStandardConsumeService(ClientId clientId, String consumerGroup,
MessageListener messageListener,
- ThreadPoolExecutor consumptionExecutor, MessageInterceptor
messageInterceptor,
+ ExecutorService consumptionExecutor, MessageInterceptor
messageInterceptor,
ScheduledExecutorService scheduler) {
super(clientId, consumerGroup, messageListener, consumptionExecutor,
messageInterceptor, scheduler);
}
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/PushConsumerImpl.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/PushConsumerImpl.java
index 52f7fe67..94bd7374 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/PushConsumerImpl.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/PushConsumerImpl.java
@@ -41,6 +41,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -90,7 +91,7 @@ class PushConsumerImpl extends ConsumerImpl implements
PushConsumer {
private static final Logger log =
LoggerFactory.getLogger(PushConsumerImpl.class);
protected final MessageListener messageListener;
- protected final ThreadPoolExecutor consumptionExecutor;
+ protected final ExecutorService consumptionExecutor;
protected final boolean enableFifoConsumeAccelerator;
final AtomicLong consumptionOkQuantity;
@@ -152,13 +153,14 @@ class PushConsumerImpl extends ConsumerImpl implements
PushConsumer {
this.processQueueTable = new ConcurrentHashMap<>();
- this.consumptionExecutor = new ThreadPoolExecutor(
- consumptionThreadCount,
- consumptionThreadCount,
- 60,
- TimeUnit.SECONDS,
- new LinkedBlockingQueue<>(),
- new ThreadFactoryImpl("MessageConsumption",
this.getClientId().getIndex()));
+ this.consumptionExecutor =
ExecutorServices.newConcurrencyLimitedExecutorService(
+ clientConfiguration.isVirtualThreadsEnabled(),
consumptionThreadCount, () -> new ThreadPoolExecutor(
+ consumptionThreadCount,
+ consumptionThreadCount,
+ 60,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(),
+ new ThreadFactoryImpl("MessageConsumption",
this.getClientId().getIndex())));
this.inflightRequestCountInterceptor = new
InflightRequestCountInterceptor();
this.addMessageInterceptor(inflightRequestCountInterceptor);
@@ -606,7 +608,7 @@ class PushConsumerImpl extends ConsumerImpl implements
PushConsumer {
return getSettings().getRetryPolicy();
}
- public ThreadPoolExecutor getConsumptionExecutor() {
+ public ExecutorService getConsumptionExecutor() {
return consumptionExecutor;
}
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/StandardConsumeService.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/StandardConsumeService.java
index 828fb8e0..10da2fe6 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/StandardConsumeService.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/consumer/StandardConsumeService.java
@@ -22,8 +22,8 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.MessageListener;
import org.apache.rocketmq.client.java.hook.MessageInterceptor;
@@ -37,7 +37,7 @@ public class StandardConsumeService extends ConsumeService {
private static final Logger log =
LoggerFactory.getLogger(StandardConsumeService.class);
public StandardConsumeService(ClientId clientId, String consumerGroup,
MessageListener messageListener,
- ThreadPoolExecutor consumptionExecutor, MessageInterceptor
messageInterceptor,
+ ExecutorService consumptionExecutor, MessageInterceptor
messageInterceptor,
ScheduledExecutorService scheduler) {
super(clientId, consumerGroup, messageListener, consumptionExecutor,
messageInterceptor, scheduler);
}
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/misc/ExecutorServices.java
b/java/client/src/main/java/org/apache/rocketmq/client/java/misc/ExecutorServices.java
index cc7fde8c..5757b748 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/misc/ExecutorServices.java
+++
b/java/client/src/main/java/org/apache/rocketmq/client/java/misc/ExecutorServices.java
@@ -17,15 +17,150 @@
package org.apache.rocketmq.client.java.misc;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class ExecutorServices {
+ private static final Logger log =
LoggerFactory.getLogger(ExecutorServices.class);
+ private static final Method NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR =
findVirtualThreadExecutorFactory();
+ private static final AtomicBoolean VIRTUAL_THREAD_FALLBACK_LOGGED = new
AtomicBoolean(false);
+
private ExecutorServices() {
}
+ /**
+ * Creates a virtual-thread-per-task executor when requested and supported
by the runtime. Reflection keeps the
+ * client binary compatible with Java 8 while allowing it to use the JDK
21 API when available.
+ */
+ public static ExecutorService newExecutorService(boolean
virtualThreadsEnabled,
+ Supplier<ExecutorService> platformExecutorSupplier) {
+ if (!virtualThreadsEnabled) {
+ return platformExecutorSupplier.get();
+ }
+ if (null == NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR) {
+ logVirtualThreadFallback(null);
+ return platformExecutorSupplier.get();
+ }
+ try {
+ return (ExecutorService)
NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR.invoke(null);
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ logVirtualThreadFallback(e);
+ return platformExecutorSupplier.get();
+ }
+ }
+
+ /**
+ * Creates an executor which uses one virtual thread for each task when
requested and supported, while limiting the
+ * number of concurrently running tasks. A semaphore is used instead of
pooling virtual threads so tasks waiting for
+ * a permit do not occupy carrier threads.
+ */
+ public static ExecutorService newConcurrencyLimitedExecutorService(boolean
virtualThreadsEnabled,
+ int maxConcurrency, Supplier<ExecutorService>
platformExecutorSupplier) {
+ if (!virtualThreadsEnabled) {
+ return platformExecutorSupplier.get();
+ }
+ if (maxConcurrency <= 0) {
+ throw new IllegalArgumentException("maxConcurrency should be
positive");
+ }
+ return new ConcurrencyLimitedExecutorService(
+ newExecutorService(true, platformExecutorSupplier),
maxConcurrency);
+ }
+
+ static boolean isVirtualThreadSupported() {
+ return null != NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR;
+ }
+
+ private static Method findVirtualThreadExecutorFactory() {
+ try {
+ return
java.util.concurrent.Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
+ } catch (NoSuchMethodException | SecurityException ignored) {
+ return null;
+ }
+ }
+
+ private static void logVirtualThreadFallback(Throwable t) {
+ if (!VIRTUAL_THREAD_FALLBACK_LOGGED.compareAndSet(false, true)) {
+ return;
+ }
+ if (null == t) {
+ log.warn("Virtual threads were enabled, but the runtime does not
provide them; falling back to platform "
+ + "threads");
+ return;
+ }
+ log.warn("Failed to create a virtual-thread executor; falling back to
platform threads", t);
+ }
+
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean awaitTerminated(ExecutorService executor) throws
InterruptedException {
return executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
+
+ private static class ConcurrencyLimitedExecutorService extends
AbstractExecutorService {
+ private final ExecutorService delegate;
+ private final Semaphore semaphore;
+
+ private ConcurrencyLimitedExecutorService(ExecutorService delegate,
int maxConcurrency) {
+ this.delegate = delegate;
+ this.semaphore = new Semaphore(maxConcurrency, true);
+ }
+
+ @Override
+ public void shutdown() {
+ delegate.shutdown();
+ }
+
+ @Override
+ public List<Runnable> shutdownNow() {
+ return delegate.shutdownNow();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return delegate.isShutdown();
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return delegate.isTerminated();
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, TimeUnit unit) throws
InterruptedException {
+ return delegate.awaitTermination(timeout, unit);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ Objects.requireNonNull(command, "command");
+ delegate.execute(() -> {
+ boolean acquired = false;
+ try {
+ semaphore.acquire();
+ acquired = true;
+ command.run();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ if (command instanceof Future<?>) {
+ // AbstractExecutorService.submit() wraps tasks in a
FutureTask before execute(); cancel it so
+ // callers are not left waiting.
+ ((Future<?>) command).cancel(false);
+ }
+ } finally {
+ if (acquired) {
+ semaphore.release();
+ }
+ }
+ });
+ }
+ }
}
diff --git
a/java/client/src/main/java/org/apache/rocketmq/client/java/misc/ExecutorServices.java
b/java/client/src/test/java/org/apache/rocketmq/client/apis/ClientConfigurationTest.java
similarity index 53%
copy from
java/client/src/main/java/org/apache/rocketmq/client/java/misc/ExecutorServices.java
copy to
java/client/src/test/java/org/apache/rocketmq/client/apis/ClientConfigurationTest.java
index cc7fde8c..d523db84 100644
---
a/java/client/src/main/java/org/apache/rocketmq/client/java/misc/ExecutorServices.java
+++
b/java/client/src/test/java/org/apache/rocketmq/client/apis/ClientConfigurationTest.java
@@ -15,17 +15,29 @@
* limitations under the License.
*/
-package org.apache.rocketmq.client.java.misc;
+package org.apache.rocketmq.client.apis;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
+import org.junit.Assert;
+import org.junit.Test;
-public class ExecutorServices {
- private ExecutorServices() {
+public class ClientConfigurationTest {
+
+ @Test
+ public void testVirtualThreadsDisabledByDefault() {
+ ClientConfiguration configuration = ClientConfiguration.newBuilder()
+ .setEndpoints("localhost:8081")
+ .build();
+
+ Assert.assertFalse(configuration.isVirtualThreadsEnabled());
}
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- public static boolean awaitTerminated(ExecutorService executor) throws
InterruptedException {
- return executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+ @Test
+ public void testEnableVirtualThreads() {
+ ClientConfiguration configuration = ClientConfiguration.newBuilder()
+ .setEndpoints("localhost:8081")
+ .enableVirtualThreads(true)
+ .build();
+
+ Assert.assertTrue(configuration.isVirtualThreadsEnabled());
}
}
diff --git
a/java/client/src/test/java/org/apache/rocketmq/client/java/misc/ExecutorServicesTest.java
b/java/client/src/test/java/org/apache/rocketmq/client/java/misc/ExecutorServicesTest.java
new file mode 100644
index 00000000..a0e65834
--- /dev/null
+++
b/java/client/src/test/java/org/apache/rocketmq/client/java/misc/ExecutorServicesTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.rocketmq.client.java.misc;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ExecutorServicesTest {
+
+ @Test
+ public void testVirtualThreadsDisabled() throws Exception {
+ ExecutorService executor = ExecutorServices.newExecutorService(false,
Executors::newSingleThreadExecutor);
+ try {
+ Future<Boolean> future = executor.submit(() ->
isVirtual(Thread.currentThread()));
+ Assert.assertFalse(future.get());
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testVirtualThreadsEnabledWhenSupported() throws Exception {
+ AtomicBoolean platformExecutorCreated = new AtomicBoolean(false);
+ ExecutorService executor = ExecutorServices.newExecutorService(true,
() -> {
+ platformExecutorCreated.set(true);
+ return Executors.newSingleThreadExecutor();
+ });
+ try {
+ Future<Boolean> future = executor.submit(() ->
isVirtual(Thread.currentThread()));
+ Assert.assertEquals(ExecutorServices.isVirtualThreadSupported(),
future.get());
+ Assert.assertEquals(!ExecutorServices.isVirtualThreadSupported(),
platformExecutorCreated.get());
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testConcurrencyLimitedExecutorService() throws Exception {
+ ExecutorService executor =
ExecutorServices.newConcurrencyLimitedExecutorService(
+ true, 1, Executors::newCachedThreadPool);
+ CountDownLatch firstTaskStarted = new CountDownLatch(1);
+ CountDownLatch releaseFirstTask = new CountDownLatch(1);
+ CountDownLatch secondTaskStarted = new CountDownLatch(1);
+ try {
+ Future<Boolean> first = executor.submit(() -> {
+ firstTaskStarted.countDown();
+ releaseFirstTask.await();
+ return isVirtual(Thread.currentThread());
+ });
+ Assert.assertTrue(firstTaskStarted.await(5, TimeUnit.SECONDS));
+
+ Future<Boolean> second = executor.submit(() -> {
+ secondTaskStarted.countDown();
+ return isVirtual(Thread.currentThread());
+ });
+ Assert.assertFalse(secondTaskStarted.await(200,
TimeUnit.MILLISECONDS));
+
+ releaseFirstTask.countDown();
+ Assert.assertEquals(ExecutorServices.isVirtualThreadSupported(),
first.get(5, TimeUnit.SECONDS));
+ Assert.assertEquals(ExecutorServices.isVirtualThreadSupported(),
second.get(5, TimeUnit.SECONDS));
+ Assert.assertTrue(secondTaskStarted.await(5, TimeUnit.SECONDS));
+ } finally {
+ releaseFirstTask.countDown();
+ executor.shutdownNow();
+ }
+ }
+
+ private static boolean isVirtual(Thread thread) throws Exception {
+ Method method;
+ try {
+ method = Thread.class.getMethod("isVirtual");
+ } catch (NoSuchMethodException ignored) {
+ return false;
+ }
+ return (Boolean) method.invoke(thread);
+ }
+}