DATAREDIS-720 - Reuse shared connection across reactive connection wrappers.
We now reuse a shared Lettuce connection across our reactive connection wrappers if LettuceConnectionFactory is configured to provide a shared connection instance. Reactive connections can either operate with a shared connection or with a connection provider only. We also now execute blocking Redis commands (BLPOP, BRPOP, BRPOPLPUSH) on dedicated connection. Original Pull Request: #290
This commit is contained in:
committed by
Christoph Strobl
parent
cb61a50247
commit
ca5f9ee2d6
@@ -20,12 +20,15 @@ import io.lettuce.core.ClientOptions;
|
|||||||
import io.lettuce.core.RedisClient;
|
import io.lettuce.core.RedisClient;
|
||||||
import io.lettuce.core.RedisException;
|
import io.lettuce.core.RedisException;
|
||||||
import io.lettuce.core.RedisURI;
|
import io.lettuce.core.RedisURI;
|
||||||
|
import io.lettuce.core.api.StatefulConnection;
|
||||||
import io.lettuce.core.api.StatefulRedisConnection;
|
import io.lettuce.core.api.StatefulRedisConnection;
|
||||||
import io.lettuce.core.cluster.ClusterClientOptions;
|
import io.lettuce.core.cluster.ClusterClientOptions;
|
||||||
import io.lettuce.core.cluster.RedisClusterClient;
|
import io.lettuce.core.cluster.RedisClusterClient;
|
||||||
|
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
|
||||||
import io.lettuce.core.codec.RedisCodec;
|
import io.lettuce.core.codec.RedisCodec;
|
||||||
import io.lettuce.core.resource.ClientResources;
|
import io.lettuce.core.resource.ClientResources;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -88,6 +91,7 @@ public class LettuceConnectionFactory
|
|||||||
private boolean validateConnection = false;
|
private boolean validateConnection = false;
|
||||||
private boolean shareNativeConnection = true;
|
private boolean shareNativeConnection = true;
|
||||||
private @Nullable StatefulRedisConnection<byte[], byte[]> connection;
|
private @Nullable StatefulRedisConnection<byte[], byte[]> connection;
|
||||||
|
private @Nullable StatefulConnection<ByteBuffer, ByteBuffer> reactiveConnection;
|
||||||
private @Nullable LettucePool pool;
|
private @Nullable LettucePool pool;
|
||||||
/** Synchronization monitor for the shared Connection */
|
/** Synchronization monitor for the shared Connection */
|
||||||
private final Object connectionMonitor = new Object();
|
private final Object connectionMonitor = new Object();
|
||||||
@@ -242,6 +246,7 @@ public class LettuceConnectionFactory
|
|||||||
public void destroy() {
|
public void destroy() {
|
||||||
|
|
||||||
resetConnection();
|
resetConnection();
|
||||||
|
resetReactiveConnection();
|
||||||
|
|
||||||
if (connectionProvider instanceof DisposableBean) {
|
if (connectionProvider instanceof DisposableBean) {
|
||||||
try {
|
try {
|
||||||
@@ -318,7 +323,10 @@ public class LettuceConnectionFactory
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public LettuceReactiveRedisConnection getReactiveConnection() {
|
public LettuceReactiveRedisConnection getReactiveConnection() {
|
||||||
return new LettuceReactiveRedisConnection(reactiveConnectionProvider);
|
|
||||||
|
return getShareNativeConnection()
|
||||||
|
? new LettuceReactiveRedisConnection(getSharedReactiveConnection(), reactiveConnectionProvider)
|
||||||
|
: new LettuceReactiveRedisConnection(reactiveConnectionProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -327,11 +335,16 @@ public class LettuceConnectionFactory
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public LettuceReactiveRedisClusterConnection getReactiveClusterConnection() {
|
public LettuceReactiveRedisClusterConnection getReactiveClusterConnection() {
|
||||||
|
|
||||||
if (!isClusterAware()) {
|
if (!isClusterAware()) {
|
||||||
throw new InvalidDataAccessApiUsageException("Cluster is not configured!");
|
throw new InvalidDataAccessApiUsageException("Cluster is not configured!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LettuceReactiveRedisClusterConnection(reactiveConnectionProvider);
|
RedisClusterClient client = (RedisClusterClient) this.client;
|
||||||
|
|
||||||
|
return getShareNativeConnection()
|
||||||
|
? new LettuceReactiveRedisClusterConnection(getSharedReactiveConnection(), reactiveConnectionProvider, client)
|
||||||
|
: new LettuceReactiveRedisClusterConnection(reactiveConnectionProvider, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initConnection() {
|
public void initConnection() {
|
||||||
@@ -344,11 +357,23 @@ public class LettuceConnectionFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void initReactiveConnection() {
|
||||||
|
|
||||||
|
synchronized (this.connectionMonitor) {
|
||||||
|
if (this.reactiveConnection != null) {
|
||||||
|
resetReactiveConnection();
|
||||||
|
}
|
||||||
|
this.reactiveConnection = createReactiveLettuceConnector();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the underlying shared Connection, to be reinitialized on next access.
|
* Reset the underlying shared Connection, to be reinitialized on next access.
|
||||||
*/
|
*/
|
||||||
public void resetConnection() {
|
public void resetConnection() {
|
||||||
|
|
||||||
synchronized (this.connectionMonitor) {
|
synchronized (this.connectionMonitor) {
|
||||||
|
|
||||||
if (this.connection != null) {
|
if (this.connection != null) {
|
||||||
this.connectionProvider.release(this.connection);
|
this.connectionProvider.release(this.connection);
|
||||||
}
|
}
|
||||||
@@ -356,17 +381,49 @@ public class LettuceConnectionFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the underlying shared Connection, to be reinitialized on next access.
|
||||||
|
*
|
||||||
|
* @since 2.0.1
|
||||||
|
*/
|
||||||
|
public void resetReactiveConnection() {
|
||||||
|
|
||||||
|
synchronized (this.connectionMonitor) {
|
||||||
|
|
||||||
|
if (this.reactiveConnection != null) {
|
||||||
|
this.reactiveConnectionProvider.release(this.reactiveConnection);
|
||||||
|
}
|
||||||
|
this.reactiveConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the shared Connection and reinitialize if invalid
|
* Validate the shared Connection and reinitialize if invalid
|
||||||
*/
|
*/
|
||||||
public void validateConnection() {
|
public void validateConnection() {
|
||||||
|
|
||||||
|
validateConnection(connection, connectionProvider, this::initConnection);
|
||||||
|
|
||||||
|
validateConnection(reactiveConnection, reactiveConnectionProvider, this::initReactiveConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateConnection(@Nullable StatefulConnection<?, ?> connection,
|
||||||
|
LettuceConnectionProvider connectionProvider, Runnable initConnection) {
|
||||||
|
|
||||||
synchronized (this.connectionMonitor) {
|
synchronized (this.connectionMonitor) {
|
||||||
|
|
||||||
boolean valid = false;
|
boolean valid = false;
|
||||||
|
|
||||||
if (connection.isOpen()) {
|
if (connection != null && connection.isOpen()) {
|
||||||
try {
|
try {
|
||||||
connection.sync().ping();
|
|
||||||
|
if (connection instanceof StatefulRedisConnection) {
|
||||||
|
((StatefulRedisConnection) connection).sync().ping();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection instanceof StatefulRedisClusterConnection) {
|
||||||
|
((StatefulRedisConnection) connection).sync().ping();
|
||||||
|
}
|
||||||
valid = true;
|
valid = true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Validation failed", e);
|
log.debug("Validation failed", e);
|
||||||
@@ -375,9 +432,13 @@ public class LettuceConnectionFactory
|
|||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
|
||||||
connectionProvider.release(connection);
|
if (connection != null) {
|
||||||
|
connectionProvider.release(connection);
|
||||||
|
}
|
||||||
|
|
||||||
log.warn("Validation of shared connection failed. Creating a new connection.");
|
log.warn("Validation of shared connection failed. Creating a new connection.");
|
||||||
initConnection();
|
|
||||||
|
initConnection.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -775,7 +836,12 @@ public class LettuceConnectionFactory
|
|||||||
return clusterConfiguration != null;
|
return clusterConfiguration != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the shared connection using {@literal byte} array encoding for imperative API use. {@literal null} if
|
||||||
|
* {@link #getShareNativeConnection() connection sharing} is disabled.
|
||||||
|
*/
|
||||||
protected StatefulRedisConnection<byte[], byte[]> getSharedConnection() {
|
protected StatefulRedisConnection<byte[], byte[]> getSharedConnection() {
|
||||||
|
|
||||||
if (shareNativeConnection) {
|
if (shareNativeConnection) {
|
||||||
synchronized (this.connectionMonitor) {
|
synchronized (this.connectionMonitor) {
|
||||||
if (this.connection == null) {
|
if (this.connection == null) {
|
||||||
@@ -791,6 +857,28 @@ public class LettuceConnectionFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the shared connection using {@link ByteBuffer} encoding for reactive API use. {@literal null} if
|
||||||
|
* {@link #getShareNativeConnection() connection sharing} is disabled.
|
||||||
|
* @since 2.0.1
|
||||||
|
*/
|
||||||
|
protected StatefulConnection<ByteBuffer, ByteBuffer> getSharedReactiveConnection() {
|
||||||
|
|
||||||
|
if (shareNativeConnection) {
|
||||||
|
synchronized (this.connectionMonitor) {
|
||||||
|
if (this.reactiveConnection == null) {
|
||||||
|
initReactiveConnection();
|
||||||
|
}
|
||||||
|
if (validateConnection) {
|
||||||
|
validateConnection();
|
||||||
|
}
|
||||||
|
return this.reactiveConnection;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected StatefulRedisConnection<byte[], byte[]> createLettuceConnector() {
|
protected StatefulRedisConnection<byte[], byte[]> createLettuceConnector() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -809,6 +897,23 @@ public class LettuceConnectionFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected StatefulConnection<ByteBuffer, ByteBuffer> createReactiveLettuceConnector() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
StatefulConnection<ByteBuffer, ByteBuffer> connection;
|
||||||
|
connection = reactiveConnectionProvider.getConnection(StatefulConnection.class);
|
||||||
|
|
||||||
|
if (connection instanceof StatefulRedisConnection && getDatabase() > 0) {
|
||||||
|
((StatefulRedisConnection) connection).sync().select(getDatabase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
} catch (RedisException e) {
|
||||||
|
throw new RedisConnectionFailureException("Unable to connect to Redis on " + getHostName() + ":" + getPort(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private LettuceConnectionProvider createConnectionProvider(AbstractRedisClient client, RedisCodec<?, ?> codec) {
|
private LettuceConnectionProvider createConnectionProvider(AbstractRedisClient client, RedisCodec<?, ?> codec) {
|
||||||
|
|
||||||
LettuceConnectionProvider connectionProvider = doConnectionProvider(client, codec);
|
LettuceConnectionProvider connectionProvider = doConnectionProvider(client, codec);
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class LettuceReactiveListCommands implements ReactiveListCommands {
|
|||||||
@Override
|
@Override
|
||||||
public Flux<PopResponse> bPop(Publisher<BPopCommand> commands) {
|
public Flux<PopResponse> bPop(Publisher<BPopCommand> commands) {
|
||||||
|
|
||||||
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
|
return connection.executeDedicated(cmd -> Flux.from(commands).concatMap(command -> {
|
||||||
|
|
||||||
Assert.notNull(command.getKeys(), "Keys must not be null!");
|
Assert.notNull(command.getKeys(), "Keys must not be null!");
|
||||||
Assert.notNull(command.getDirection(), "Direction must not be null!");
|
Assert.notNull(command.getDirection(), "Direction must not be null!");
|
||||||
@@ -281,7 +281,7 @@ class LettuceReactiveListCommands implements ReactiveListCommands {
|
|||||||
@Override
|
@Override
|
||||||
public Flux<ByteBufferResponse<BRPopLPushCommand>> bRPopLPush(Publisher<BRPopLPushCommand> commands) {
|
public Flux<ByteBufferResponse<BRPopLPushCommand>> bRPopLPush(Publisher<BRPopLPushCommand> commands) {
|
||||||
|
|
||||||
return connection.execute(cmd -> Flux.from(commands).concatMap(command -> {
|
return connection.executeDedicated(cmd -> Flux.from(commands).concatMap(command -> {
|
||||||
|
|
||||||
Assert.notNull(command.getKey(), "Key must not be null!");
|
Assert.notNull(command.getKey(), "Key must not be null!");
|
||||||
Assert.notNull(command.getDestination(), "Destination key must not be null!");
|
Assert.notNull(command.getDestination(), "Destination key must not be null!");
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.data.redis.connection.lettuce;
|
package org.springframework.data.redis.connection.lettuce;
|
||||||
|
|
||||||
|
import io.lettuce.core.api.StatefulConnection;
|
||||||
import io.lettuce.core.api.reactive.BaseRedisReactiveCommands;
|
import io.lettuce.core.api.reactive.BaseRedisReactiveCommands;
|
||||||
import io.lettuce.core.api.reactive.RedisReactiveCommands;
|
import io.lettuce.core.api.reactive.RedisReactiveCommands;
|
||||||
import io.lettuce.core.cluster.RedisClusterClient;
|
import io.lettuce.core.cluster.RedisClusterClient;
|
||||||
@@ -45,19 +46,37 @@ class LettuceReactiveRedisClusterConnection extends LettuceReactiveRedisConnecti
|
|||||||
private final ClusterTopologyProvider topologyProvider;
|
private final ClusterTopologyProvider topologyProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new {@link LettuceReactiveRedisClusterConnection}.
|
* Creates new {@link LettuceReactiveRedisClusterConnection} given {@link LettuceConnectionProvider} and
|
||||||
|
* {@link RedisClusterClient}.
|
||||||
*
|
*
|
||||||
|
* @param connectionProvider must not be {@literal null}.
|
||||||
* @param client must not be {@literal null}.
|
* @param client must not be {@literal null}.
|
||||||
* @throws IllegalArgumentException when {@code client} is {@literal null}.
|
* @throws IllegalArgumentException when {@code client} is {@literal null}.
|
||||||
* @throws org.springframework.dao.InvalidDataAccessResourceUsageException when {@code client} is not suitable for
|
|
||||||
* cluster environment.
|
|
||||||
*/
|
*/
|
||||||
LettuceReactiveRedisClusterConnection(LettuceConnectionProvider connectionProvider) {
|
LettuceReactiveRedisClusterConnection(LettuceConnectionProvider connectionProvider, RedisClusterClient client) {
|
||||||
|
|
||||||
super(connectionProvider);
|
super(connectionProvider);
|
||||||
|
|
||||||
this.topologyProvider = new LettuceClusterTopologyProvider(
|
this.topologyProvider = new LettuceClusterTopologyProvider(client);
|
||||||
((ClusterConnectionProvider) connectionProvider).getClient());
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates new {@link LettuceReactiveRedisClusterConnection} given a shared {@link StatefulConnection connection},
|
||||||
|
* {@link LettuceConnectionProvider} and {@link RedisClusterClient}.
|
||||||
|
*
|
||||||
|
* @param sharedConnection must not be {@literal null}.
|
||||||
|
* @param connectionProvider must not be {@literal null}.
|
||||||
|
* @param client must not be {@literal null}.
|
||||||
|
* @throws IllegalArgumentException when {@code client} is {@literal null}.
|
||||||
|
* @since 2.0.1
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
LettuceReactiveRedisClusterConnection(StatefulConnection<ByteBuffer, ByteBuffer> sharedConnection,
|
||||||
|
LettuceConnectionProvider connectionProvider, RedisClusterClient client) {
|
||||||
|
|
||||||
|
super(sharedConnection, connectionProvider);
|
||||||
|
|
||||||
|
this.topologyProvider = new LettuceClusterTopologyProvider(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -183,7 +202,7 @@ class LettuceReactiveRedisClusterConnection extends LettuceReactiveRedisConnecti
|
|||||||
return Flux.error(e);
|
return Flux.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Flux.defer(() -> callback.doWithCommands(getCommands(node))).onErrorMap(translateException());
|
return getCommands(node).flatMapMany(callback::doWithCommands).onErrorMap(translateException());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -192,33 +211,27 @@ class LettuceReactiveRedisClusterConnection extends LettuceReactiveRedisConnecti
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
@Override
|
@Override
|
||||||
protected StatefulRedisClusterConnection<ByteBuffer, ByteBuffer> getConnection() {
|
protected Mono<StatefulRedisClusterConnection<ByteBuffer, ByteBuffer>> getConnection() {
|
||||||
|
return (Mono) super.getConnection();
|
||||||
Assert.isInstanceOf(StatefulRedisClusterConnection.class, super.getConnection(),
|
|
||||||
"Connection needs to be instance of StatefulRedisClusterConnection");
|
|
||||||
|
|
||||||
return (StatefulRedisClusterConnection) super.getConnection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* (non-Javadoc)
|
* (non-Javadoc)
|
||||||
* @see org.springframework.data.redis.connection.lettuce.LettuceReactiveRedisConnection#getCommands()
|
* @see org.springframework.data.redis.connection.lettuce.LettuceReactiveRedisConnection#getCommands()
|
||||||
*/
|
*/
|
||||||
protected RedisClusterReactiveCommands<ByteBuffer, ByteBuffer> getCommands() {
|
protected Mono<RedisClusterReactiveCommands<ByteBuffer, ByteBuffer>> getCommands() {
|
||||||
return getConnection().reactive();
|
return getConnection().map(StatefulRedisClusterConnection::reactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
protected RedisReactiveCommands<ByteBuffer, ByteBuffer> getCommands(RedisNode node) {
|
protected Mono<RedisReactiveCommands<ByteBuffer, ByteBuffer>> getCommands(RedisNode node) {
|
||||||
|
|
||||||
if (!(getConnection() instanceof StatefulRedisClusterConnection)) {
|
|
||||||
throw new IllegalArgumentException("o.O connection needs to be cluster compatible " + getConnection());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.hasText(node.getId())) {
|
if (StringUtils.hasText(node.getId())) {
|
||||||
return ((StatefulRedisClusterConnection) getConnection()).getConnection(node.getId()).reactive();
|
return getConnection().cast(StatefulRedisClusterConnection.class)
|
||||||
|
.map(it -> it.getConnection(node.getId()).reactive());
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((StatefulRedisClusterConnection) getConnection()).getConnection(node.getHost(), node.getPort()).reactive();
|
return getConnection().cast(StatefulRedisClusterConnection.class)
|
||||||
|
.map(it -> it.getConnection(node.getHost(), node.getPort()).reactive());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands;
|
|||||||
import io.lettuce.core.codec.RedisCodec;
|
import io.lettuce.core.codec.RedisCodec;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
@@ -43,9 +46,9 @@ class LettuceReactiveRedisConnection implements ReactiveRedisConnection {
|
|||||||
|
|
||||||
static final RedisCodec<ByteBuffer, ByteBuffer> CODEC = ByteBufferCodec.INSTANCE;
|
static final RedisCodec<ByteBuffer, ByteBuffer> CODEC = ByteBufferCodec.INSTANCE;
|
||||||
|
|
||||||
private final LettuceConnectionProvider connectionProvider;
|
private final AsyncConnect dedicatedConnection;
|
||||||
|
|
||||||
private @Nullable StatefulConnection<ByteBuffer, ByteBuffer> connection;
|
private @Nullable Mono<StatefulConnection<ByteBuffer, ByteBuffer>> sharedConnection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new {@link LettuceReactiveRedisConnection}.
|
* Creates new {@link LettuceReactiveRedisConnection}.
|
||||||
@@ -59,8 +62,27 @@ class LettuceReactiveRedisConnection implements ReactiveRedisConnection {
|
|||||||
|
|
||||||
Assert.notNull(connectionProvider, "LettuceConnectionProvider must not be null!");
|
Assert.notNull(connectionProvider, "LettuceConnectionProvider must not be null!");
|
||||||
|
|
||||||
this.connectionProvider = connectionProvider;
|
this.dedicatedConnection = new AsyncConnect(connectionProvider);
|
||||||
this.connection = connectionProvider.getConnection(StatefulConnection.class);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates new {@link LettuceReactiveRedisConnection} given a shared {@link StatefulConnection connection}.
|
||||||
|
*
|
||||||
|
* @param sharedConnection must not be {@literal null}.
|
||||||
|
* @param connectionProvider must not be {@literal null}.
|
||||||
|
* @throws IllegalArgumentException when {@code client} is {@literal null}.
|
||||||
|
* @throws InvalidDataAccessResourceUsageException when {@code client} is not suitable for connection.
|
||||||
|
* @since 2.0.1
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
LettuceReactiveRedisConnection(StatefulConnection<ByteBuffer, ByteBuffer> sharedConnection,
|
||||||
|
LettuceConnectionProvider connectionProvider) {
|
||||||
|
|
||||||
|
Assert.notNull(sharedConnection, "Shared StatefulConnection must not be null!");
|
||||||
|
Assert.notNull(connectionProvider, "LettuceConnectionProvider must not be null!");
|
||||||
|
|
||||||
|
this.dedicatedConnection = new AsyncConnect(connectionProvider);
|
||||||
|
this.sharedConnection = Mono.just(sharedConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -176,33 +198,55 @@ class LettuceReactiveRedisConnection implements ReactiveRedisConnection {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public <T> Flux<T> execute(LettuceReactiveCallback<T> callback) {
|
public <T> Flux<T> execute(LettuceReactiveCallback<T> callback) {
|
||||||
return Flux.defer(() -> callback.doWithCommands(getCommands())).onErrorMap(translateException());
|
return getCommands().flatMapMany(callback::doWithCommands).onErrorMap(translateException());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (non-Javadoc)
|
/**
|
||||||
|
* @param callback
|
||||||
|
* @return
|
||||||
|
* @since 2.0.1
|
||||||
|
*/
|
||||||
|
public <T> Flux<T> executeDedicated(LettuceReactiveCallback<T> callback) {
|
||||||
|
return getDedicatedCommands().flatMapMany(callback::doWithCommands).onErrorMap(translateException());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* (non-Javadoc)
|
||||||
* @see java.io.Closeable#close()
|
* @see java.io.Closeable#close()
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
dedicatedConnection.close();
|
||||||
if (connection != null) {
|
|
||||||
synchronized (connectionProvider) {
|
|
||||||
connectionProvider.release(connection);
|
|
||||||
connection = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected StatefulConnection<ByteBuffer, ByteBuffer> getConnection() {
|
protected Mono<? extends StatefulConnection<ByteBuffer, ByteBuffer>> getConnection() {
|
||||||
|
|
||||||
if (connection != null) {
|
if (sharedConnection != null) {
|
||||||
return connection;
|
return sharedConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalStateException("Connection is closed");
|
return getDedicatedConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected RedisClusterReactiveCommands<ByteBuffer, ByteBuffer> getCommands() {
|
protected Mono<StatefulConnection<ByteBuffer, ByteBuffer>> getDedicatedConnection() {
|
||||||
|
return dedicatedConnection.getConnection().onErrorMap(translateException());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Mono<? extends RedisClusterReactiveCommands<ByteBuffer, ByteBuffer>> getCommands() {
|
||||||
|
|
||||||
|
if (sharedConnection != null) {
|
||||||
|
return sharedConnection.map(LettuceReactiveRedisConnection::getRedisClusterReactiveCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDedicatedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Mono<? extends RedisClusterReactiveCommands<ByteBuffer, ByteBuffer>> getDedicatedCommands() {
|
||||||
|
return dedicatedConnection.getConnection().map(LettuceReactiveRedisConnection::getRedisClusterReactiveCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RedisClusterReactiveCommands<ByteBuffer, ByteBuffer> getRedisClusterReactiveCommands(
|
||||||
|
StatefulConnection<ByteBuffer, ByteBuffer> connection) {
|
||||||
|
|
||||||
if (connection instanceof StatefulRedisConnection) {
|
if (connection instanceof StatefulRedisConnection) {
|
||||||
return ((StatefulRedisConnection<ByteBuffer, ByteBuffer>) connection).reactive();
|
return ((StatefulRedisConnection<ByteBuffer, ByteBuffer>) connection).reactive();
|
||||||
@@ -210,7 +254,7 @@ class LettuceReactiveRedisConnection implements ReactiveRedisConnection {
|
|||||||
return ((StatefulRedisClusterConnection<ByteBuffer, ByteBuffer>) connection).reactive();
|
return ((StatefulRedisClusterConnection<ByteBuffer, ByteBuffer>) connection).reactive();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException("o.O unknown connection type " + connection);
|
throw new IllegalStateException("o.O unknown connection type " + connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
<T> Function<Throwable, Throwable> translateException() {
|
<T> Function<Throwable, Throwable> translateException() {
|
||||||
@@ -260,4 +304,94 @@ class LettuceReactiveRedisConnection implements ReactiveRedisConnection {
|
|||||||
return value.duplicate();
|
return value.duplicate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous connection utility. This utility has a lifecycle controlled by {@link #getConnection()} and
|
||||||
|
* {@link #close()} methods:
|
||||||
|
* <ol>
|
||||||
|
* <li>Initial state: Not connected. Calling {@link #getConnection()} will transition to the next state.</li>
|
||||||
|
* <li>Connection requested: First caller to {@link #getConnection()} initiates an asynchronous connect. Connection is
|
||||||
|
* accessible through the resulting {@link Mono}.</li>
|
||||||
|
* <li>Closing: Call to {@link #close()} initiates connection closing.</li>
|
||||||
|
* <li>Closed: Connection is closed.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author Mark Paluch
|
||||||
|
* @since 2.0.1
|
||||||
|
*/
|
||||||
|
static class AsyncConnect {
|
||||||
|
|
||||||
|
private final Mono<StatefulConnection<ByteBuffer, ByteBuffer>> connectionPublisher;
|
||||||
|
private final LettuceConnectionProvider connectionProvider;
|
||||||
|
|
||||||
|
private AtomicReference<State> state = new AtomicReference<>(State.INITIAL);
|
||||||
|
private volatile @Nullable CompletableFuture<StatefulConnection<ByteBuffer, ByteBuffer>> connection;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
AsyncConnect(LettuceConnectionProvider connectionProvider) {
|
||||||
|
|
||||||
|
Assert.notNull(connectionProvider, "LettuceConnectionProvider must not be null!");
|
||||||
|
|
||||||
|
this.connectionProvider = connectionProvider;
|
||||||
|
|
||||||
|
Mono<StatefulConnection<ByteBuffer, ByteBuffer>> defer = Mono
|
||||||
|
.defer(() -> Mono.<StatefulConnection<ByteBuffer, ByteBuffer>> just(
|
||||||
|
connectionProvider.getConnection(StatefulConnection.class)));
|
||||||
|
|
||||||
|
this.connectionPublisher = defer.subscribeOn(Schedulers.elastic());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain a connection publisher. This method connects asynchronously by requesting a connection from
|
||||||
|
* {@link LettuceConnectionProvider} with non-blocking synchronization if called concurrently.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Mono<StatefulConnection<ByteBuffer, ByteBuffer>> getConnection() {
|
||||||
|
|
||||||
|
if (state.get() == State.CLOSED) {
|
||||||
|
throw new IllegalStateException("Connection is closed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<StatefulConnection<ByteBuffer, ByteBuffer>> connection = this.connection;
|
||||||
|
|
||||||
|
if (connection != null) {
|
||||||
|
return Mono.fromCompletionStage(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.compareAndSet(State.INITIAL, State.CONNECTION_REQUESTED)) {
|
||||||
|
this.connection = connectionPublisher.toFuture();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
|
||||||
|
connection = this.connection;
|
||||||
|
if (connection != null) {
|
||||||
|
return Mono.fromCompletionStage(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread thread = Thread.currentThread();
|
||||||
|
if (thread.isInterrupted()) {
|
||||||
|
thread.interrupt();
|
||||||
|
return Mono.error(new InterruptedException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close connection (blocking call).
|
||||||
|
*/
|
||||||
|
void close() {
|
||||||
|
|
||||||
|
if (state.compareAndSet(State.CONNECTION_REQUESTED, State.CLOSING)) {
|
||||||
|
getConnection().doOnSuccess(connectionProvider::release).doOnSuccess(it -> state.set(State.CLOSED)).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.compareAndSet(State.INITIAL, State.CLOSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
INITIAL, CONNECTION_REQUESTED, CLOSING, CLOSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ public abstract class LettuceReactiveClusterCommandsTestsBase {
|
|||||||
assumeThat(clientProvider.test(), is(true));
|
assumeThat(clientProvider.test(), is(true));
|
||||||
nativeCommands = clientProvider.getClient().connect().sync();
|
nativeCommands = clientProvider.getClient().connect().sync();
|
||||||
connection = new LettuceReactiveRedisClusterConnection(
|
connection = new LettuceReactiveRedisClusterConnection(
|
||||||
new ClusterConnectionProvider(clientProvider.getClient(), LettuceReactiveRedisConnection.CODEC));
|
new ClusterConnectionProvider(clientProvider.getClient(), LettuceReactiveRedisConnection.CODEC),
|
||||||
|
clientProvider.getClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|||||||
@@ -119,8 +119,10 @@ public abstract class LettuceReactiveCommandsTestsBase {
|
|||||||
this.connection = new LettuceReactiveRedisConnection(connectionProvider);
|
this.connection = new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
ClusterConnectionProvider clusterConnectionProvider = (ClusterConnectionProvider) nativeConnectionProvider;
|
||||||
nativeCommands = nativeConnectionProvider.getConnection(StatefulRedisClusterConnection.class).sync();
|
nativeCommands = nativeConnectionProvider.getConnection(StatefulRedisClusterConnection.class).sync();
|
||||||
this.connection = new LettuceReactiveRedisClusterConnection(connectionProvider);
|
this.connection = new LettuceReactiveRedisClusterConnection(connectionProvider,
|
||||||
|
clusterConnectionProvider.getClient());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 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
|
||||||
|
*
|
||||||
|
* 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.springframework.data.redis.connection.lettuce;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import io.lettuce.core.RedisConnectionException;
|
||||||
|
import io.lettuce.core.api.StatefulConnection;
|
||||||
|
import io.lettuce.core.api.StatefulRedisConnection;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import org.springframework.data.redis.RedisConnectionFailureException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link LettuceReactiveRedisConnection}.
|
||||||
|
*
|
||||||
|
* @author Mark Paluch
|
||||||
|
*/
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class LettuceReactiveRedisConnectionUnitTests {
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_MOCKS) StatefulRedisConnection<ByteBuffer, ByteBuffer> sharedConnection;
|
||||||
|
|
||||||
|
@Mock LettuceConnectionProvider connectionProvider;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
when(connectionProvider.getConnection(any())).thenReturn(sharedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldLazilyInitializeConnection() {
|
||||||
|
|
||||||
|
new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
|
||||||
|
verifyZeroInteractions(connectionProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldExecuteUsingConnectionProvider() {
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
|
||||||
|
StepVerifier.create(connection.execute(cmd -> Mono.just("foo"))).expectNext("foo").verifyComplete();
|
||||||
|
|
||||||
|
verify(connectionProvider).getConnection(StatefulConnection.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldExecuteDedicatedUsingConnectionProvider() {
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
|
||||||
|
StepVerifier.create(connection.executeDedicated(cmd -> Mono.just("foo"))).expectNext("foo").verifyComplete();
|
||||||
|
|
||||||
|
verify(connectionProvider).getConnection(StatefulConnection.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldExecuteOnSharedConnection() {
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(sharedConnection,
|
||||||
|
connectionProvider);
|
||||||
|
|
||||||
|
StepVerifier.create(connection.execute(cmd -> Mono.just("foo"))).expectNext("foo").verifyComplete();
|
||||||
|
|
||||||
|
verifyZeroInteractions(connectionProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldExecuteDedicatedWithSharedConnection() {
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(sharedConnection,
|
||||||
|
connectionProvider);
|
||||||
|
|
||||||
|
StepVerifier.create(connection.executeDedicated(cmd -> Mono.just("foo"))).expectNext("foo").verifyComplete();
|
||||||
|
|
||||||
|
verify(connectionProvider).getConnection(StatefulConnection.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldOperateOnDedicatedConnection() {
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
|
||||||
|
StepVerifier.create(connection.getConnection()).expectNextCount(1).verifyComplete();
|
||||||
|
|
||||||
|
verify(connectionProvider).getConnection(StatefulConnection.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldCloseOnlyDedicatedConnection() {
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(sharedConnection,
|
||||||
|
connectionProvider);
|
||||||
|
|
||||||
|
StepVerifier.create(connection.getConnection()).expectNextCount(1).verifyComplete();
|
||||||
|
StepVerifier.create(connection.getDedicatedConnection()).expectNextCount(1).verifyComplete();
|
||||||
|
|
||||||
|
connection.close();
|
||||||
|
|
||||||
|
verify(sharedConnection, never()).close();
|
||||||
|
verify(connectionProvider, times(1)).release(sharedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldCloseConnectionOnlyOnce() {
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
|
||||||
|
StepVerifier.create(connection.getConnection()).expectNextCount(1).verifyComplete();
|
||||||
|
|
||||||
|
connection.close();
|
||||||
|
connection.close();
|
||||||
|
|
||||||
|
verify(connectionProvider, times(1)).release(sharedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void multipleCallsInProgressShouldConnectOnlyOnce() throws Exception {
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
reset(connectionProvider);
|
||||||
|
when(connectionProvider.getConnection(any())).thenAnswer(invocation -> {
|
||||||
|
|
||||||
|
latch.await();
|
||||||
|
return sharedConnection;
|
||||||
|
});
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
|
||||||
|
CompletableFuture<StatefulConnection<ByteBuffer, ByteBuffer>> first = (CompletableFuture) connection.getConnection()
|
||||||
|
.toFuture();
|
||||||
|
CompletableFuture<StatefulConnection<ByteBuffer, ByteBuffer>> second = (CompletableFuture) connection
|
||||||
|
.getConnection().toFuture();
|
||||||
|
|
||||||
|
assertThat(first).isNotDone();
|
||||||
|
assertThat(second).isNotDone();
|
||||||
|
|
||||||
|
verify(connectionProvider, times(1)).getConnection(StatefulConnection.class);
|
||||||
|
|
||||||
|
latch.countDown();
|
||||||
|
|
||||||
|
first.get(10, TimeUnit.SECONDS);
|
||||||
|
second.get(10, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertThat(first).isCompletedWithValue(sharedConnection);
|
||||||
|
assertThat(second).isCompletedWithValue(sharedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // DATAREDIS-720
|
||||||
|
public void shouldPropagateConnectionFailures() {
|
||||||
|
|
||||||
|
reset(connectionProvider);
|
||||||
|
when(connectionProvider.getConnection(any())).thenThrow(new RedisConnectionException("something went wrong"));
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
|
||||||
|
StepVerifier.create(connection.getConnection()).expectError(RedisConnectionFailureException.class).verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException.class) // DATAREDIS-720
|
||||||
|
public void shouldRejectCommandsAfterClose() {
|
||||||
|
|
||||||
|
LettuceReactiveRedisConnection connection = new LettuceReactiveRedisConnection(connectionProvider);
|
||||||
|
connection.close();
|
||||||
|
|
||||||
|
connection.getConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user