All Downloads are FREE. Search and download functionalities are using the official Maven repository.

redis.clients.jedis.executors.ClusterCommandExecutor Maven / Gradle / Ivy

There is a newer version: 5.2.0
Show newest version
package redis.clients.jedis.executors;

import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import redis.clients.jedis.CommandObject;
import redis.clients.jedis.Connection;
import redis.clients.jedis.ConnectionPool;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.exceptions.*;
import redis.clients.jedis.providers.ClusterConnectionProvider;
import redis.clients.jedis.util.IOUtils;

public class ClusterCommandExecutor implements CommandExecutor {

  private final Logger log = LoggerFactory.getLogger(getClass());

  public final ClusterConnectionProvider provider;
  protected final int maxAttempts;
  protected final Duration maxTotalRetriesDuration;

  public ClusterCommandExecutor(ClusterConnectionProvider provider, int maxAttempts,
      Duration maxTotalRetriesDuration) {
    this.provider = provider;
    this.maxAttempts = maxAttempts;
    this.maxTotalRetriesDuration = maxTotalRetriesDuration;
  }

  @Override
  public void close() {
    this.provider.close();
  }

  @Override
  public final  T broadcastCommand(CommandObject commandObject) {
    Map connectionMap = provider.getConnectionMap();

    boolean isErrored = false;
    T reply = null;
    JedisBroadcastException bcastError = new JedisBroadcastException();
    for (Map.Entry entry : connectionMap.entrySet()) {
      HostAndPort node = HostAndPort.from(entry.getKey());
      ConnectionPool pool = entry.getValue();
      try (Connection connection = pool.getResource()) {
        T aReply = execute(connection, commandObject);
        bcastError.addReply(node, aReply);
        if (isErrored) { // already errored
        } else if (reply == null) {
          reply = aReply; // ok
        } else if (reply.equals(aReply)) {
          // ok
        } else {
          isErrored = true;
          reply = null;
        }
      } catch (Exception anError) {
        bcastError.addReply(node, anError);
        isErrored = true;
      }
    }
    if (isErrored) {
      throw bcastError;
    }
    return reply;
  }

  @Override
  public final  T executeCommand(CommandObject commandObject) {
    Instant deadline = Instant.now().plus(maxTotalRetriesDuration);

    JedisRedirectionException redirect = null;
    int consecutiveConnectionFailures = 0;
    Exception lastException = null;
    for (int attemptsLeft = this.maxAttempts; attemptsLeft > 0; attemptsLeft--) {
      Connection connection = null;
      try {
        if (redirect != null) {
          connection = provider.getConnection(redirect.getTargetNode());
          if (redirect instanceof JedisAskDataException) {
            // TODO: Pipeline asking with the original command to make it faster....
            connection.executeCommand(Protocol.Command.ASKING);
          }
        } else {
          connection = provider.getConnection(commandObject.getArguments());
        }

        return execute(connection, commandObject);

      } catch (JedisClusterOperationException jnrcne) {
        throw jnrcne;
      } catch (JedisConnectionException jce) {
        lastException = jce;
        ++consecutiveConnectionFailures;
        log.debug("Failed connecting to Redis: {}", connection, jce);
        // "- 1" because we just did one, but the attemptsLeft counter hasn't been decremented yet
        boolean reset = handleConnectionProblem(attemptsLeft - 1, consecutiveConnectionFailures, deadline);
        if (reset) {
          consecutiveConnectionFailures = 0;
          redirect = null;
        }
      } catch (JedisRedirectionException jre) {
        // avoid updating lastException if it is a connection exception
        if (lastException == null || lastException instanceof JedisRedirectionException) {
          lastException = jre;
        }
        log.debug("Redirected by server to {}", jre.getTargetNode());
        consecutiveConnectionFailures = 0;
        redirect = jre;
        // if MOVED redirection occurred,
        if (jre instanceof JedisMovedDataException) {
          // it rebuilds cluster's slot cache recommended by Redis cluster specification
          provider.renewSlotCache(connection);
        }
      } finally {
        IOUtils.closeQuietly(connection);
      }
      if (Instant.now().isAfter(deadline)) {
        throw new JedisClusterOperationException("Cluster retry deadline exceeded.");
      }
    }

    JedisClusterOperationException maxAttemptsException
        = new JedisClusterOperationException("No more cluster attempts left.");
    maxAttemptsException.addSuppressed(lastException);
    throw maxAttemptsException;
  }

  /**
   * WARNING: This method is accessible for the purpose of testing.
   * This should not be used or overriden.
   */
  protected  T execute(Connection connection, CommandObject commandObject) {
    return connection.executeCommand(commandObject);
  }

  /**
   * Related values should be reset if TRUE is returned.
   *
   * @param attemptsLeft
   * @param consecutiveConnectionFailures
   * @param doneDeadline
   * @return true - if some actions are taken
   * 
false - if no actions are taken */ private boolean handleConnectionProblem(int attemptsLeft, int consecutiveConnectionFailures, Instant doneDeadline) { if (this.maxAttempts < 3) { // Since we only renew the slots cache after two consecutive connection // failures (see consecutiveConnectionFailures above), we need to special // case the situation where we max out after two or fewer attempts. // Otherwise, on two or fewer max attempts, the slots cache would never be // renewed. if (attemptsLeft == 0) { provider.renewSlotCache(); return true; } return false; } if (consecutiveConnectionFailures < 2) { return false; } sleep(getBackoffSleepMillis(attemptsLeft, doneDeadline)); //We need this because if node is not reachable anymore - we need to finally initiate slots //renewing, or we can stuck with cluster state without one node in opposite case. //TODO make tracking of successful/unsuccessful operations for node - do renewing only //if there were no successful responses from this node last few seconds provider.renewSlotCache(); return true; } private static long getBackoffSleepMillis(int attemptsLeft, Instant deadline) { if (attemptsLeft <= 0) { return 0; } long millisLeft = Duration.between(Instant.now(), deadline).toMillis(); if (millisLeft < 0) { throw new JedisClusterOperationException("Cluster retry deadline exceeded."); } long maxBackOff = millisLeft / (attemptsLeft * attemptsLeft); return ThreadLocalRandom.current().nextLong(maxBackOff + 1); } /** * WARNING: This method is accessible for the purpose of testing. * This should not be used or overriden. */ protected void sleep(long sleepMillis) { try { TimeUnit.MILLISECONDS.sleep(sleepMillis); } catch (InterruptedException e) { throw new JedisClusterOperationException(e); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy