io.vertx.redis.client.impl.RedisClusterClient Maven / Gradle / Ivy
/*
* Copyright 2019 Red Hat, Inc.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.vertx.redis.client.impl;
import io.vertx.core.*;
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.LoggerFactory;
import io.vertx.redis.client.*;
import io.vertx.redis.client.impl.types.MultiType;
import io.vertx.redis.client.impl.types.NumberType;
import io.vertx.redis.client.impl.types.SimpleStringType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import static io.vertx.redis.client.Command.*;
import static io.vertx.redis.client.Request.cmd;
public class RedisClusterClient extends BaseRedisClient implements Redis {
private static final Logger LOG = LoggerFactory.getLogger(RedisClusterClient.class);
public static void addReducer(Command command, Function, Response> fn) {
RedisClusterConnection.addReducer(command, fn);
}
public static void addUnSupportedCommand(Command command, String error) {
RedisClusterConnection.addUnSupportedCommand(command, error);
}
public static void addMasterOnlyCommand(Command command) {
RedisClusterConnection.addMasterOnlyCommand(command);
}
static {
// provided reducers
addReducer(MSET, list ->
// Simple string reply: always OK since MSET can't fail.
SimpleStringType.OK);
addReducer(DEL, list ->
NumberType.create(list.stream()
.mapToLong(el -> {
Long l = el.toLong();
if (l == null) {
return 0L;
} else {
return l;
}
}).sum()));
addReducer(MGET, list -> {
int total = 0;
for (Response resp : list) {
total += resp.size();
}
MultiType multi = MultiType.create(total, false);
for (Response resp : list) {
for (Response child : resp) {
multi.add(child);
}
}
return multi;
});
addReducer(KEYS, list -> {
int total = 0;
for (Response resp : list) {
total += resp.size();
}
MultiType multi = MultiType.create(total, false);
for (Response resp : list) {
for (Response child : resp) {
multi.add(child);
}
}
return multi;
});
addReducer(FLUSHDB, list ->
// Simple string reply: always OK since FLUSHDB can't fail.
SimpleStringType.OK);
addReducer(DBSIZE, list ->
// Sum of key numbers on all Key Slots
NumberType.create(list.stream()
.mapToLong(el -> {
Long l = el.toLong();
if (l == null) {
return 0L;
} else {
return l;
}
}).sum()));
Arrays.asList(ASKING, AUTH, BGREWRITEAOF, BGSAVE, CLIENT, COMMAND, CONFIG,
DEBUG, DISCARD, HOST, INFO, LASTSAVE, LATENCY, LOLWUT, MEMORY, MODULE, MONITOR, PFDEBUG, PFSELFTEST,
PING, READONLY, READWRITE, REPLCONF, REPLICAOF, ROLE, SAVE, SCAN, SCRIPT, SELECT, SHUTDOWN, SLAVEOF, SLOWLOG, SWAPDB,
SYNC, SENTINEL).forEach(command -> addUnSupportedCommand(command, null));
addUnSupportedCommand(FLUSHALL, "RedisClusterClient does not handle command FLUSHALL, use FLUSHDB");
addMasterOnlyCommand(WAIT);
addMasterOnlyCommand(SUBSCRIBE);
addMasterOnlyCommand(PSUBSCRIBE);
addReducer(UNSUBSCRIBE, list -> SimpleStringType.OK);
addReducer(PUNSUBSCRIBE, list -> SimpleStringType.OK);
}
private final RedisOptions options;
public RedisClusterClient(Vertx vertx, RedisOptions options) {
super(vertx, options);
this.options = options;
// validate options
if (options.getMaxPoolWaiting() < options.getMaxPoolSize()) {
throw new IllegalStateException("Invalid options: maxPoolWaiting < maxPoolSize");
// we can't validate the max pool size yet as we need to know the slots first
// the remaining validation will happen at the connect time
}
}
@Override
public Future connect() {
final Promise promise = vertx.promise();
// attempt to load the slots from the first good endpoint
connect(options.getEndpoints(), 0, promise);
return promise.future();
}
private void connect(List endpoints, int index, Handler> onConnect) {
if (index >= endpoints.size()) {
// stop condition
onConnect.handle(Future.failedFuture("Cannot connect to any of the provided endpoints"));
return;
}
connectionManager.getConnection(endpoints.get(index), RedisReplicas.NEVER != options.getUseReplicas() ? cmd(READONLY) : null)
.onFailure(err -> {
// failed try with the next endpoint
connect(endpoints, index + 1, onConnect);
})
.onSuccess(conn -> {
// fetch slots from the cluster immediately to ensure slots are correct
getSlots(endpoints.get(index), conn, getSlots -> {
if (getSlots.failed()) {
// the slots command failed.
conn.close().onFailure(LOG::warn);
// try with the next one
connect(endpoints, index + 1, onConnect);
return;
}
// slots are loaded (this connection isn't needed anymore)
conn.close().onFailure(LOG::warn);
// create a cluster connection
final Slots slots = getSlots.result();
final AtomicBoolean failed = new AtomicBoolean(false);
final AtomicInteger counter = new AtomicInteger();
final Map connections = new HashMap<>();
// validate if the pool config is valid
final int totalUniqueEndpoints = slots.endpoints().length;
if (options.getMaxPoolSize() < totalUniqueEndpoints) {
// this isn't a valid setup, the connection pool will not accommodate all the required connections
onConnect.handle(Future.failedFuture("RedisOptions maxPoolSize < Cluster size(" + totalUniqueEndpoints + "): The pool is not able to hold all required connections!"));
return;
}
for (String endpoint : slots.endpoints()) {
connectionManager.getConnection(endpoint, RedisReplicas.NEVER != options.getUseReplicas() ? cmd(READONLY) : null)
.onFailure(err -> {
// failed try with the next endpoint
failed.set(true);
connectionComplete(counter, slots, connections, failed, onConnect);
})
.onSuccess(cconn -> {
// there can be concurrent access to the connection map
// since this is a one time operation we can pay the penalty of
// synchronizing on each write (hopefully is only a few writes)
synchronized (connections) {
connections.put(endpoint, cconn);
}
connectionComplete(counter, slots, connections, failed, onConnect);
});
}
});
});
}
private void connectionComplete(AtomicInteger counter, Slots slots, Map connections, AtomicBoolean failed, Handler> onConnect) {
if (counter.incrementAndGet() == slots.endpoints().length) {
// end condition
if (failed.get()) {
// cleanup
// during an error we lock the map because we will change it
// probably this isn't an issue as no more write should happen anyway
synchronized (connections) {
for (RedisConnection value : connections.values()) {
if (value != null) {
value.close().onFailure(LOG::warn);
}
}
}
// return
onConnect.handle(Future.failedFuture("Failed to connect to all nodes of the cluster"));
} else {
onConnect.handle(Future.succeededFuture(new RedisClusterConnection(vertx, options, slots, connections)));
}
}
}
private void getSlots(String endpoint, RedisConnection conn, Handler> onGetSlots) {
conn.send(cmd(CLUSTER).arg("SLOTS"), send -> {
if (send.failed()) {
// failed to load the slots from this connection
onGetSlots.handle(Future.failedFuture(send.cause()));
return;
}
final Response reply = send.result();
if (reply == null || reply.size() == 0) {
// no slots available we can't really proceed
onGetSlots.handle(Future.failedFuture("SLOTS No slots available in the cluster."));
return;
}
onGetSlots.handle(Future.succeededFuture(new Slots(endpoint, reply)));
});
}
}