/* * Copyright (C) 2014-2015 ULYSSIS VZW * * This file is part of i++. * * i++ is free software: you can redistribute it and/or modify * it under the terms of version 3 of the GNU Affero General Public License * as published by the Free Software Foundation. No other versions apply. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> */ package org.ulyssis.ipp.ui.state; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.ulyssis.ipp.config.Config; import org.ulyssis.ipp.control.CommandDispatcher; import org.ulyssis.ipp.processor.Database; import org.ulyssis.ipp.publisher.Score; import org.ulyssis.ipp.snapshot.Snapshot; import org.ulyssis.ipp.status.StatusMessage; import org.ulyssis.ipp.utils.JedisHelper; import org.ulyssis.ipp.utils.Serialization; import redis.clients.jedis.Jedis; import eu.webtoolkit.jwt.WApplication; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.SQLException; import java.util.EnumSet; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; public class SharedState { private static final Logger LOG = LogManager.getLogger(SharedState.class); private final URI redisUri; public URI getRedisURI() { return redisUri; } @FunctionalInterface public interface SnapshotScoreListener { void newSnapshotAndScore(Snapshot snapshot, Score score, boolean newSnapshot); } private final ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); private final Thread dispatcherThread; private final CommandDispatcher commandDispatcher; private final JedisHelper.BinaryCallBackPubSub statusSubscriber = new JedisHelper.BinaryCallBackPubSub(); private final Jedis statusJedis; private final Thread statusThread; private final ConcurrentMap<WApplication, Set<SnapshotScoreListener>> applicationToScoreListeners = new ConcurrentHashMap<>(); private final ConcurrentMap<WApplication, Set<Consumer<StatusMessage>>> statusMessageListeners = new ConcurrentHashMap<>(); private final BiConsumer<byte[], byte[]> onMessageListener = (channel, message) -> { try { final StatusMessage statusMessage = Serialization.getJsonMapper().readValue(message, StatusMessage.class); service.submit(() -> announceStatus(statusMessage)); } catch (IOException e) { LOG.error("Couldn't process status message", e); } }; private Snapshot latestSnapshot; private void resendLatestScore() { if (latestSnapshot == null) return; Score score = new Score(latestSnapshot, false); applicationToScoreListeners.keySet().forEach(app -> { Set<SnapshotScoreListener> snapshotListeners = applicationToScoreListeners.get(app); WApplication.UpdateLock lock = app.getUpdateLock(); try { for (SnapshotScoreListener l : snapshotListeners) { l.newSnapshotAndScore(latestSnapshot, score, false); } app.triggerUpdate(); } catch (Exception e) { LOG.error("Exception", e); } finally { lock.release(); } }); } private void announceStatus(StatusMessage message) { Set<WApplication> apps = new HashSet<>(); apps.addAll(statusMessageListeners.keySet()); apps.addAll(applicationToScoreListeners.keySet()); apps.forEach(app -> { Set<Consumer<StatusMessage>> listeners = statusMessageListeners.get(app); Set<SnapshotScoreListener> snapshotListeners = applicationToScoreListeners.get(app); Snapshot snapshot = null; Score score = null; if (message.getType() == StatusMessage.MessageType.NEW_SNAPSHOT) { try (Connection connection = Database.createConnection(EnumSet.of(Database.ConnectionFlags.READ_ONLY))) { Optional<Snapshot> snapshotOptional = Snapshot.loadLatest(connection); if (snapshotOptional.isPresent()) { snapshot = snapshotOptional.get(); latestSnapshot = snapshot; score = new Score(snapshot, false); } } catch (SQLException e) { LOG.error("Failed to retrieve snapshot due to SQLException", e); } catch (IOException e) { LOG.error("Failure processing snapshot", e); } } WApplication.UpdateLock lock = app.getUpdateLock(); try { if (listeners != null) { listeners.forEach(l -> l.accept(message)); } if (snapshotListeners != null && message.getType() == StatusMessage.MessageType.NEW_SNAPSHOT) { for (SnapshotScoreListener l : snapshotListeners) { l.newSnapshotAndScore(snapshot, score, true); } } app.triggerUpdate(); } finally { lock.release(); } }); } public SharedState(URI redisUri) { this.redisUri = redisUri; commandDispatcher = new CommandDispatcher(redisUri, Config.getCurrentConfig().getControlChannel(), Config.getCurrentConfig().getStatusChannel()); dispatcherThread = new Thread(commandDispatcher); dispatcherThread.start(); try (Connection connection = Database.createConnection(EnumSet.of(Database.ConnectionFlags.READ_ONLY))) { Optional<Snapshot> snapshot = Snapshot.loadLatest(connection); if (snapshot.isPresent()) { latestSnapshot = snapshot.get(); } } catch (SQLException e) { LOG.error("Couldn't get snapshot due to SQLException", e); } catch (IOException e) { LOG.error("Couldn't read snapshot", e); } statusJedis = JedisHelper.get(redisUri); statusSubscriber.addOnMessageListener(onMessageListener); statusThread = new Thread(() -> { statusJedis.subscribe(statusSubscriber, JedisHelper.dbLocalChannel(Config.getCurrentConfig().getStatusChannel(), redisUri).getBytes(StandardCharsets.UTF_8)); }); statusThread.start(); service.scheduleAtFixedRate(this::resendLatestScore, 0L, 200L, TimeUnit.MILLISECONDS); } public void addScoreListener(WApplication app, SnapshotScoreListener listener) { Set<SnapshotScoreListener> listeners = new CopyOnWriteArraySet<>(); listeners.add(listener); listeners = applicationToScoreListeners.putIfAbsent(app, listeners); if (listeners != null) { // TODO: This is actually still a minor race condition, but I don't think we'll ever get here listeners.add(listener); } } public void removeScoreListener(WApplication app, SnapshotScoreListener listener) { Set<SnapshotScoreListener> listeners = applicationToScoreListeners.get(app); if (listeners != null) { // TODO: This is actually still a minor race condition, but I don't think we'll ever get here listeners.remove(listener); } } public void addStatusListener(WApplication app, Consumer<StatusMessage> listener) { Set<Consumer<StatusMessage>> listeners = new CopyOnWriteArraySet<>(); listeners.add(listener); listeners = statusMessageListeners.putIfAbsent(app, listeners); if (listeners != null) { listeners.add(listener); } } public void removeStatusListener(WApplication app, Consumer<StatusMessage> listener) { Set<Consumer<StatusMessage>> listeners = statusMessageListeners.get(app); if (listeners != null) { listeners.remove(listener); } } public void removeApplication(WApplication app) { applicationToScoreListeners.remove(app); statusMessageListeners.remove(app); } public CommandDispatcher getCommandDispatcher() { return commandDispatcher; } public void stop() { service.shutdownNow(); dispatcherThread.interrupt(); try { dispatcherThread.join(); } catch (InterruptedException ignored) { } statusSubscriber.unsubscribe(); statusThread.interrupt(); try { statusThread.join(); } catch (InterruptedException ignored) { } } }