package com.lambdaworks.redis.cluster;
import static com.lambdaworks.redis.cluster.SlotHash.getSlot;
import com.lambdaworks.redis.*;
import com.lambdaworks.redis.api.StatefulRedisConnection;
import com.lambdaworks.redis.cluster.models.partitions.Partitions;
import com.lambdaworks.redis.internal.HostAndPort;
import com.lambdaworks.redis.internal.LettuceAssert;
import com.lambdaworks.redis.protocol.CommandArgs;
import com.lambdaworks.redis.protocol.CommandKeyword;
import com.lambdaworks.redis.protocol.ProtocolKeyword;
import com.lambdaworks.redis.protocol.RedisCommand;
import io.netty.util.concurrent.EventExecutorGroup;
/**
* Channel writer for cluster operation. This writer looks up the right partition by hash/slot for the operation.
*
* @param <K> Key type.
* @param <V> Value type.
* @author Mark Paluch
* @since 3.0
*/
class ClusterDistributionChannelWriter<K, V> implements RedisChannelWriter<K, V> {
private final RedisChannelWriter<K, V> defaultWriter;
private final ClusterEventListener clusterEventListener;
private final EventExecutorGroup eventExecutors;
private final int executionLimit;
private ClusterConnectionProvider clusterConnectionProvider;
private boolean closed = false;
ClusterDistributionChannelWriter(ClientOptions clientOptions, RedisChannelWriter<K, V> defaultWriter,
ClusterEventListener clusterEventListener, EventExecutorGroup eventExecutors) {
if (clientOptions instanceof ClusterClientOptions) {
this.executionLimit = ((ClusterClientOptions) clientOptions).getMaxRedirects();
} else {
this.executionLimit = 5;
}
this.defaultWriter = defaultWriter;
this.clusterEventListener = clusterEventListener;
this.eventExecutors = eventExecutors;
}
@Override
@SuppressWarnings("unchecked")
public <T, C extends RedisCommand<K, V, T>> C write(C command) {
LettuceAssert.notNull(command, "Command must not be null");
if (closed) {
throw new RedisException("Connection is closed");
}
RedisCommand<K, V, T> commandToSend = command;
CommandArgs<K, V> args = command.getArgs();
if (!(command instanceof ClusterCommand)) {
commandToSend = new ClusterCommand<>(command, this, executionLimit);
}
if (commandToSend instanceof ClusterCommand && !commandToSend.isDone()) {
ClusterCommand<K, V, T> clusterCommand = (ClusterCommand<K, V, T>) commandToSend;
if (clusterCommand.isMoved() || clusterCommand.isAsk()) {
HostAndPort target;
boolean asking;
if (clusterCommand.isMoved()) {
target = getMoveTarget(clusterCommand.getError());
clusterEventListener.onMovedRedirection();
asking = false;
} else {
target = getAskTarget(clusterCommand.getError());
asking = true;
clusterEventListener.onAskRedirection();
}
commandToSend.getOutput().setError((String) null);
eventExecutors.submit(() -> {
try {
RedisChannelHandler<K, V> connection = (RedisChannelHandler<K, V>) clusterConnectionProvider
.getConnection(ClusterConnectionProvider.Intent.WRITE, target.getHostText(), target.getPort());
if (asking) {
// set asking bit
StatefulRedisConnection<K, V> statefulRedisConnection = (StatefulRedisConnection<K, V>) connection;
statefulRedisConnection.async().asking();
}
connection.getChannelWriter().write(command);
} catch (Exception e) {
command.completeExceptionally(e);
}
});
return command;
}
}
RedisChannelWriter<K, V> channelWriter = null;
if (args != null && args.getFirstEncodedKey() != null) {
int hash = getSlot(args.getFirstEncodedKey());
ClusterConnectionProvider.Intent intent = getIntent(command.getType());
RedisChannelHandler<K, V> connection = (RedisChannelHandler<K, V>) clusterConnectionProvider.getConnection(intent,
hash);
channelWriter = connection.getChannelWriter();
}
if (channelWriter instanceof ClusterDistributionChannelWriter) {
ClusterDistributionChannelWriter<K, V> writer = (ClusterDistributionChannelWriter<K, V>) channelWriter;
channelWriter = writer.defaultWriter;
}
if (command.getOutput() != null) {
commandToSend.getOutput().setError((String) null);
}
if (channelWriter != null && channelWriter != this && channelWriter != defaultWriter) {
return channelWriter.write((C) commandToSend);
}
defaultWriter.write((C) commandToSend);
return command;
}
private ClusterConnectionProvider.Intent getIntent(ProtocolKeyword type) {
for (ProtocolKeyword readOnlyCommand : ReadOnlyCommands.READ_ONLY_COMMANDS) {
if (readOnlyCommand == type) {
return ClusterConnectionProvider.Intent.READ;
}
}
return ClusterConnectionProvider.Intent.WRITE;
}
static HostAndPort getMoveTarget(String errorMessage) {
LettuceAssert.notEmpty(errorMessage, "ErrorMessage must not be empty");
LettuceAssert.isTrue(errorMessage.startsWith(CommandKeyword.MOVED.name()),
"ErrorMessage must start with " + CommandKeyword.MOVED);
String[] movedMessageParts = errorMessage.split(" ");
LettuceAssert.isTrue(movedMessageParts.length >= 3, "ErrorMessage must consist of 3 tokens (" + errorMessage + ")");
return HostAndPort.parseCompat(movedMessageParts[2]);
}
static HostAndPort getAskTarget(String errorMessage) {
LettuceAssert.notEmpty(errorMessage, "ErrorMessage must not be empty");
LettuceAssert.isTrue(errorMessage.startsWith(CommandKeyword.ASK.name()),
"ErrorMessage must start with " + CommandKeyword.ASK);
String[] movedMessageParts = errorMessage.split(" ");
LettuceAssert.isTrue(movedMessageParts.length >= 3, "ErrorMessage must consist of 3 tokens (" + errorMessage + ")");
return HostAndPort.parseCompat(movedMessageParts[2]);
}
@Override
public void close() {
if (closed) {
return;
}
closed = true;
if (defaultWriter != null) {
defaultWriter.close();
}
if (clusterConnectionProvider != null) {
clusterConnectionProvider.close();
clusterConnectionProvider = null;
}
}
@Override
public void setRedisChannelHandler(RedisChannelHandler<K, V> redisChannelHandler) {
defaultWriter.setRedisChannelHandler(redisChannelHandler);
}
@Override
public void setAutoFlushCommands(boolean autoFlush) {
getClusterConnectionProvider().setAutoFlushCommands(autoFlush);
}
@Override
public void flushCommands() {
getClusterConnectionProvider().flushCommands();
}
public ClusterConnectionProvider getClusterConnectionProvider() {
return clusterConnectionProvider;
}
@Override
public void reset() {
defaultWriter.reset();
clusterConnectionProvider.reset();
}
public void setClusterConnectionProvider(ClusterConnectionProvider clusterConnectionProvider) {
this.clusterConnectionProvider = clusterConnectionProvider;
}
public void setPartitions(Partitions partitions) {
if (clusterConnectionProvider != null) {
clusterConnectionProvider.setPartitions(partitions);
}
}
/**
* Set from which nodes data is read. The setting is used as default for read operations on this connection. See the
* documentation for {@link ReadFrom} for more information.
*
* @param readFrom the read from setting, must not be {@literal null}
*/
public void setReadFrom(ReadFrom readFrom) {
clusterConnectionProvider.setReadFrom(readFrom);
}
/**
* Gets the {@link ReadFrom} setting for this connection. Defaults to {@link ReadFrom#MASTER} if not set.
*
* @return the read from setting
*/
public ReadFrom getReadFrom() {
return clusterConnectionProvider.getReadFrom();
}
}