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

io.vertx.redis.client.impl.RedisSentinelClient Maven / Gradle / Ivy

There is a newer version: 5.0.0.CR3
Show newest version
/*
 * 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.ErrorType; import java.util.List; import java.util.SplittableRandom; import static io.vertx.redis.client.Command.*; import static io.vertx.redis.client.Request.cmd; public class RedisSentinelClient extends BaseRedisClient implements Redis { // We don't need to be secure, we just want so simple // randomization to avoid picking the same replica all the time private static final SplittableRandom RANDOM = new SplittableRandom(); private static class Pair { final L left; final R right; Pair(L left, R right) { this.left = left; this.right = right; } } private static final Logger LOG = LoggerFactory.getLogger(RedisSentinelClient.class); private final RedisOptions options; private RedisConnection sentinel; public RedisSentinelClient(Vertx vertx, RedisOptions options) { super(vertx, options); this.options = options; // validate options if (options.getMaxPoolSize() < 2) { throw new IllegalStateException("Invalid options: maxPoolSize must be at least 2"); } if (options.getMaxPoolWaiting() < options.getMaxPoolSize()) { throw new IllegalStateException("Invalid options: maxPoolWaiting < maxPoolSize"); } } @Override public Future connect() { final Promise promise = vertx.promise(); // sentinel (HA) requires 2 connections, one to watch for sentinel events and the connection itself createConnectionInternal(options, options.getRole(), createConnection -> { if (createConnection.failed()) { promise.fail(createConnection.cause()); return; } final RedisConnection conn = createConnection.result(); createConnectionInternal(options, RedisRole.SENTINEL, create -> { if (create.failed()) { LOG.error("Redis PUB/SUB wrap failed.", create.cause()); return; } sentinel = create.result(); sentinel .handler(msg -> { if (msg.type() == ResponseType.MULTI) { if ("MESSAGE".equalsIgnoreCase(msg.get(0).toString())) { // we don't care about the payload if (conn != null) { ((RedisStandaloneConnection) conn).fail(ErrorType.create("SWITCH-MASTER Received +switch-master message from Redis Sentinel.")); } else { LOG.warn("Received +switch-master message from Redis Sentinel."); } } } }); sentinel.send(cmd(SUBSCRIBE).arg("+switch-master"), send -> { if (send.failed()) { promise.fail(send.cause()); } else { // both connections ready promise.complete(new RedisSentinelConnection(conn, sentinel)); } }); sentinel.exceptionHandler(t -> { if (conn != null) { ((RedisStandaloneConnection) conn).fail(t); } else { LOG.error("Unhandled exception in Sentinel PUBSUB", t); } }); }); }); return promise.future(); } private void createConnectionInternal(RedisOptions options, RedisRole role, Handler> onCreate) { final Handler> createAndConnect = resolve -> { if (resolve.failed()) { onCreate.handle(Future.failedFuture(resolve.cause())); return; } final RedisURI uri = resolve.result(); final String endpoint = getBaseEndpoint(uri); final Request setup; // SELECT is only allowed on non sentinel if (role != RedisRole.SENTINEL && uri.select() != null) { setup = cmd(SELECT).arg(uri.select()); } else { setup = null; } // wrap a new client connectionManager.getConnection(endpoint, setup).onComplete(onCreate); }; switch (role) { case SENTINEL: resolveClient(this::isSentinelOk, options, createAndConnect); break; case MASTER: resolveClient(this::getMasterFromEndpoint, options, createAndConnect); break; case REPLICA: resolveClient(this::getReplicaFromEndpoint, options, createAndConnect); break; } } /** * We use the algorithm from http://redis.io/topics/sentinel-clients * to get a sentinel client and then do 'stuff' with it */ private static void resolveClient(final Resolver checkEndpointFn, final RedisOptions options, final Handler> callback) { // Because finding the master is going to be an async list we will terminate // when we find one then use promises... iterate(0, checkEndpointFn, options, iterate -> { if (iterate.failed()) { callback.handle(Future.failedFuture(iterate.cause())); } else { final Pair found = iterate.result(); // This is the endpoint that has responded so stick it on the top of // the list final List endpoints = options.getEndpoints(); String endpoint = endpoints.get(found.left); endpoints.set(found.left, endpoints.get(0)); endpoints.set(0, endpoint); // now return the right address callback.handle(Future.succeededFuture(found.right)); } }); } private static void iterate(final int idx, final Resolver checkEndpointFn, final RedisOptions argument, final Handler>> resultHandler) { // stop condition final List endpoints = argument.getEndpoints(); if (idx >= endpoints.size()) { resultHandler.handle(Future.failedFuture("No more endpoints in chain.")); return; } // attempt to perform operation checkEndpointFn.resolve(endpoints.get(idx), argument, res -> { if (res.succeeded()) { resultHandler.handle(Future.succeededFuture(new Pair<>(idx, res.result()))); } else { // try again with next endpoint iterate(idx + 1, checkEndpointFn, argument, resultHandler); } }); } // begin endpoint check methods private void isSentinelOk(String endpoint, RedisOptions argument, Handler> handler) { // we can't use the endpoint as is, it should not contain a database selection, // but can contain authentication final RedisURI uri = new RedisURI(endpoint); connectionManager.getConnection(getBaseEndpoint(uri), null) .onFailure(err -> handler.handle(Future.failedFuture(err))) .onSuccess(conn -> { // Send a command just to check we have a working node conn.send(cmd(PING), ping -> { if (ping.failed()) { handler.handle(Future.failedFuture(ping.cause())); } else { handler.handle(Future.succeededFuture(uri)); } // connection is not needed anymore conn.close().onFailure(LOG::warn); }); }); } private void getMasterFromEndpoint(String endpoint, RedisOptions options, Handler> handler) { // we can't use the endpoint as is, it should not contain a database selection, // but can contain authentication final RedisURI uri = new RedisURI(endpoint); connectionManager.getConnection(getBaseEndpoint(uri), null) .onFailure(err -> handler.handle(Future.failedFuture(err))) .onSuccess(conn -> { final String masterName = options.getMasterName(); // Send a command just to check we have a working node conn.send(cmd(SENTINEL).arg("GET-MASTER-ADDR-BY-NAME").arg(masterName), getMasterAddrByName -> { if (getMasterAddrByName.failed()) { handler.handle(Future.failedFuture(getMasterAddrByName.cause())); } else { // Test the response final Response response = getMasterAddrByName.result(); if (response == null) { handler.handle(Future.failedFuture("Failed to GET-MASTER-ADDR-BY-NAME " + masterName)); } else { final String rHost = response.get(0).toString(); final Integer rPort = response.get(1).toInteger(); handler.handle(Future.succeededFuture(new RedisURI(uri, rHost.contains(":") ? "[" + rHost + "]" : rHost, rPort))); } } // we don't need this connection anymore conn.close().onFailure(LOG::warn); }); }); } private void getReplicaFromEndpoint(String endpoint, RedisOptions options, Handler> handler) { // we can't use the endpoint as is, it should not contain a database selection, // but can contain authentication final RedisURI uri = new RedisURI(endpoint); connectionManager.getConnection(getBaseEndpoint(uri), null) .onFailure(err -> handler.handle(Future.failedFuture(err))) .onSuccess(conn -> { final String masterName = options.getMasterName(); // Send a command just to check we have a working node conn.send(cmd(SENTINEL).arg("SLAVES").arg(masterName), sentinelReplicas -> { if (sentinelReplicas.failed()) { handler.handle(Future.failedFuture(sentinelReplicas.cause())); } else { final Response response = sentinelReplicas.result(); // Test the response if (response == null || response.size() == 0) { handler.handle(Future.failedFuture("No replicas linked to the master: " + masterName)); } else { Response replicaInfoArr = response.get(RANDOM.nextInt(response.size())); if ((replicaInfoArr.size() % 2) > 0) { handler.handle(Future.failedFuture("Corrupted response from the sentinel")); } else { int port = 6379; String ip = null; if (replicaInfoArr.containsKey("port")) { port = replicaInfoArr.get("port").toInteger(); } if (replicaInfoArr.containsKey("ip")) { ip = replicaInfoArr.get("ip").toString(); } if (ip == null) { handler.handle(Future.failedFuture("No IP found for a REPLICA node!")); } else { final String host = ip.contains(":") ? "[" + ip + "]" : ip; handler.handle(Future.succeededFuture(new RedisURI(uri, host, port))); } } } } // connection is not needed anymore conn.close().onFailure(LOG::warn); }); }); } private String getBaseEndpoint(RedisURI uri) { StringBuilder sb = new StringBuilder(); if (uri.unix()) { sb.append("unix://"); sb.append(uri.socketAddress().path()); } else { sb.append("redis"); if (uri.ssl()) { sb.append('s'); } sb.append("://"); sb.append(uri.userinfo()); sb.append(uri.socketAddress().host()); sb.append(':'); sb.append(uri.socketAddress().port()); } return sb.toString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy