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

org.swisspush.reststorage.redis.DefaultRedisProvider Maven / Gradle / Ivy

package org.swisspush.reststorage.redis;

import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.redis.client.Redis;
import io.vertx.redis.client.RedisAPI;
import io.vertx.redis.client.RedisConnection;
import io.vertx.redis.client.RedisClientType;
import io.vertx.redis.client.RedisOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.swisspush.reststorage.exception.RestStorageExceptionFactory;
import org.swisspush.reststorage.util.ModuleConfiguration;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Default implementation for a Provider for {@link RedisAPI}
 *
 * @author https://github.com/mcweba [Marc-Andre Weber]
 */
public class DefaultRedisProvider implements RedisProvider {

    private static final Logger log = LoggerFactory.getLogger(DefaultRedisProvider.class);
    private final Vertx vertx;

    private final ModuleConfiguration configuration;
    private final RestStorageExceptionFactory exceptionFactory;

    private RedisAPI redisAPI;
    private Redis redis;
    private final AtomicBoolean connecting = new AtomicBoolean();
    private RedisConnection client;
    private RedisReadyProvider readyProvider;

    private final AtomicReference> connectPromiseRef = new AtomicReference<>();

    public DefaultRedisProvider(
            Vertx vertx,
            ModuleConfiguration configuration,
            RestStorageExceptionFactory exceptionFactory
    ) {
        this.vertx = vertx;
        this.configuration = configuration;
        this.exceptionFactory = exceptionFactory;

        maybeInitRedisReadyProvider();
    }

    private void maybeInitRedisReadyProvider() {
        if (configuration.getRedisReadyCheckIntervalMs() > 0) {
            this.readyProvider = new DefaultRedisReadyProvider(vertx, configuration.getRedisReadyCheckIntervalMs());
        }
    }

    @Override
    public Future redis() {
        if(redisAPI == null) {
            return setupRedisClient();
        }
        if(readyProvider == null) {
            return Future.succeededFuture(redisAPI);
        }
        return readyProvider.ready(redisAPI).compose(ready -> {
            if (ready) {
                return Future.succeededFuture(redisAPI);
            }
            return Future.failedFuture("Not yet ready!");
        });
    }

    private boolean reconnectEnabled() {
        return configuration.getRedisReconnectAttempts() != 0;
    }

    private Future setupRedisClient() {
        Promise currentPromise = Promise.promise();
        Promise masterPromise = connectPromiseRef.accumulateAndGet(
                currentPromise, (oldVal, newVal) -> (oldVal != null) ? oldVal : newVal);
        if (currentPromise == masterPromise) {
            // Our promise is THE promise. So WE have to resolve it.
            connectToRedis().onComplete(event -> {
                connectPromiseRef.getAndSet(null);
                if (event.failed()) {
                    currentPromise.fail(event.cause());
                } else {
                    redisAPI = event.result();
                    currentPromise.complete(redisAPI);
                }
            });
        }

        // Always return master promise (even if we didn't create it ourselves)
        return masterPromise.future();
    }

    private Future connectToRedis() {
        String redisAuth = configuration.getRedisAuth();
        int redisMaxPoolSize = configuration.getMaxRedisConnectionPoolSize();
        int redisMaxPoolWaitingSize = configuration.getMaxQueueWaiting();
        int redisMaxPipelineWaitingSize = configuration.getMaxRedisWaitingHandlers();
        int redisPoolRecycleTimeoutMs = configuration.getRedisPoolRecycleTimeoutMs();

        Promise promise = Promise.promise();

        // make sure to invalidate old connection if present
        if (redis != null) {
            redis.close();
        }

        if (connecting.compareAndSet(false, true)) {
            RedisOptions redisOptions = new RedisOptions()
                    .setPassword((redisAuth == null ? "" : redisAuth))
                    .setMaxPoolSize(redisMaxPoolSize)
                    .setMaxPoolWaiting(redisMaxPoolWaitingSize)
                    .setPoolRecycleTimeout(redisPoolRecycleTimeoutMs)
                    .setMaxWaitingHandlers(redisMaxPipelineWaitingSize)
                    .setType(configuration.getRedisClientType());

            createConnectStrings().forEach(redisOptions::addConnectionString);
            redis = Redis.createClient(vertx, redisOptions);

            redis.connect().onComplete(ev -> {
                if (ev.failed()) {
                    promise.fail(exceptionFactory.newException("redis.connect() failed", ev.cause()));
                    connecting.set(false);
                    return;
                }
                var conn = ev.result();
                log.info("Successfully connected to redis");
                client = conn;

                if (configuration.getRedisClientType() == RedisClientType.STANDALONE) {
                    client.close();
                }

                // make sure the client is reconnected on error
                // eg, the underlying TCP connection is closed but the client side doesn't know it yet
                // the client tries to use the staled connection to talk to server. An exceptions will be raised
                if (reconnectEnabled()) {
                    conn.exceptionHandler(ex -> {
                        log.warn("redis connection reports problem", ex);
                        attemptReconnect(0);
                    });
                }

                // make sure the client is reconnected on connection close
                // eg, the underlying TCP connection is closed with normal 4-Way-Handshake
                // this handler will be notified instantly
                if (reconnectEnabled()) {
                    conn.endHandler(nothing -> {
                        log.warn("redis connection got closed");
                        attemptReconnect(0);
                    });
                }

                // allow further processing
                redisAPI = RedisAPI.api(conn);
                promise.complete(redisAPI);
                connecting.set(false);
            });
        } else {
            promise.complete(redisAPI);
        }

        return promise.future();
    }

    private List createConnectStrings() {
        String redisPassword = configuration.getRedisPassword();
        String redisUser = configuration.getRedisUser();
        StringBuilder connectionStringPrefixBuilder = new StringBuilder();
        connectionStringPrefixBuilder.append(configuration.isRedisEnableTls() ? "rediss://" : "redis://");
        if (redisUser != null && !redisUser.isEmpty()) {
            connectionStringPrefixBuilder.append(redisUser).append(":").append((redisPassword == null ? "" : redisPassword)).append("@");
        }
        List connectionString = new ArrayList<>();
        String connectionStringPrefix = connectionStringPrefixBuilder.toString();
        for (int i = 0; i < configuration.getRedisHosts().size(); i++) {
            String host = configuration.getRedisHosts().get(i);
            int port = configuration.getRedisPorts().get(i);
            connectionString.add(connectionStringPrefix + host + ":" + port);
        }
        return connectionString;
    }

    private void attemptReconnect(int retry) {

        log.info("About to reconnect to redis with attempt #{}", retry);
        int reconnectAttempts = configuration.getRedisReconnectAttempts();
        if (reconnectAttempts < 0) {
            doReconnect(retry);
        } else if (retry > reconnectAttempts) {
            log.warn("Not reconnecting anymore since max reconnect attempts ({}) are reached", reconnectAttempts);
            connecting.set(false);
        } else {
            doReconnect(retry);
        }
    }

    private void doReconnect(int retry) {
        long backoffMs = (long) (Math.pow(2, Math.min(retry, 10)) * configuration.getRedisReconnectDelaySec());
        log.debug("Schedule reconnect #{} in {}ms.", retry, backoffMs);
        vertx.setTimer(backoffMs, timer -> connectToRedis()
                .onFailure(t -> attemptReconnect(retry + 1)));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy