/*
* Copyright 2014, The Sporting Exchange Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.betfair.cougar.netutil.nio;
import com.betfair.cougar.netutil.nio.message.*;
import com.betfair.cougar.util.jmx.Exportable;
import com.betfair.cougar.util.jmx.JMXControl;
import org.apache.mina.common.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import static com.betfair.cougar.netutil.nio.NioLogger.LoggingLevel.PROTOCOL;
import static com.betfair.cougar.netutil.nio.NioLogger.LoggingLevel.SESSION;
@ManagedResource
public class CougarProtocol1 extends IoFilterAdapter implements Exportable, ICougarProtocol {
private static final Logger LOG = LoggerFactory.getLogger(CougarProtocol1.class);
private static final KeepAliveMessage KEEP_ALIVE = new KeepAliveMessage();
private final NioLogger nioLogger;
private final byte applicationVersion;
private boolean isServer;
private volatile boolean isEnabled = false;
private final int interval ;
private final int timeout ;
private final AtomicLong heartbeatsMissed = new AtomicLong();
private final AtomicLong heartbeatsSent = new AtomicLong();
private final AtomicLong sessionsCreated = new AtomicLong();
private String lastSessionFrom = null;
public CougarProtocol1(boolean server, NioLogger nioLogger, byte applicationVersion, int keepAliveInterval, int keepAliveTimeout) {
this.isServer = server;
this.nioLogger = nioLogger;
this.applicationVersion = applicationVersion;
this.interval = keepAliveInterval;
this.timeout = keepAliveTimeout;
export(nioLogger.getJmxControl());
}
public void closeSession(IoSession ioSession) {
WriteFuture future = ioSession.write(new DisconnectMessage());
future.addListener(new IoFutureListener() {
@Override
public void operationComplete(IoFuture future) {
future.getSession().close();
}
});
}
@Override
public void sessionOpened(NextFilter nextFilter, IoSession session) throws Exception {
if (!isServer) {
ClientHandshake clientHandshake = new ClientHandshake();
session.setAttribute(ClientHandshake.HANDSHAKE, clientHandshake);
session.write(new ConnectMessage(new byte[] {applicationVersion}));
}
super.sessionOpened(nextFilter,session);
}
@Override
public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
try {
if (status == IdleStatus.WRITER_IDLE) {
nioLogger.log(PROTOCOL, session, "CougarProtocolCodecFilter: sending KEEP_ALIVE");
session.write(KEEP_ALIVE);
heartbeatsSent.incrementAndGet();
} else {
nioLogger.log(PROTOCOL, session, "CougarProtocolCodecFilter: KEEP_ALIVE timeout closing session");
session.close();
heartbeatsMissed.incrementAndGet();
}
} finally {
nextFilter.sessionIdle(session, status);
}
}
@Override
public void sessionCreated(NextFilter nextFilter, IoSession session) throws Exception {
session.setIdleTime(IdleStatus.READER_IDLE, timeout);
session.setIdleTime(IdleStatus.WRITER_IDLE, interval);
nextFilter.sessionCreated(session);
nioLogger.log(SESSION, session, "CougarProtocolCodecFilter: Created session at %s from %s", session.getCreationTime(), session.getRemoteAddress());
sessionsCreated.incrementAndGet();
lastSessionFrom = session.getRemoteAddress().toString();
}
@Override
public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception {
if (message instanceof byte[]) {
super.messageReceived(nextFilter,session,message);
}
else if (message instanceof ProtocolMessage) {
ProtocolMessage protocolMessage = (ProtocolMessage) message;
switch (protocolMessage.getProtocolMessageType()) {
case CONNECT:
if (isEnabled()) {
ConnectMessage connectMessage = (ConnectMessage) protocolMessage;
//As a server, ensure that we support the version the client is expecting us to communicate with
if (Arrays.binarySearch(connectMessage.getApplicationVersions(), applicationVersion) >= 0) {
nioLogger.log(PROTOCOL, session, "CougarProtocolDecoder: ACCEPTing connection request with version %s", applicationVersion);
session.write(new AcceptMessage(applicationVersion));
} else {
//we don't speak your language. goodbye
nioLogger.log(PROTOCOL, session, "CougarProtocolDecoder: REJECTing connection request with versions %s", getAsString(connectMessage.getApplicationVersions()));
LOG.info("REJECTing connection request from session "+session.getRemoteAddress()+" with versions "+ getAsString(connectMessage.getApplicationVersions()));
session.write(new RejectMessage(RejectMessageReason.INCOMPATIBLE_VERSION,new byte[] {applicationVersion}));
}
}
else {
nioLogger.log(PROTOCOL, session, "REJECTing connection request from session %s as service unavailable", session.getReadMessages());
LOG.info("REJECTing connection request from session " + session.getReadMessages() + " as service unavailable");
session.write(new RejectMessage(RejectMessageReason.SERVER_UNAVAILABLE, new byte[] {applicationVersion}));
}
break;
case ACCEPT:
//Client Side - server has accepted our connection request
AcceptMessage acceptMessage = (AcceptMessage) protocolMessage;
if (acceptMessage.getAcceptedVersion() != applicationVersion) {
session.close();
throw new IllegalStateException("Protocol version mismatch - client version is "+applicationVersion+", server has accepted "+acceptMessage.getAcceptedVersion());
}
nioLogger.log(PROTOCOL, session, "CougarProtocolDecoder: ACCEPT received for with version %s", acceptMessage.getAcceptedVersion());
((ClientHandshake)session.getAttribute(ClientHandshake.HANDSHAKE)).accept();
break;
case REJECT:
//Client Side - server has said foxtrot oscar
RejectMessage rejectMessage = (RejectMessage) protocolMessage;
nioLogger.log(PROTOCOL, session, "CougarProtocolDecoder: REJECT received: versions accepted are %s", getAsString(rejectMessage.getAcceptableVersions()));
ClientHandshake handshake = (ClientHandshake)session.getAttribute(ClientHandshake.HANDSHAKE);
if (handshake != null) {
handshake.reject();
}
break;
case KEEP_ALIVE:
//Both sides keep alive received, which is ignored
nioLogger.log(PROTOCOL, session, "CougarProtocolDecoder: KEEP_ALIVE received");
break;
case DISCONNECT:
//Client Side - server doesn't love us anymore
session.setAttribute(ProtocolMessage.ProtocolMessageType.DISCONNECT.name());
session.close();
break;
default:
LOG.error("Unknown message type "+protocolMessage.getProtocolMessageType()+" - Ignoring");
}
}
}
private String getAsString(byte[] versions) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (byte b: versions) {
if (first) {
first = false;
} else {
sb.append(",");
}
sb.append(b);
}
sb.append("}");
return sb.toString();
}
@ManagedAttribute
public void setEnabled(boolean healthy) {
this.isEnabled = healthy;
}
@ManagedAttribute
public boolean isEnabled() {
return this.isEnabled;
}
/**
* Exports this service as an MBean, if the JMXControl is available
*/
@Override
public void export(JMXControl jmxControl) {
if (jmxControl != null) {
jmxControl.registerMBean("CoUGAR.socket.transport:name=wireProtocol", this);
}
}
@ManagedAttribute
public int getInterval() {
return interval;
}
@ManagedAttribute
public int getTimeout() {
return timeout;
}
@ManagedAttribute
public long getHeartbeatsMissed() {
return heartbeatsMissed.get();
}
@ManagedAttribute
public long getHeartbeatsSent() {
return heartbeatsSent.get();
}
@ManagedAttribute
public long getSessionsCreated() {
return sessionsCreated.get();
}
@ManagedAttribute
public String getLastSessionFrom() {
return lastSessionFrom;
}
}