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.*;
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.redis.RedisOptions;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* 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);
// there are 2 queues:
// pending: commands that have not yet been sent to the server
private final Queue> pending = new ConcurrentLinkedQueue<>();
// waiting: commands that have been sent but not answered
private final Queue> waiting = new ConcurrentLinkedQueue<>();
private final ReplyParser replyParser;
private final NetClient client;
private final RedisOptions config;
private enum State {
/**
* The connection is not active. The is a stop state.
*/
DISCONNECTED,
/**
* The connection is in transit, from here it can become connected or and error can occur.
*/
CONNECTING,
/**
* Connection is active from here it can become an error or disconnected.
*/
CONNECTED,
/**
* Connection problem
*/
ERROR
}
private volatile State state = State.DISCONNECTED;
private volatile NetSocket netSocket;
private volatile Context context;
/**
* Create a RedisConnection.
*
* A Redis connection should be used for normal actions, i.e.: not for pub/sub
*/
public RedisConnection(NetClient client, RedisOptions config) {
this.client = client;
this.config = config;
this.replyParser = new ReplyParser(this::handleReply);
}
/**
* Create a Pub/Sub connection.
*/
public RedisConnection(NetClient client, RedisOptions config, RedisSubscriptions subscriptions) {
this.client = client;
this.config = config;
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);
});
}
private synchronized void connect() {
if (state == State.DISCONNECTED) {
state = State.CONNECTING;
replyParser.reset();
client.connect(config.getPort(), config.getHost(), asyncResult -> {
context = Vertx.currentContext();
if (asyncResult.failed()) {
state = State.ERROR;
Command> command;
// clean up any waiting command
while ((command = waiting.poll()) != null) {
command.handle(Future.failedFuture(asyncResult.cause()));
}
// clean up any pending command
while ((command = pending.poll()) != null) {
command.handle(Future.failedFuture(asyncResult.cause()));
}
// close the socket if previously connected
if (netSocket != null) {
netSocket.close();
}
state = State.DISCONNECTED;
} else {
netSocket = asyncResult.result()
.handler(replyParser)
.closeHandler(v -> {
state = State.ERROR;
// should clean up queues
Command> command;
// clean up any waiting command
while ((command = waiting.poll()) != null) {
command.handle(Future.failedFuture("Connection closed!"));
}
// clean up any pending command
while ((command = pending.poll()) != null) {
command.handle(Future.failedFuture("Connection closed!"));
}
netSocket.close();
state = State.DISCONNECTED;
})
.exceptionHandler(e -> {
state = State.ERROR;
// should clean up queues
Command> command;
// clean up any waiting command
while ((command = waiting.poll()) != null) {
command.handle(Future.failedFuture(e));
}
// clean up any pending command
while ((command = pending.poll()) != null) {
command.handle(Future.failedFuture(e));
}
netSocket.close();
state = State.DISCONNECTED;
});
Command> command;
// clean up any waiting command
while ((command = waiting.poll()) != null) {
command.handle(Future.failedFuture("Connection lost"));
}
state = State.CONNECTED;
// handle the connection handshake
doAuth();
}
});
}
}
synchronized void disconnect(Handler> closeHandler) {
switch (state) {
case CONNECTED:
case CONNECTING:
final Command cmd = new Command<>(RedisCommand.QUIT, null, Charset.defaultCharset(), ResponseTransform.NONE, Void.class);
cmd.handler(v -> {
// at this we force the state to error so any incoming command will not start a connection
state = State.ERROR;
// should clean up queues
Command> command;
// clean up any waiting command
while ((command = waiting.poll()) != null) {
command.handle(Future.failedFuture("Connection closed!"));
}
// clean up any pending command
while ((command = pending.poll()) != null) {
command.handle(Future.failedFuture("Connection closed!"));
}
netSocket.close();
state = State.DISCONNECTED;
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
*/
void send(final Command> command) {
switch (state) {
case CONNECTED:
// The order read must match the order written, vertx guarantees
// that this is only called from a single thread.
for (int i = 0; i < command.getExpectedReplies(); ++i) {
waiting.add(command);
}
// write to the socket in the netSocket context
context.runOnContext(v -> command.writeTo(netSocket));
break;
case CONNECTING:
case ERROR:
pending.add(command);
break;
case DISCONNECTED:
pending.add(command);
connect();
break;
}
}
/**
* Once a socket connection is established one needs to authenticate if there is a password
*/
private synchronized void doAuth() {
if (config.getAuth() != null) {
// we need to authenticate first
final List