org.redisson.command.RedisExecutor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of redisson-all Show documentation
Show all versions of redisson-all Show documentation
Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava3/Reactive API. Client side caching. Over 50 Redis based Java objects and services: JCache API, Apache Tomcat, Hibernate, Spring, Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Scheduler, RPC
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* 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.redisson.command;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import io.netty.util.concurrent.FutureListener;
import org.redisson.RedissonReference;
import org.redisson.RedissonShutdownException;
import org.redisson.ScanResult;
import org.redisson.api.RFuture;
import org.redisson.cache.LRUCacheMap;
import org.redisson.client.*;
import org.redisson.client.codec.BaseCodec;
import org.redisson.client.codec.Codec;
import org.redisson.client.protocol.*;
import org.redisson.client.protocol.decoder.ListScanResult;
import org.redisson.client.protocol.decoder.MapScanResult;
import org.redisson.connection.ConnectionManager;
import org.redisson.connection.NodeSource;
import org.redisson.connection.NodeSource.Redirect;
import org.redisson.liveobject.core.RedissonObjectBuilder;
import org.redisson.misc.LogHelper;
import org.redisson.misc.RPromise;
import org.redisson.misc.RedissonPromise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
/**
*
* @author Nikita Koksharov
*
* @param type of value
* @param type of returned value
*/
@SuppressWarnings({"NestedIfDepth"})
public class RedisExecutor {
static final Logger log = LoggerFactory.getLogger(RedisExecutor.class);
final boolean readOnlyMode;
final RedisCommand command;
final Object[] params;
final RPromise mainPromise;
final boolean ignoreRedirect;
final RedissonObjectBuilder objectBuilder;
final ConnectionManager connectionManager;
RFuture connectionFuture;
NodeSource source;
Codec codec;
volatile int attempt;
volatile Timeout timeout;
volatile BiConsumer mainPromiseListener;
volatile ChannelFuture writeFuture;
volatile RedisException exception;
int attempts;
long retryInterval;
long responseTimeout;
public RedisExecutor(boolean readOnlyMode, NodeSource source, Codec codec, RedisCommand command,
Object[] params, RPromise mainPromise, boolean ignoreRedirect,
ConnectionManager connectionManager, RedissonObjectBuilder objectBuilder) {
super();
this.readOnlyMode = readOnlyMode;
this.source = source;
this.codec = codec;
this.command = command;
this.params = params;
this.mainPromise = mainPromise;
this.ignoreRedirect = ignoreRedirect;
this.connectionManager = connectionManager;
this.objectBuilder = objectBuilder;
this.attempts = connectionManager.getConfig().getRetryAttempts();
this.retryInterval = connectionManager.getConfig().getRetryInterval();
this.responseTimeout = connectionManager.getConfig().getTimeout();
}
public void execute() {
if (mainPromise.isCancelled()) {
free();
return;
}
if (!connectionManager.getShutdownLatch().acquire()) {
free();
mainPromise.tryFailure(new RedissonShutdownException("Redisson is shutdown"));
return;
}
codec = getCodec(codec);
RFuture connectionFuture = getConnection();
RPromise attemptPromise = new RedissonPromise();
mainPromiseListener = (r, e) -> {
if (mainPromise.isCancelled() && connectionFuture.cancel(false)) {
log.debug("Connection obtaining canceled for {}", command);
timeout.cancel();
if (attemptPromise.cancel(false)) {
free();
}
}
};
if (attempt == 0) {
mainPromise.onComplete((r, e) -> {
if (this.mainPromiseListener != null) {
this.mainPromiseListener.accept(r, e);
}
});
}
scheduleRetryTimeout(connectionFuture, attemptPromise);
connectionFuture.onComplete((connection, e) -> {
if (connectionFuture.isCancelled()) {
connectionManager.getShutdownLatch().release();
return;
}
if (!connectionFuture.isSuccess()) {
connectionManager.getShutdownLatch().release();
exception = convertException(connectionFuture);
return;
}
sendCommand(attemptPromise, connection);
writeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
checkWriteFuture(writeFuture, attemptPromise, connection);
}
});
});
attemptPromise.onComplete((r, e) -> {
releaseConnection(attemptPromise, connectionFuture);
checkAttemptPromise(attemptPromise, connectionFuture);
});
}
private void scheduleRetryTimeout(RFuture connectionFuture, RPromise attemptPromise) {
TimerTask retryTimerTask = new TimerTask() {
@Override
public void run(Timeout t) throws Exception {
if (attemptPromise.isDone()) {
return;
}
if (connectionFuture.cancel(false)) {
if (exception == null) {
exception = new RedisTimeoutException("Unable to acquire connection! " +
"Avoid to use blocking commands in Async/JavaRx/Reactive handlers. " +
"Try to increase connection pool size. "
+ "Node source: " + source
+ ", command: " + LogHelper.toString(command, params)
+ " after " + attempt + " retry attempts");
}
} else {
if (connectionFuture.isSuccess()) {
if (writeFuture == null || !writeFuture.isDone()) {
if (attempt == attempts) {
if (writeFuture != null && writeFuture.cancel(false)) {
if (exception == null) {
long totalSize = 0;
if (params != null) {
for (Object param : params) {
if (param instanceof ByteBuf) {
totalSize += ((ByteBuf) param).readableBytes();
}
}
}
exception = new RedisTimeoutException("Command still hasn't been written into connection! " +
"Avoid to use blocking commands in Async/JavaRx/Reactive handlers. " +
"Try to increase nettyThreads setting. Payload size in bytes: " + totalSize
+ ". Node source: " + source + ", connection: " + connectionFuture.getNow()
+ ", command: " + LogHelper.toString(command, params)
+ " after " + attempt + " retry attempts");
}
attemptPromise.tryFailure(exception);
}
return;
}
attempt++;
scheduleRetryTimeout(connectionFuture, attemptPromise);
return;
}
if (writeFuture.isSuccess()) {
return;
}
}
}
if (mainPromise.isCancelled()) {
if (attemptPromise.cancel(false)) {
free();
}
return;
}
if (attempt == attempts) {
// filled out in connectionFuture or writeFuture handler
attemptPromise.tryFailure(exception);
return;
}
if (!attemptPromise.cancel(false)) {
return;
}
attempt++;
if (log.isDebugEnabled()) {
log.debug("attempt {} for command {} and params {}",
attempt, command, LogHelper.toString(params));
}
mainPromiseListener = null;
execute();
}
};
timeout = connectionManager.newTimeout(retryTimerTask, retryInterval, TimeUnit.MILLISECONDS);
}
protected void free() {
free(params);
}
protected void free(Object[] params) {
for (Object obj : params) {
ReferenceCountUtil.safeRelease(obj);
}
}
private void checkWriteFuture(ChannelFuture future, RPromise attemptPromise, RedisConnection connection) {
if (future.isCancelled() || attemptPromise.isDone()) {
return;
}
if (!future.isSuccess()) {
exception = new WriteRedisConnectionException(
"Unable to write command into connection! Node source: " + source + ", connection: " + connection +
", command: " + LogHelper.toString(command, params)
+ " after " + attempt + " retry attempts", future.cause());
if (attempt == attempts) {
attemptPromise.tryFailure(exception);
}
return;
}
timeout.cancel();
scheduleResponseTimeout(attemptPromise, connection);
}
private void scheduleResponseTimeout(RPromise attemptPromise, RedisConnection connection) {
long timeoutTime = responseTimeout;
if (command != null
&& (RedisCommands.BLOCKING_COMMAND_NAMES.contains(command.getName())
|| RedisCommands.BLOCKING_COMMANDS.contains(command))) {
Long popTimeout = null;
if (RedisCommands.BLOCKING_COMMANDS.contains(command)) {
for (int i = 0; i < params.length-1; i++) {
if ("BLOCK".equals(params[i])) {
popTimeout = Long.valueOf(params[i+1].toString()) / 1000;
break;
}
}
} else {
popTimeout = Long.valueOf(params[params.length - 1].toString());
}
handleBlockingOperations(attemptPromise, connection, popTimeout);
if (popTimeout == 0) {
return;
}
timeoutTime += popTimeout * 1000;
// add 1 second due to issue https://github.com/antirez/redis/issues/874
timeoutTime += 1000;
}
long timeoutAmount = timeoutTime;
TimerTask timeoutTask = new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
if (isResendAllowed(attempt, attempts)) {
if (!attemptPromise.cancel(false)) {
return;
}
attempt++;
if (log.isDebugEnabled()) {
log.debug("attempt {} for command {} and params {}",
attempt, command, LogHelper.toString(params));
}
mainPromiseListener = null;
execute();
return;
}
attemptPromise.tryFailure(
new RedisResponseTimeoutException("Redis server response timeout (" + timeoutAmount + " ms) occured"
+ " after " + attempt + " retry attempts. Increase nettyThreads and/or timeout settings. Try to define pingConnectionInterval setting. Command: "
+ LogHelper.toString(command, params) + ", channel: " + connection.getChannel()));
}
};
timeout = connectionManager.newTimeout(timeoutTask, timeoutTime, TimeUnit.MILLISECONDS);
}
protected boolean isResendAllowed(int attempt, int attempts) {
return attempt < attempts;
}
private void handleBlockingOperations(RPromise attemptPromise, RedisConnection connection, Long popTimeout) {
FutureListener listener = f -> {
mainPromise.tryFailure(new RedissonShutdownException("Redisson is shutdown"));
};
Timeout scheduledFuture;
if (popTimeout != 0) {
// handling cases when connection has been lost
scheduledFuture = connectionManager.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
if (attemptPromise.trySuccess(null)) {
connection.forceFastReconnectAsync();
}
}
}, popTimeout + 1, TimeUnit.SECONDS);
} else {
scheduledFuture = null;
}
mainPromise.onComplete((res, e) -> {
if (scheduledFuture != null) {
scheduledFuture.cancel();
}
synchronized (listener) {
connectionManager.getShutdownPromise().removeListener(listener);
}
// handling cancel operation for blocking commands
if ((mainPromise.isCancelled()
|| mainPromise.cause() instanceof InterruptedException)
&& !attemptPromise.isDone()) {
log.debug("Canceled blocking operation {} used {}", command, connection);
connection.forceFastReconnectAsync().onComplete((r, ex) -> {
attemptPromise.cancel(true);
});
return;
}
if (e instanceof RedissonShutdownException) {
attemptPromise.tryFailure(e);
}
});
synchronized (listener) {
if (!mainPromise.isDone()) {
connectionManager.getShutdownPromise().addListener(listener);
}
}
}
protected void checkAttemptPromise(RPromise attemptFuture, RFuture connectionFuture) {
timeout.cancel();
if (attemptFuture.isCancelled()) {
return;
}
try {
mainPromiseListener = null;
if (attemptFuture.cause() instanceof RedisMovedException && !ignoreRedirect) {
RedisMovedException ex = (RedisMovedException) attemptFuture.cause();
if (source.getRedirect() == Redirect.MOVED) {
mainPromise.tryFailure(new RedisException("MOVED redirection loop detected. Node " + source.getAddr() + " has further redirect to " + ex.getUrl()));
return;
}
onException();
source = new NodeSource(ex.getSlot(), connectionManager.applyNatMap(ex.getUrl()), Redirect.MOVED);
execute();
return;
}
if (attemptFuture.cause() instanceof RedisAskException && !ignoreRedirect) {
RedisAskException ex = (RedisAskException) attemptFuture.cause();
onException();
source = new NodeSource(ex.getSlot(), connectionManager.applyNatMap(ex.getUrl()), Redirect.ASK);
execute();
return;
}
if (attemptFuture.cause() instanceof RedisLoadingException
|| attemptFuture.cause() instanceof RedisTryAgainException
|| attemptFuture.cause() instanceof RedisClusterDownException
|| attemptFuture.cause() instanceof RedisBusyException) {
if (attempt < attempts) {
onException();
connectionManager.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
attempt++;
execute();
}
}, Math.min(responseTimeout, 1000), TimeUnit.MILLISECONDS);
return;
}
}
free();
handleResult(attemptFuture, connectionFuture);
} catch (Exception e) {
handleError(connectionFuture, e);
}
}
protected void handleResult(RPromise attemptPromise, RFuture connectionFuture) throws ReflectiveOperationException {
if (attemptPromise.isSuccess()) {
R res = attemptPromise.getNow();
if (res instanceof ScanResult) {
((ScanResult) res).setRedisClient(connectionFuture.getNow().getRedisClient());
}
handleSuccess(mainPromise, connectionFuture, res);
} else {
handleError(connectionFuture, attemptPromise.cause());
}
}
protected void onException() {
}
protected void handleError(RFuture connectionFuture, Throwable cause) {
mainPromise.tryFailure(cause);
}
protected void handleSuccess(RPromise promise, RFuture connectionFuture, R res) throws ReflectiveOperationException {
if (objectBuilder != null) {
handleReference(promise, res);
} else {
promise.trySuccess(res);
}
}
private void handleReference(RPromise promise, R res) throws ReflectiveOperationException {
promise.trySuccess((R) tryHandleReference(objectBuilder, res));
}
public static Object tryHandleReference(RedissonObjectBuilder objectBuilder, Object o) throws ReflectiveOperationException {
boolean hasConversion = false;
if (o instanceof List) {
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy