io.vertx.redis.impl.RedisConnection Maven / Gradle / Ivy
/**
* Copyright 2015 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.impl;
import io.vertx.core.AsyncResult;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.impl.VertxInternal;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.SocketAddress;
import io.vertx.redis.RedisOptions;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicReference;
/**
* Base class for Redis Vert.x client. Generated client would use the facilities
* in this class to implement typed commands.
*/
class RedisConnection {
private static final Logger log = LoggerFactory.getLogger(RedisConnection.class);
private final Vertx vertx;
private final Context context;
/**
* there are 2 queues, one for commands not yet sent over the wire to redis and another for commands already sent to
* redis. At start up it expected that until the connection handshake is complete the pending queue will grow and once
* the handshake completes it will be empty while the second one will be in constant movement.
*
* Since the client works **ALWAYS** in pipeline mode the order of adding and removing elements to the queues is
* crucial. A command is sent only when its reply handler or handlers are added to any of the queues and the command
* is send to the wire.
*
* For this reason we must **ALWAYS** synchronize the access to the queues and writes to the socket.
*/
// pending: commands that have not yet been sent to the server
private final Queue> pending = new LinkedList<>();
// waiting: commands that have been sent but not answered
private final Queue> waiting = new LinkedList<>();
private final ReplyParser replyParser;
private final RedisSubscriptions subscriptions;
private final RedisOptions config;
private final AtomicReference state = new AtomicReference<>(State.DISCONNECTED);
// attempt to reconnect on error, by default true
private volatile boolean reconnect = true;
private volatile NetSocket netSocket;
/**
* Create a RedisConnection.
*/
public RedisConnection(Vertx vertx, RedisOptions config, RedisSubscriptions subscriptions) {
// Make sure we have an event loop context for serializability of the commands
Context ctx = Vertx.currentContext();
if (ctx == null) {
ctx = vertx.getOrCreateContext();
} else if (!ctx.isEventLoopContext()) {
VertxInternal vi = (VertxInternal) vertx;
ctx = vi.createEventLoopContext(null, null, new JsonObject(), Thread.currentThread().getContextClassLoader());
}
this.vertx = vertx;
this.context = ctx;
this.config = config;
this.subscriptions = subscriptions;
if (subscriptions != null) {
this.replyParser = new ReplyParser(reply -> {
// Pub/sub messages are always multi-bulk
if (reply.is('*')) {
Reply[] data = (Reply[]) reply.data();
if (data != null) {
// message
if (data.length == 3) {
if (data[0].is('$') && "message".equals(data[0].asType(String.class))) {
String channel = data[1].asType(String.class);
subscriptions.handleChannel(channel, data);
return;
}
}
// pmessage
else if (data.length == 4) {
if (data[0].is('$') && "pmessage".equals(data[0].asType(String.class))) {
String pattern = data[1].asType(String.class);
subscriptions.handlePattern(pattern, data);
return;
}
}
}
}
// fallback to normal handler
handleReply(reply);
});
} else {
this.replyParser = new ReplyParser(this::handleReply);
}
}
private boolean useSentinel() {
// in case the user has disconnected before, update the state
reconnect = true;
return config.getSentinels() != null && config.getSentinels().size() > 0 && config.getMasterName() != null;
}
private void connect(SocketAddress socketAddress, boolean checkMaster) {
replyParser.reset();
// create a netClient for the connection
final NetClient client = vertx.createNetClient(config);
client.connect(socketAddress, asyncResult -> {
if (asyncResult.failed()) {
client.close();
if (state.compareAndSet(State.CONNECTING, State.ERROR)) {
// clean up any waiting command
clearQueue(waiting, asyncResult.cause());
// clean up any pending command
clearQueue(pending, asyncResult.cause());
state.set(State.DISCONNECTED);
// Should we retry?
if (reconnect) {
vertx.setTimer(config.getReconnectInterval(), v0 -> connect());
}
}
} else {
netSocket = asyncResult.result()
.handler(replyParser)
.closeHandler(v2 -> {
state.set(State.ERROR);
// clean up any waiting command
clearQueue(waiting, "Connection closed");
// clean up any pending command
clearQueue(pending, "Connection closed");
state.set(State.DISCONNECTED);
client.close();
// was this close intentional?
if (reconnect) {
vertx.setTimer(config.getReconnectInterval(), v0 -> connect());
}
})
.exceptionHandler(e ->
netSocket.close());
// clean up any waiting command
clearQueue(waiting, "Connection lost");
// handle the connection handshake
doAuth();
// check if the Redis instance is master
if (checkMaster) {
doCheckMaster();
}
}
});
}
private void connect() {
if (state.compareAndSet(State.DISCONNECTED, State.CONNECTING)) {
runOnContext(v -> {
if (useSentinel()) {
RedisMasterResolver resolver = new RedisMasterResolver(context.owner(), config);
resolver.getMasterAddressByName(jsonObjectAsyncResult -> {
if (jsonObjectAsyncResult.succeeded()) {
JsonObject masterAddress = jsonObjectAsyncResult.result();
connect(SocketAddress.inetSocketAddress(masterAddress.getInteger("port"), masterAddress.getString("host")), true);
} else {
// clean up any waiting command
clearQueue(waiting, jsonObjectAsyncResult.cause());
// clean up any pending command
clearQueue(pending, jsonObjectAsyncResult.cause());
state.set(State.DISCONNECTED);
}
resolver.close();
});
} else {
// if the domain socket option is enabled, use the domain socket address to connect to redis server
SocketAddress socketAddress;
if (config.isDomainSocket()) {
socketAddress = SocketAddress.domainSocketAddress(config.getDomainSocketAddress());
} else {
socketAddress = SocketAddress.inetSocketAddress(config.getPort(), config.getHost());
}
connect(socketAddress, false);
}
});
}
}
void disconnect(Handler> closeHandler) {
// update state to notify that the user wants to disconnect
reconnect = false;
switch (state.get()) {
case CONNECTING:
// eventually will become connected
case CONNECTED:
final Command cmd = new Command<>(context, RedisCommand.QUIT, null, Charset.defaultCharset(), ResponseTransform.NONE, Void.class);
cmd.handler(ar -> {
// at this we force the state to error so any incoming command will not start a connection
if (state.compareAndSet(State.CONNECTED, State.ERROR)) {
// clean up any waiting command
clearQueue(waiting, "Connection closed");
// clean up any pending command
clearQueue(pending, "Connection closed");
netSocket.close();
}
closeHandler.handle(Future.succeededFuture());
});
send(cmd);
break;
case ERROR:
// eventually will become DISCONNECTED
case DISCONNECTED:
closeHandler.handle(Future.succeededFuture());
break;
}
}
/**
* Sends a message to redis, if the connection is not active then the command is queued for processing and the
* procedure to start a connection is started.
*
* While this procedure is going on (CONNECTING) incomming commands are queued.
*
* @param command the redis command to send
*/
void send(final Command> command) {
// start the handshake if not connected
if (state.get() == State.DISCONNECTED) {
connect();
}
// write to the socket in the netSocket context
runOnContext(v -> {
switch (state.get()) {
case CONNECTED:
write(command);
break;
case CONNECTING:
case ERROR:
case DISCONNECTED:
pending.add(command);
break;
}
});
}
/**
* Write the command to the socket. The order read must match the order written, vertx
* guarantees that this is only called from a single thread.
*/
private void write(Command> command) {
for (int i = 0; i < command.getExpectedReplies(); ++i) {
waiting.add(command);
}
command.writeTo(netSocket);
}
/**
* Once a socket connection is established one needs to authenticate if there is a password
*/
private void doAuth() {
if (config.getAuth() != null) {
// we need to authenticate first
final List