com.vsct.dt.hesperides.storage.RedisEventStore.java Source code

Java tutorial

Introduction

Here is the source code for com.vsct.dt.hesperides.storage.RedisEventStore.java

Source

/*
 *
 *  * This file is part of the Hesperides distribution.
 *  * (https://github.com/voyages-sncf-technologies/hesperides)
 *  * Copyright (c) 2016 VSCT.
 *  *
 *  * Hesperides is free software: you can redistribute it and/or modify
 *  * it under the terms of the GNU General Public License as
 *  * published by the Free Software Foundation, version 3.
 *  *
 *  * Hesperides is distributed in the hope that it will be useful, but
 *  * WITHOUT ANY WARRANTY; without even the implied warranty of
 *  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 *  * General Public License for more details.
 *  *
 *  * You should have received a copy of the GNU General Public License
 *  * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 */

package com.vsct.dt.hesperides.storage;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vsct.dt.hesperides.util.ManageableJedisConnectionInterface;
import io.dropwizard.jackson.Jackson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.util.Pool;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Created by william_montaz on 03/11/2014.
 */
public final class RedisEventStore<A extends JedisCommands & MultiKeyCommands & AdvancedJedisCommands & ScriptingCommands & BasicCommands & ClusterCommands & Closeable>
        implements EventStore {

    /**
     * Logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisEventStore.class);

    /**
     * Builder of event from JSON.
     */
    private static final ObjectMapper MAPPER = Jackson.newObjectMapper();

    /**
     * Pool.
     */
    private final Pool<A> dataPool;

    /**
     * Snapshot pool.
     */
    private final Pool<A> snapshotPool;

    /**
     * Number of retry.
     */
    private final int nRetry;

    /**
     * Wait time before retry.
     */
    private final int waitBeforeRetryMs;

    /**
     * Number of events to be loaded at once when replaying
     * It is also the events list pagination page size.
     */
    private int BATCH_SIZE = 100;

    public RedisEventStore(final ManageableJedisConnectionInterface<A> dataPool,
            final ManageableJedisConnectionInterface<A> snapshotPool) {
        this.dataPool = dataPool.getPool();
        this.snapshotPool = snapshotPool.getPool();
        this.nRetry = dataPool.getnRetry();
        this.waitBeforeRetryMs = dataPool.getWaitBeforeRetryMs();
    }

    private <T> T execute(final Pool<A> dataPool, final Pool<A> snapshotPool, final RedisCommand<T, A> command) {
        int attempt = 1;

        for (;;) {

            try (A jedisData = dataPool.getResource(); A jedisSnapshot = snapshotPool.getResource()) {
                return command.execute(jedisData, jedisSnapshot);
            } catch (final JedisException e) {
                if (attempt <= nRetry) {
                    LOGGER.warn("JEDIS CONNECTION - ATTEMPT {} ON {}", attempt, nRetry);
                    try {
                        Thread.sleep(waitBeforeRetryMs);
                    } catch (InterruptedException ie) {
                        ie.printStackTrace();
                    }
                    attempt++;
                    continue;
                } else {
                    LOGGER.error("JEDIS CONNECTION FAILED AFTER {} ATTEMPTS", nRetry);
                    throw e;
                }
            } catch (Throwable e) {
                command.error(e);

                return null;
            }
        }
    }

    @Override
    public <T> T store(final String streamName, final T event, final UserInfo userInfo,
            final EventStoreCallback callback) {

        return execute(dataPool, snapshotPool, new RedisCommand<T, A>() {
            @Override
            public T execute(final A jedisData, final A jedisSnapshot) throws Throwable {
                Event eventStoreEvent = new Event(event.getClass().getCanonicalName(),
                        MAPPER.writeValueAsString(event), System.currentTimeMillis(), userInfo.getUsername());

                jedisData.rpush(streamName, MAPPER.writeValueAsString(eventStoreEvent));

                LOGGER.debug("stored event {}", event);

                callback.complete();

                return event;
            }

            @Override
            public void error(final Throwable e) {
                if (e instanceof JsonProcessingException) {
                    LOGGER.error("Could not serialize the event to string");
                }

                throw new RuntimeException(e);
            }
        });
    }

    @Override
    public HesperidesSnapshotItem findSnapshot(final String streamName, final long offset,
            final EventTester<Event> ev) {
        return execute(dataPool, snapshotPool, new RedisCommand<HesperidesSnapshotItem, A>() {
            @Override
            public HesperidesSnapshotItem execute(final A jedisData, final A jedisSnapshot) throws Throwable {
                final String redisSnapshotKey = String.format("snapshotevents-%s", streamName);

                if (jedisData.exists(streamName) && jedisSnapshot.exists(redisSnapshotKey)) {
                    final long lastIndex = jedisData.llen(streamName) - 1;

                    long currentEventIndex;
                    long selectedCacheIndex;

                    // Check event that jump to ev.increment
                    for (currentEventIndex = 0; currentEventIndex <= lastIndex; currentEventIndex += offset) {
                        final List<String> binaryEvents = jedisData.lrange(streamName, currentEventIndex,
                                currentEventIndex);

                        final Event event = MAPPER.readValue(binaryEvents.get(0), Event.class);

                        if (ev.test(event)) {
                            break;
                        }
                    }

                    // E.g. we stop to event #8000
                    // Cache index to event #8000 is #79
                    // But #8000 is rejected, we must reject cache #79 an get cache #78
                    selectedCacheIndex = ((currentEventIndex / offset) - 1) - 1;

                    final List<String> lastCache = jedisSnapshot.lrange(redisSnapshotKey, selectedCacheIndex,
                            selectedCacheIndex);

                    final HesperidesSnapshotCacheEntry cache = MAPPER.readValue(lastCache.get(0),
                            HesperidesSnapshotCacheEntry.class);

                    Object object = MAPPER.readValue(cache.getData(), Class.forName(cache.getCacheType()));

                    return new HesperidesSnapshotItem(object, cache.getNbEvents(), currentEventIndex);
                }

                return null;
            }

            @Override
            public void error(final Throwable e) {
                if (e instanceof ClassNotFoundException || e instanceof IOException) {
                    LOGGER.error("Could not deserialize the snapshot '{}' cache.", streamName);
                    // Don't bock cause Hesperides could be work
                    LOGGER.error("Stacktrace : ", e);
                } else {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    @Override
    public HesperidesSnapshotItem findLastSnapshot(final String streamName) {
        return execute(dataPool, snapshotPool, new RedisCommand<HesperidesSnapshotItem, A>() {
            @Override
            public HesperidesSnapshotItem execute(final A jedisData, final A jedisSnapshot) throws Throwable {
                final String redisSnapshotKey = String.format("snapshotevents-%s", streamName);

                if (jedisSnapshot.exists(redisSnapshotKey)) {
                    final long lastIndex = jedisSnapshot.llen(redisSnapshotKey) - 1;
                    final List<String> lastCache = jedisSnapshot.lrange(redisSnapshotKey, lastIndex, lastIndex);

                    final HesperidesSnapshotCacheEntry cache = MAPPER.readValue(lastCache.get(0),
                            HesperidesSnapshotCacheEntry.class);

                    Object object = MAPPER.readValue(cache.getData(), Class.forName(cache.getCacheType()));

                    return new HesperidesSnapshotItem(object, cache.getNbEvents(), jedisData.llen(streamName));
                }

                return null;
            }

            @Override
            public void error(final Throwable e) {
                if (e instanceof ClassNotFoundException || e instanceof IOException) {
                    LOGGER.error("Could not deserialize the snapshot '{}' cache.", streamName);
                    // Don't bock cause Hesperides could be work
                    LOGGER.error("Stacktrace : ", e);
                } else {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    @Override
    public <T> void storeSnapshot(final String streamName, final T object, final long offset) {
        execute(dataPool, snapshotPool, new RedisCommand<Void, A>() {
            @Override
            public Void execute(final A jedisData, final A jedisSnapshot) throws Throwable {
                final String redisSnapshotKey = String.format("snapshotevents-%s", streamName);

                final long currentNbEvent = jedisData.llen(streamName);
                final long cacheNbEvent = jedisSnapshot.llen(redisSnapshotKey);

                HesperidesSnapshotCacheEntry hsce;

                // We need check if in cache we have no already store snapshot.
                // In case of platform, we have update platform and update properties in same time
                // that mean we create two snapshot. It's not good.
                if (offset == 0 || (currentNbEvent % offset == 0 && (currentNbEvent / offset) > cacheNbEvent)) {
                    LOGGER.debug("Store new snapshot for key {}.", redisSnapshotKey);
                    hsce = new HesperidesSnapshotCacheEntry(object.getClass().getCanonicalName(),
                            MAPPER.writeValueAsString(object), currentNbEvent);

                    jedisSnapshot.rpush(redisSnapshotKey, MAPPER.writeValueAsString(hsce));
                }

                return null;
            }

            @Override
            public void error(final Throwable e) {
                LOGGER.error("Could not serialize the snapshot '{}' cache to string.", streamName);
                // Don't bock cause Hesperides could be work
                e.printStackTrace();
            }
        });
    }

    @Override
    public <T> void createSnapshot(final String streamName, final T object, final long nbEvent) {
        execute(dataPool, snapshotPool, new RedisCommand<Void, A>() {
            @Override
            public Void execute(final A jedisData, final A jedisSnapshot) throws Throwable {
                final String redisSnapshotKey = String.format("snapshotevents-%s", streamName);

                LOGGER.debug("Store new snapshot for key {}.", redisSnapshotKey);
                HesperidesSnapshotCacheEntry hsce = new HesperidesSnapshotCacheEntry(
                        object.getClass().getCanonicalName(), MAPPER.writeValueAsString(object), nbEvent);

                jedisSnapshot.rpush(redisSnapshotKey, MAPPER.writeValueAsString(hsce));

                return null;
            }

            @Override
            public void error(final Throwable e) {
                LOGGER.error("Could not serialize the snapshot '{}' cache to string.", streamName);
                // Don't bock cause Hesperides could be work
                e.printStackTrace();
            }
        });
    }

    @Override
    public void withEvents(final String streamName, final long stopTimestamp, final Consumer<Object> eventConsumer)
            throws StoreReadingException {
        long len;

        try (A jedis = dataPool.getResource()) {

            len = jedis.llen(streamName);

            withEvents(streamName, 0, len, stopTimestamp, eventConsumer);
        } catch (StoreReadingException | IOException e) {
            e.printStackTrace();
            throw new StoreReadingException(e);
        }
    }

    @Override
    public void withEvents(final String streamName, final long start, final long stop, final long stopTimestamp,
            final Consumer<Object> eventConsumer) throws StoreReadingException {
        try (A jedis = dataPool.getResource()) {
            LOGGER.debug("{} events to restore for stream {}", stop - start, streamName);

            final long startTime = System.nanoTime();

            int indexEvent;
            long indexBatch;
            int counter = 0;
            long startIO;
            long stopIO;

            long ioAccumulator = 0, serializationAccumulator = 0, processingAccumulator = 0;

            for (indexBatch = start; indexBatch < stop; indexBatch = indexBatch + BATCH_SIZE) {

                startIO = System.nanoTime();

                List<String> events = jedis.lrange(streamName, indexBatch, indexBatch + BATCH_SIZE - 1);

                if (LOGGER.isDebugEnabled()) {
                    stopIO = System.nanoTime();

                    ioAccumulator += stopIO - startIO;
                }

                for (indexEvent = 0; indexEvent < events.size(); indexEvent++) {

                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace("Processing event {}", indexBatch + indexEvent);
                    }

                    long startSerialization = System.nanoTime();

                    Event event = MAPPER.readValue(events.get(indexEvent), Event.class);

                    if (event.getTimestamp() > stopTimestamp) {
                        //No need to go beyong this point in time
                        indexBatch = stop;
                        break;
                    }

                    Object hesperidesEvent = MAPPER.readValue(event.getData(), Class.forName(event.getEventType()));
                    long stopSerialization = System.nanoTime();

                    serializationAccumulator += stopSerialization - startSerialization;

                    long startProcessing = System.nanoTime();
                    eventConsumer.accept(hesperidesEvent);
                    long stopProcessing = System.nanoTime();

                    processingAccumulator += stopProcessing - startProcessing;

                    counter++;
                }
            }

            if (LOGGER.isDebugEnabled()) {
                final long stopTime = System.nanoTime();

                long durationMs = (stopTime - startTime) / 1000000;

                double frequency = ((double) counter / durationMs) * 1000;

                LOGGER.debug(
                        "Stream {} complete ({} events processed - duration {} ms - {} msg/sec - {} ms IO -"
                                + "{} ms Serialization - {} ms processing)",
                        streamName, counter, durationMs, frequency, ioAccumulator / 1000000,
                        serializationAccumulator / 1000000, processingAccumulator / 1000000);
            }
        } catch (StoreReadingException | ClassNotFoundException | IOException e) {
            e.printStackTrace();
            throw new StoreReadingException(e);
        }
    }

    @Override
    public void withEvents(final String streamName, final long start, final long stop,
            final Consumer<Object> eventConsumer) throws StoreReadingException {
        try (A jedis = dataPool.getResource()) {

            LOGGER.debug("{} events to restore for stream {}", stop - start, streamName);

            final List<String> events = jedis.lrange(streamName, start, stop);

            for (int indexEvent = 0; indexEvent < events.size(); indexEvent++) {

                LOGGER.trace("Processing event {}", indexEvent);

                Event event = MAPPER.readValue(events.get(indexEvent), Event.class);

                Object hesperidesEvent = MAPPER.readValue(event.getData(), Class.forName(event.getEventType()));

                eventConsumer.accept(hesperidesEvent);
            }

            LOGGER.debug("Stream {} complete ({} events processed)", streamName, stop - start);

        } catch (StoreReadingException | ClassNotFoundException | IOException e) {
            e.printStackTrace();
            throw new StoreReadingException(e);
        }
    }

    @Override
    public Set<String> getStreamsLike(final String term) {
        try (A jedis = dataPool.getResource()) {
            return jedis.keys(term);
        } catch (IOException e) {
            return null;
        }
    }

    @Override
    public List<Event> getEventsList(final String streamName, final int page, final int size)
            throws StoreReadingException {

        try (A jedis = dataPool.getResource()) {

            LOGGER.debug("Start Retrieving {} events for {}", size, streamName);

            // size of entry for  streamName
            Long len = jedis.llen(streamName);

            //
            // This is a little calculation to get redis events from oldest to newest
            // This was required because of troubles on sorting events with pagination.
            //

            // Calculating from where we should start retrieving
            // Note : The default page number is 1 and the default pagination size is 25
            long from = len - ((page > 0 ? page : 1) * (size > 0 ? size : 25));
            from = (from > 0) ? from : 0;

            // Calculating til where we should retrieve. The total retrieved items should be equal to the size !
            long to = -1 * (((page - 1) * size) + 1);

            // Querying redis
            List<String> binaryEvents = jedis.lrange(streamName, from, to);

            // Converting items from redis to Event objects
            List<Event> events = new ArrayList<>();

            for (int index = 0; index < binaryEvents.size(); index++) {
                events.add(MAPPER.readValue(binaryEvents.get(index), Event.class));
            }

            LOGGER.debug("End retrieving events from {} to {}. Size of events retrieved {} events for {}.", from,
                    to, events.size(), streamName);

            return events;

        } catch (StoreReadingException | IOException e) {
            LOGGER.debug("Exception {} occurred when getting the list of events for {}. Stacktrace : {}",
                    e.getClass().getCanonicalName(), streamName, e.getStackTrace());
            throw new StoreReadingException(e);
        }
    }

    @Override
    public void clearCache(final String streamName) {
        execute(dataPool, snapshotPool, new RedisCommand<Void, A>() {
            @Override
            public Void execute(final A jedisData, final A jedisSnapshot) throws Throwable {
                final String redisKey = String.format("snapshotevents-%s", streamName);

                LOGGER.debug("Clear snapshot snapshot {}.", redisKey);

                jedisSnapshot.del(redisKey);

                return null;
            }

            @Override
            public void error(final Throwable e) {
                LOGGER.error("Could not serialize the snapshot '{}' cache to string.", streamName);
                // Don't bock cause Hesperides could be work
                e.printStackTrace();
            }
        });
    }

    public boolean isConnected() {
        try (A jedis = dataPool.getResource()) {
            jedis.ping();
            return true;
        } catch (IOException e) {
            return false;
        }
    }
}