package io.muoncore.channel.impl; import io.muoncore.channel.Channel; import io.muoncore.channel.ChannelConnection; import io.muoncore.channel.support.Scheduler; import io.muoncore.exception.MuonException; import io.muoncore.message.MuonInboundMessage; import io.muoncore.message.MuonMessage; import io.muoncore.message.MuonMessageBuilder; import io.muoncore.message.MuonOutboundMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Dispatcher; import java.util.concurrent.TimeUnit; import static io.muoncore.transport.TransportEvents.CONNECTION_FAILURE; /** * Implements keep alive behaviour. * * Assumes that there is another KeepAliveChannel down the pipe somewhere. * * If no data moves right within the keep alive period, sends a dedicated keep alive message * * If no data moves left within the keep alive period, will terminate the channel as failed. */ public class KeepAliveChannel implements Channel<MuonOutboundMessage, MuonInboundMessage> { public final static String KEEP_ALIVE_STEP = "keep-alive"; private ChannelConnection<MuonOutboundMessage, MuonInboundMessage> left; private ChannelConnection<MuonInboundMessage, MuonOutboundMessage> right; private ChannelConnection.ChannelFunction<MuonOutboundMessage> leftFunction; private ChannelConnection.ChannelFunction<MuonInboundMessage> rightFunction; private static final long KEEP_ALIVE_TIMEOUT = 5500; private static final long KEEP_ALIVE_PERIOD = 1000; private final Scheduler scheduler; private Scheduler.TimerControl timeoutTimerControl; private Scheduler.TimerControl keepAliveTimerControl; private boolean channelFailed = false; private String protocol; private Logger logger = LoggerFactory.getLogger(getClass()); private long lastMsg; public KeepAliveChannel(Dispatcher dispatcher, String protocol, Scheduler scheduler) { this.protocol = protocol; this.scheduler = scheduler; String leftname = "left"; String rightname = "right"; resetKeepAlivePing(); resetTimeout(); left = new ChannelConnection<MuonOutboundMessage, MuonInboundMessage>() { @Override public void receive(ChannelFunction<MuonInboundMessage> function) { rightFunction = function; } @Override public void send(MuonOutboundMessage message) { if (message == null) { shutdown(); return; } resetKeepAlivePing(); if (leftFunction == null) { throw new MuonException("Other side of the channel [" + rightname + "] is not connected to receive data"); } dispatcher.dispatch(message, msg -> { if (StandardAsyncChannel.echoOut) System.out.println("KeepAliveChannel[" + leftname + " >>>>> " + rightname + "]: Sending " + msg + " to " + leftFunction); leftFunction.apply(message); } , Throwable::printStackTrace); if (message.getChannelOperation() == MuonMessage.ChannelOperation.closed) { shutdown(); } } @Override public void shutdown() { timeoutTimerControl.cancel(); keepAliveTimerControl.cancel(); leftFunction.apply(null); } }; right = new ChannelConnection<MuonInboundMessage, MuonOutboundMessage>() { @Override public void receive(ChannelFunction<MuonOutboundMessage> function) { leftFunction = function; } @Override public void send(MuonInboundMessage message) { if (rightFunction == null) { throw new MuonException("Other side of the channel [" + rightname + "] is not connected to receive data"); } if (message == null) { rightFunction.apply(null); return; } resetTimeout(); if(!KEEP_ALIVE_STEP.equals(message.getStep())) { dispatcher.dispatch(message, msg -> { if (StandardAsyncChannel.echoOut) System.out.println("KeepAliveChannel[" + leftname + " <<<< " + rightname + "]: " + msg + " to " + rightFunction); rightFunction.apply(message); }, Throwable::printStackTrace); } if (message == null || message.getChannelOperation() == MuonMessage.ChannelOperation.closed) { shutdown(); } } @Override public void shutdown() { timeoutTimerControl.cancel(); keepAliveTimerControl.cancel(); } }; } @Override public ChannelConnection<MuonInboundMessage, MuonOutboundMessage> right() { return right; } @Override public ChannelConnection<MuonOutboundMessage, MuonInboundMessage> left() { return left; } private void resetKeepAlivePing() { if (channelFailed) return; if (keepAliveTimerControl != null) { keepAliveTimerControl.cancel(); } keepAliveTimerControl = scheduler.executeIn(KEEP_ALIVE_PERIOD, TimeUnit.MILLISECONDS, () -> { logger.debug("Sending keep alive ping"); left.send(MuonMessageBuilder.fromService("local") .protocol(protocol) .step(KEEP_ALIVE_STEP).build()); }); } private void resetTimeout() { lastMsg = System.currentTimeMillis(); if (channelFailed) return; if (timeoutTimerControl != null) { timeoutTimerControl.cancel(); } timeoutTimerControl = scheduler.executeIn(KEEP_ALIVE_TIMEOUT, TimeUnit.MILLISECONDS, () -> { keepAliveTimerControl.cancel(); logger.debug("Connection has failed to stay alive, last message was received " + (System.currentTimeMillis() - this.lastMsg) + "ms ago, sending failure to protocol level: " + protocol); channelFailed = true; right().send( MuonMessageBuilder.fromService("local") .protocol(protocol) .operation(MuonMessage.ChannelOperation.closed) .step(CONNECTION_FAILURE).buildInbound()); left().send(MuonMessageBuilder.fromService("local") .protocol(protocol) .operation(MuonMessage.ChannelOperation.closed) .step(CONNECTION_FAILURE).build()); }); } }