diff --git a/src/main/java/redis/clients/jedis/ClusterPipeline.java b/src/main/java/redis/clients/jedis/ClusterPipeline.java index 9564bd167b..404f09ce9b 100644 --- a/src/main/java/redis/clients/jedis/ClusterPipeline.java +++ b/src/main/java/redis/clients/jedis/ClusterPipeline.java @@ -2,6 +2,7 @@ import java.time.Duration; import java.util.Set; + import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import redis.clients.jedis.providers.ClusterConnectionProvider; import redis.clients.jedis.util.IOUtils; @@ -40,6 +41,12 @@ public ClusterPipeline(ClusterConnectionProvider provider, ClusterCommandObjects this.provider = provider; } + public ClusterPipeline(ClusterConnectionProvider provider, ClusterCommandObjects commandObjects, + ClusterPipelineExecutor executorService) { + super(commandObjects, executorService); + this.provider = provider; + } + private static ClusterCommandObjects createClusterCommandObjects(RedisProtocol protocol) { ClusterCommandObjects cco = new ClusterCommandObjects(); if (protocol == RedisProtocol.RESP3) cco.setProtocol(protocol); diff --git a/src/main/java/redis/clients/jedis/ClusterPipelineExecutor.java b/src/main/java/redis/clients/jedis/ClusterPipelineExecutor.java new file mode 100644 index 0000000000..d38fb1d30d --- /dev/null +++ b/src/main/java/redis/clients/jedis/ClusterPipelineExecutor.java @@ -0,0 +1,43 @@ +package redis.clients.jedis; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +/** + * Executor used for parallel syncing of multinode pipeline when provided in + * {@link DefaultJedisClientConfig.Builder#pipelineExecutorProvider(PipelineExecutorProvider)} + */ +public interface ClusterPipelineExecutor extends Executor, AutoCloseable { + + /** + * To avoid following hte {@link JedisCluster} client lifecycle in shutting down the executor service + * provide your own implementation of this interface to {@link PipelineExecutorProvider} + */ + default void shutdown() {} + + default void close() { + shutdown(); + } + + /** + * Wrap an executor service into a {@link ClusterPipelineExecutor} to allow clients to provide their + * desired implementation of the {@link ExecutorService} to support parallel syncing of {@link MultiNodePipelineBase}. + * + * @param executorService + * @return ClusterPipelineExecutor that will be shutdown alongside the {@link JedisCluster} client. + */ + static ClusterPipelineExecutor from(ExecutorService executorService) { + return new ClusterPipelineExecutor() { + @Override + public void execute(Runnable command) { + executorService.execute(command); + } + + @Override + public void shutdown() { + executorService.shutdown(); + } + }; + } + +} diff --git a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java index 25a4737ec0..ac0e18bdb2 100644 --- a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java @@ -33,6 +33,8 @@ public final class DefaultJedisClientConfig implements JedisClientConfig { private final AuthXManager authXManager; + private final PipelineExecutorProvider pipelineExecutorProvider; + private DefaultJedisClientConfig(DefaultJedisClientConfig.Builder builder) { this.redisProtocol = builder.redisProtocol; this.connectionTimeoutMillis = builder.connectionTimeoutMillis; @@ -50,6 +52,7 @@ private DefaultJedisClientConfig(DefaultJedisClientConfig.Builder builder) { this.clientSetInfoConfig = builder.clientSetInfoConfig; this.readOnlyForRedisClusterReplicas = builder.readOnlyForRedisClusterReplicas; this.authXManager = builder.authXManager; + this.pipelineExecutorProvider = builder.pipelineExecutorProvider; } @Override @@ -143,6 +146,11 @@ public boolean isReadOnlyForRedisClusterReplicas() { return readOnlyForRedisClusterReplicas; } + @Override + public PipelineExecutorProvider getPipelineExecutorProvider() { + return pipelineExecutorProvider; + } + public static Builder builder() { return new Builder(); } @@ -175,6 +183,8 @@ public static class Builder { private AuthXManager authXManager = null; + private PipelineExecutorProvider pipelineExecutorProvider = PipelineExecutorProvider.DEFAULT; + private Builder() { } @@ -297,6 +307,11 @@ public Builder authXManager(AuthXManager authXManager) { return this; } + public Builder pipelineExecutorProvider(PipelineExecutorProvider pipelineExecutorProvider) { + this.pipelineExecutorProvider = pipelineExecutorProvider; + return this; + } + public Builder from(JedisClientConfig instance) { this.redisProtocol = instance.getRedisProtocol(); this.connectionTimeoutMillis = instance.getConnectionTimeoutMillis(); @@ -314,6 +329,7 @@ public Builder from(JedisClientConfig instance) { this.clientSetInfoConfig = instance.getClientSetInfoConfig(); this.readOnlyForRedisClusterReplicas = instance.isReadOnlyForRedisClusterReplicas(); this.authXManager = instance.getAuthXManager(); + this.pipelineExecutorProvider = instance.getPipelineExecutorProvider(); return this; } } diff --git a/src/main/java/redis/clients/jedis/JedisClientConfig.java b/src/main/java/redis/clients/jedis/JedisClientConfig.java index ce7fd82de4..1046086de8 100644 --- a/src/main/java/redis/clients/jedis/JedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/JedisClientConfig.java @@ -115,4 +115,15 @@ default boolean isReadOnlyForRedisClusterReplicas() { default ClientSetInfoConfig getClientSetInfoConfig() { return ClientSetInfoConfig.DEFAULT; } + + /** + * If different then DEFAULT this will provide an Executor implementation that will sync/close multi node pipelines + * in parallel. This replaces the deprecated internal usage of new Executor Services for every pipeline, resulting in + * high thread creation rates and impact on latency. + * + * @return PipelineExecutorProvider + */ + default PipelineExecutorProvider getPipelineExecutorProvider() { + return PipelineExecutorProvider.DEFAULT; + } } diff --git a/src/main/java/redis/clients/jedis/JedisCluster.java b/src/main/java/redis/clients/jedis/JedisCluster.java index d4c555230c..4f74ab260c 100644 --- a/src/main/java/redis/clients/jedis/JedisCluster.java +++ b/src/main/java/redis/clients/jedis/JedisCluster.java @@ -13,6 +13,7 @@ import redis.clients.jedis.csc.Cache; import redis.clients.jedis.csc.CacheConfig; import redis.clients.jedis.csc.CacheFactory; +import redis.clients.jedis.util.IOUtils; import redis.clients.jedis.util.JedisClusterCRC16; public class JedisCluster extends UnifiedJedis { @@ -29,6 +30,12 @@ public class JedisCluster extends UnifiedJedis { */ public static final int DEFAULT_MAX_ATTEMPTS = 5; + /** + * Executor used to close MultiNodePipeline in parallel. See {@link JedisClientConfig#getPipelineExecutorProvider()} + * for mor details on configuration. + */ + private ClusterPipelineExecutor clusterPipelineExecutor; + /** * Creates a JedisCluster instance. The provided node is used to make the first contact with the cluster. *
@@ -251,6 +258,8 @@ public JedisCluster(Set
* Key is the HOST:PORT and the value is the connection pool.
@@ -376,9 +395,14 @@ public void ssubscribe(BinaryJedisShardedPubSub jedisPubSub, final byte[]... cha
@Override
public ClusterPipeline pipelined() {
- return new ClusterPipeline((ClusterConnectionProvider) provider, (ClusterCommandObjects) commandObjects);
+ if (clusterPipelineExecutor == null) {
+ return new ClusterPipeline((ClusterConnectionProvider) provider, (ClusterCommandObjects) commandObjects);
+ }
+ return new ClusterPipeline((ClusterConnectionProvider) provider, (ClusterCommandObjects) commandObjects,
+ clusterPipelineExecutor);
}
+
/**
* @param doMulti param
* @return nothing
diff --git a/src/main/java/redis/clients/jedis/MultiNodePipelineBase.java b/src/main/java/redis/clients/jedis/MultiNodePipelineBase.java
index 247069410a..86044a240d 100644
--- a/src/main/java/redis/clients/jedis/MultiNodePipelineBase.java
+++ b/src/main/java/redis/clients/jedis/MultiNodePipelineBase.java
@@ -1,13 +1,12 @@
package redis.clients.jedis;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
@@ -24,11 +23,17 @@ public abstract class MultiNodePipelineBase extends PipelineBase {
* The number of processes for {@code sync()}. If you have enough cores for client (and you have
* more than 3 cluster nodes), you may increase this number of workers.
* Suggestion: ≤ cluster nodes.
+ *
+ * @deprecated Client using this approach are paying the thread creation cost for every pipeline sync. Clients
+ * should use refer to {@link JedisClientConfig#getPipelineExecutorProvider()} to provide a single Executor for
+ * gain in performance.
*/
public static volatile int MULTI_NODE_PIPELINE_SYNC_WORKERS = 3;
private final Map