/*
* 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.control;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ulyssis.ipp.control.commands.Command;
import org.ulyssis.ipp.status.StatusMessage;
import org.ulyssis.ipp.utils.JedisHelper;
import org.ulyssis.ipp.utils.Serialization;
import redis.clients.jedis.BinaryJedisPubSub;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.BiConsumer;
public final class CommandDispatcher implements Runnable {
private static final Logger LOG = LogManager.getLogger(CommandDispatcher.class);
/**
* = The result of running a command
*/
public enum Result {
/**
* The command was successfully executed
*/
SUCCESS,
/**
* The command is unsupported by the target
*/
UNSUPPORTED,
/**
* The command failed for some reason
*/
ERROR,
/**
* The command execution timed out.
* <p>
* Note that at any time, a timeout may still be followed by a success result,
* if it was received correctly.
*/
TIMEOUT
}
private static class ProcessingCommand {
Command command;
BiConsumer<Command, Result> callback;
TimerTask timerTask;
ProcessingCommand(Command command, BiConsumer<Command, Result> callback, TimerTask timerTask) {
this.command = command;
this.callback = callback;
this.timerTask = timerTask;
}
}
private final LinkedBlockingQueue<Command> commandsToSend = new LinkedBlockingQueue<>();
private final ConcurrentHashMap<String, ProcessingCommand> processingCommands = new ConcurrentHashMap<>();
private final Timer timeoutTimer = new Timer();
private final URI redisUri;
private final Jedis jedis;
private final byte[] controlChannel;
private final byte[] statusChannel;
public CommandDispatcher(URI redisUri, String controlChannel, String statusChannel) {
this.redisUri = redisUri;
this.jedis = JedisHelper.get(redisUri);
this.controlChannel = JedisHelper.dbLocalChannel(controlChannel, redisUri).getBytes();
this.statusChannel = JedisHelper.dbLocalChannel(statusChannel, redisUri).getBytes();
}
public void run() {
Thread statusThread =
new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
try {
JedisHelper.get(redisUri).subscribe(createResultListener(), statusChannel);
} catch (JedisConnectionException e) {
// TODO: After a while, deregister the processor?
LOG.error("Connection with Redis was broken! Trying again in 0.5s.", e);
Thread.sleep(500L);
}
}
} catch (InterruptedException ignored) {
}
});
statusThread.start();
while (!Thread.interrupted()) {
try {
Command command = commandsToSend.take();
LOG.debug("Sending command {}", command.getCommandId());
jedis.publish(controlChannel, Serialization.getJsonMapper().writeValueAsBytes(command));
} catch (InterruptedException ignored) {
} catch (JsonProcessingException e) {
LOG.error("Error writing command as JSON object", e);
}
}
statusThread.interrupt();
try {
statusThread.join();
} catch (InterruptedException ignored) {
}
}
private BinaryJedisPubSub createResultListener() {
JedisHelper.BinaryCallBackPubSub pubSub = new JedisHelper.BinaryCallBackPubSub();
pubSub.addOnMessageListener(this::onMessage);
return pubSub;
}
private void onMessage(byte[] channel, byte[] message) {
assert(Arrays.equals(channel, statusChannel));
try {
StatusMessage statusMessage = Serialization.getJsonMapper().readValue(message, StatusMessage.class);
StatusMessage.MessageType type = statusMessage.getType();
String commandId = statusMessage.getDetails();
switch (type) {
case COMMAND_COMPLETE:
handleResult(commandId, Result.SUCCESS);
break;
case COMMAND_FAILED:
handleResult(commandId, Result.ERROR);
break;
case COMMAND_UNSUPPORTED:
handleResult(commandId, Result.UNSUPPORTED);
break;
default:
// LOG.debug("Command dispatcher got unsupported message type: {}", type.toString());
}
} catch (IOException e) {
LOG.error("Couldn't read status message: {}", new String(message), e);
}
}
public Result send(Command command) {
final CompletableFuture<Result> future = new CompletableFuture<>();
sendAsync(command, (c, r) -> {
assert (c == command);
future.complete(r);
});
try {
return future.get();
} catch (InterruptedException e) {
return Result.TIMEOUT;
} catch (ExecutionException e) {
LOG.error("We got an ExecutionException. This should not happen.", e.getCause());
return Result.ERROR;
}
}
public void sendAsync(Command command) {
sendAsync(command, (c,r) -> {});
}
public void sendAsync(Command command, BiConsumer<Command, Result> callback) {
TimerTask timerTask = new TimerTask() {
public void run() {
handleResult(command.getCommandId(), Result.TIMEOUT);
}
};
processingCommands.put(command.getCommandId(), new ProcessingCommand(command, callback, timerTask));
timeoutTimer.schedule(timerTask, 10000L);
commandsToSend.add(command);
}
private synchronized void handleResult(String commandId, Result result) {
LOG.debug("Handled command {}", commandId);
if (commandId == null) {
return;
}
ProcessingCommand processingCommand = processingCommands.get(commandId);
if (processingCommand == null) {
return;
}
processingCommands.remove(commandId);
processingCommand.timerTask.cancel();
processingCommand.callback.accept(processingCommand.command, result);
}
}