/*
* 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.publisher;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ulyssis.ipp.config.Config;
import org.ulyssis.ipp.processor.Database;
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 redis.clients.jedis.exceptions.JedisConnectionException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.util.EnumSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* A publisher that uses a database as a source, and
* listens on Redis for snapshot updates.
*/
public final class DatabasePublisher extends Publisher implements Runnable {
private static final Logger LOG = LogManager.getLogger(DatabasePublisher.class);
private static final long MS_BETWEEN_REPEATS = 1000L;
private final PublisherOptions options;
private final JedisHelper.BinaryCallBackPubSub pubSub;
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private final Semaphore semaphore = new Semaphore(0);
private Instant lastUpdateTime = Instant.EPOCH;
private void updateCallback(byte[] channel, byte[] message) {
try {
final StatusMessage statusMessage = Serialization.getJsonMapper().readValue(message, StatusMessage.class);
if (statusMessage.getType() == StatusMessage.MessageType.NEW_SNAPSHOT) {
executorService.execute(this::publishLatestScore);
}
} catch (IOException e) {
LOG.error("Couldn't process status message", e);
}
}
private void publishLatestScoreAgain() {
publishLatestScore(true);
}
private void publishLatestScore() {
publishLatestScore(false);
}
private void publishLatestScore(boolean repeat) {
Instant updateTime = Instant.now();
if (repeat && Duration.between(lastUpdateTime, updateTime).toMillis() < MS_BETWEEN_REPEATS / 2) return;
lastUpdateTime = updateTime;
semaphore.release();
}
public DatabasePublisher(PublisherOptions options) {
super(options);
this.options = options;
Database.setDatabaseURI(options.getDatabaseUri());
pubSub = new JedisHelper.BinaryCallBackPubSub();
pubSub.addOnMessageListener(this::updateCallback);
}
@Override
public void run() {
executorService.scheduleAtFixedRate((Runnable) this::publishLatestScoreAgain, 0L, MS_BETWEEN_REPEATS, TimeUnit.MILLISECONDS);
try {
Thread thread = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
try {
Jedis jedis = JedisHelper.get(options.getRedisUri());
jedis.subscribe(pubSub,
JedisHelper.dbLocalChannel(
Config.getCurrentConfig().getStatusChannel(),
options.getRedisUri()
).getBytes(StandardCharsets.UTF_8));
} catch (JedisConnectionException e) {
LOG.error("Error connecting to Redis, retrying...", e);
Thread.sleep(3L);
}
}
} catch (InterruptedException ignored) {}
});
thread.start();
try {
while (!Thread.currentThread().isInterrupted()) {
semaphore.acquire();
semaphore.drainPermits();
try (Connection connection = Database.createConnection(EnumSet.of(Database.ConnectionFlags.READ_ONLY))) {
Snapshot.loadLatest(connection).ifPresent(snapshot -> outputScore(new Score(snapshot, true)));
} catch (SQLException e) {
LOG.error("Failed to fetch snapshot", e);
} catch (IOException e) {
LOG.error("Failed to process snapshot", e);
}
}
} catch (InterruptedException ignored) {}
thread.interrupt();
pubSub.unsubscribe();
thread.join();
} catch (InterruptedException ignored) {}
executorService.shutdownNow();
cleanup();
}
}