package org.ulyssis.ipp.replayer; import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.ulyssis.ipp.config.Config; import org.ulyssis.ipp.updates.TagUpdate; import org.ulyssis.ipp.utils.JedisHelper; import org.ulyssis.ipp.utils.Serialization; import redis.clients.jedis.Jedis; import redis.clients.jedis.Response; import redis.clients.jedis.Transaction; import redis.clients.jedis.exceptions.JedisConnectionException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * The replayer replays logs, the Redis database it pushes to is determined by * the given Redis database and the reader id. */ public final class Replayer implements Runnable { private static final Logger LOG = LogManager.getLogger(Replayer.class); private final ReplayerOptions options; private final Map<Integer, Jedis> jedis = new HashMap<>(); private final Map<Integer, String> updateChannel = new HashMap<>(); private final Map<Integer, Long> updateCount = new HashMap<>(); private final ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); private final Semaphore sem = new Semaphore(0); public Replayer(ReplayerOptions options) { this.options = options; for (int i : options.getReplayMap().keySet()) { try { URI uri = new URI( options.getRedisURI().getScheme(), options.getRedisURI().getUserInfo(), options.getRedisURI().getHost(), options.getRedisURI().getPort(), "/" + i, options.getRedisURI().getQuery(), options.getRedisURI().getFragment()); jedis.put(i, JedisHelper.get(uri)); updateChannel.put(i, JedisHelper.dbLocalChannel(Config.getCurrentConfig().getUpdateChannel(), uri)); updateCount.put(i, 0L); } catch (URISyntaxException e) { LOG.fatal("The syntax of the URI should be correct by now!", e); } } } @Override public void run() { Duration offset = null; System.out.println("Will run the replayer with the following replay files:"); Config config = Config.getCurrentConfig(); int nbReaders = config.getNbReaders(); SortedMap<Integer,Path> replayMap = options.getReplayMap(); Collection<SingleReaderReplay> replays = new ArrayList<>(); for (int i = 0; i < nbReaders; i++) { if (replayMap.containsKey(i)) { System.out.println(String.format("Reader %d: %s", i, replayMap.get(i).toAbsolutePath())); } } System.out.println("Press any key to continue (or ctrl-C to cancel)"); try { System.in.read(); System.out.println("Started replay!"); for (Map.Entry<Integer, Path> entry : replayMap.entrySet()) { replays.add(new SingleReaderReplay(entry.getKey(), entry.getValue())); } SingleReaderReplay first; do { first = null; Optional<Instant> firstInstant = Optional.empty(); for (SingleReaderReplay replay : replays) { Optional<Instant> instant = replay.nextTime(); if (instant.isPresent() && (!firstInstant.isPresent() || instant.get().isBefore(firstInstant.get()))) { firstInstant = instant; first = replay; } } if (first != null) { if (offset == null) { offset = Duration.between(firstInstant.get(), Instant.now()); } TagUpdate update = first.next().get(); Instant newUpdateTime = update.getUpdateTime().plus(offset); TagUpdate updateToPush = new TagUpdate(update.getReaderId(), update.getUpdateCount(), newUpdateTime, update.getTag()); // TODO: Delay update pushing a bit! Don't push it all at once! service.schedule(() -> pushUpdate(updateToPush), Duration.between(Instant.now(), newUpdateTime).toMillis(), TimeUnit.MILLISECONDS); sem.acquire(); } else { LOG.info("No more updates! Stopping..."); } } while (first != null && !Thread.currentThread().isInterrupted()); } catch (InterruptedException ignored) { } catch (Exception e) { LOG.fatal("An unexpected exception occurred!", e); } } /** * Push an update to Redis at the given instant with the given tag. * * TODO: Refactor: split off a "pusher", so we can use it in the integration tests. */ private void pushUpdate(TagUpdate update) { try { byte[] updateBytes = Serialization.getJsonMapper().writeValueAsBytes(update); LOG.debug("Pushing update {}:{} to Redis, tag {}", update.getReaderId(), update.getUpdateCount(), update.getTag()); LOG.debug("JSON: {}", LOG.isDebugEnabled() ? Serialization.getJsonMapper().writeValueAsString(update) : null); try { Transaction t = jedis.get(update.getReaderId()).multi(); Response<Long> nextUpdateCount = t.rpush("updates".getBytes(), updateBytes); t.publish(updateChannel.get(update.getReaderId()), String.valueOf(updateCount.get(update.getReaderId()))); t.exec(); updateCount.put(update.getReaderId(), nextUpdateCount.get()); } catch (JedisConnectionException e) { LOG.error("Error pushing update {} to Redis.", update.getUpdateCount(), e); } } catch (JsonProcessingException e) { LOG.error("Error formatting update as JSON", e); } sem.release(); } }