/*
* 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 org.f1x.v1;
import org.f1x.api.FixParserException;
import org.f1x.api.FixSettings;
import org.f1x.api.FixVersion;
import org.f1x.api.message.MessageBuilder;
import org.f1x.api.message.MessageParser;
import org.f1x.api.message.fields.EncryptMethod;
import org.f1x.api.message.fields.FixTags;
import org.f1x.api.message.fields.MsgType;
import org.f1x.api.message.fields.SessionRejectReason;
import org.f1x.api.session.*;
import org.f1x.io.InputChannel;
import org.f1x.io.LoggingOutputChannel;
import org.f1x.io.OutputChannel;
import org.f1x.log.MessageLog;
import org.f1x.log.MessageLogFactory;
import org.f1x.log.file.LogUtils;
import org.f1x.store.EmptyMessageStore;
import org.f1x.store.MessageStore;
import org.f1x.store.SafeMessageStore;
import org.f1x.util.AsciiUtils;
import org.f1x.util.ByteArrayReference;
import org.f1x.util.RealTimeSource;
import org.f1x.util.TimeSource;
import org.f1x.util.timer.GlobalTimer;
import org.f1x.v1.schedule.SessionSchedule;
import org.f1x.v1.state.MemorySessionState;
import org.gflogger.GFLog;
import org.gflogger.GFLogEntry;
import org.gflogger.GFLogFactory;
import org.gflogger.Loggable;
import java.io.IOException;
import java.net.SocketException;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;
/**
* Common networking code for FIX Acceptor and FIX Initiator
*/
public abstract class FixCommunicator implements FixSession, Loggable {
private static final int MIN_FIX_MESSAGE_LENGTH = 63; // min message example: 8=FIX.4.?|9=??|35=?|34=?|49=?|56=?|52=YYYYMMDD-HH:MM:SS|10=???|
static final int CHECKSUM_LENGTH = 7; // length("10=123|") --check sum always expressed using 3 digits
protected static final GFLog LOGGER = GFLogFactory.getLog(FixCommunicator.class);
private SessionEventListener eventListener;
private final FixSettings settings;
private MessageLogFactory messageLogFactory;
private MessageLog messageLog;
protected SessionState sessionState;
// Defined during initialization
private volatile InputChannel in;
private volatile OutputChannel out;
protected MessageStore messageStore;
protected SessionSchedule schedule;
// used by receiver thread only
private final DefaultMessageParser parserForResend = new DefaultMessageParser();
private final ByteArrayReference msgTypeForResend = new ByteArrayReference();
private final byte[] messageBufferForResend;
private final MessageBuilder messageBuilderForResend;
private AtomicReference<SessionStatus> status = new AtomicReference<>(SessionStatus.Disconnected);
/** This flag is set when communicator is going though graceful disconnect initiated by this side.
* We continue to process inbound requests but do not make new attempts to establish socket connections.
* Typically inbound LOGOUT in this state causes disconnect.
*/
protected volatile boolean closeInProgress = false;
// used by receiver thread only
private final DefaultMessageParser parser = new DefaultMessageParser();
private final byte [] inboundMessageBuffer;
private final ByteArrayReference msgType = new ByteArrayReference();
private final byte [] beginString;
// Used by inbound message processing thread only
private final ByteArrayReference temporaryByteArrayReference = new ByteArrayReference();
// Used by senders
private final MessageBuilder sessionMessageBuilder;
private final RawMessageAssembler messageAssembler;
protected final TimeSource timeSource;
private final AtomicReference<TimerTask> sessionMonitoringTask = new AtomicReference<>();
private final AtomicReference<TimerTask> sessionEndTask = new AtomicReference<>();
private final Object sendLock = new Object();
public FixCommunicator (FixVersion fixVersion, FixSettings settings) {
this(fixVersion, settings, RealTimeSource.INSTANCE);
}
protected FixCommunicator (FixVersion fixVersion, FixSettings settings, TimeSource timeSource) {
this.settings = settings;
// this.logInboundMessages = settings.isLogInboundMessages();
// this.logOutboundMessages = settings.isLogOutboundMessages();
// if (logInboundMessages || logOutboundMessages)
// messageLogFactory = new FileMessageLogFactory(settings.getLogDirectory());
// else
// messageLogFactory = null;
this.beginString = AsciiUtils.getBytes(fixVersion.getBeginString());
sessionMessageBuilder = new ByteBufferMessageBuilder(settings.getMaxOutboundMessageSize(), settings.getDoubleFormatterPrecision());
messageBuilderForResend = new ByteBufferMessageBuilder(settings.getMaxOutboundMessageSize(), settings.getDoubleFormatterPrecision());
messageAssembler = new RawMessageAssembler(fixVersion, settings.getMaxOutboundMessageSize(), settings.isSendRequiresConnect());
inboundMessageBuffer = new byte [settings.getMaxInboundMessageSize()];
messageBufferForResend = new byte[settings.getMaxOutboundMessageSize()];
this.timeSource = timeSource;
}
@Override
public MessageBuilder createMessageBuilder() {
return new ByteBufferMessageBuilder(settings.getMaxOutboundMessageSize(), settings.getDoubleFormatterPrecision());
}
@Override
public void setEventListener(SessionEventListener eventListener) {
this.eventListener = eventListener;
}
@Override
public abstract SessionID getSessionID();
@Override
public FixSettings getSettings() {
return settings;
}
@Override
public SessionStatus getSessionStatus() {
return status.get();
}
public void setMessageLogFactory(MessageLogFactory messageLogFactory) {
this.messageLogFactory = messageLogFactory;
}
public void setSessionState(SessionState sessionState){
this.sessionState = sessionState;
}
public void setMessageStore(MessageStore messageStore) {
this.messageStore = messageStore;
}
public void setSessionSchedule(SessionSchedule schedule) {
this.schedule = schedule;
if (schedule != null)
LOGGER.info().append("Session ").append(this).append(" schedule: ").append(schedule).commit();
else
LOGGER.info().append("Session ").append(this).append(" will have 'run-forever' schedule").commit();
}
//@Deprecated // TODO: Switch to use CAS version
protected void setSessionStatus(SessionStatus newStatus) {
final SessionStatus oldStatus = this.status.get();
if (oldStatus == newStatus) {
LOGGER.warn().append(this).append(" Already in the status ").append(newStatus).commit();
} else {
this.status.set(newStatus);
onSessionStatusChanged(oldStatus, newStatus);
}
}
/** Attempts to change state from expected current state to given one (CAS) */
protected boolean setSessionStatus(SessionStatus expectedStatus, SessionStatus newStatus) {
assert expectedStatus != newStatus;
boolean result = this.status.compareAndSet(expectedStatus, newStatus);
if (result) {
onSessionStatusChanged(expectedStatus, newStatus);
}
return result;
}
protected void onSessionStatusChanged(final SessionStatus oldStatus, final SessionStatus newStatus) {
SessionID sessionID = getSessionID();
LOGGER.info().append("Session ").append(this).append(" changed status ").append(oldStatus).append(" => ").append(newStatus).commit();
if (eventListener != null) {
try {
eventListener.onStatusChanged(sessionID, oldStatus, newStatus);
} catch (Throwable e) {
LOGGER.error().append(this).append(" Error notifying about status change ").append(e).commit();
}
}
}
protected final void assertSessionStatus(SessionStatus expectedStatus) {
final SessionStatus actualStatus = getSessionStatus();
if (actualStatus != expectedStatus)
throw new IllegalStateException("Expecting " + expectedStatus + " status instead of " + actualStatus);
}
protected final void assertSessionStatus2(SessionStatus expectedStatus1, SessionStatus expectedStatus2) {
final SessionStatus actualStatus = getSessionStatus();
if (actualStatus != expectedStatus1 && actualStatus != expectedStatus2)
throw new IllegalStateException("Expecting " + expectedStatus1 + " or " + expectedStatus2 + " status instead of " + actualStatus);
}
protected void connect(InputChannel in, OutputChannel out) {
this.messageLog = (messageLogFactory != null) ? messageLogFactory.create(getSessionID()) : null;
this.in = in;
this.out = (messageLog != null) ? new LoggingOutputChannel(messageLog, out) : out;
}
protected void init() {
if (sessionState == null)
sessionState = new MemorySessionState();
messageStore = messageStore == null ?
EmptyMessageStore.getInstance() :
new SafeMessageStore(messageStore);
}
protected void destroy(){
}
/** Process inbound messages until session ends */
protected final boolean processInboundMessages() {
return processInboundMessages(null, 0);
}
/** Process inbound messages until session ends
* @param logonBuffer buffer containing session LOGON message and may be some other messages or a part of them (can be null)
* @param length actual number of bytes that should be consumed from logonBuffer
*/
protected final boolean processInboundMessages(byte[] logonBuffer, int length) {
LOGGER.info().append(this).append("Processing FIX Session").commit();
boolean normalExit = false;
try {
int offset = 0;
if (logonBuffer != null) {
System.arraycopy(logonBuffer, 0, inboundMessageBuffer, 0, length);
offset = processInboundMessages(length);
}
while (in != null) {
int bytesRead = in.read(inboundMessageBuffer, offset, inboundMessageBuffer.length - offset);
if (bytesRead <= 0) {
if (closeInProgress){
disconnect("No socket data"); // TODO: disconnect?
break;
}
throw ConnectionProblemException.NO_SOCKET_DATA;
} else {
offset = processInboundMessages(offset + bytesRead);
}
}
LOGGER.error().append(this).append("Finishing FIX session").commit();
normalExit = true;
} catch (InvalidFixMessageException e) {
errorProcessingMessage("Protocol Error", e, false);
} catch (ConnectionProblemException e) {
errorProcessingMessage("Connection Problem", e, false);
} catch (SocketException e) {
errorProcessingMessage("Socket Error (Other side disconnected?)", e, false);
} catch (Exception e) {
errorProcessingMessage("General error", e, true);
}
assertSessionStatus(SessionStatus.Disconnected);
return normalExit;
}
protected void errorProcessingMessage(String errorText, Exception e, boolean logStackTrace) {
//if (active) {
if (logStackTrace)
LOGGER.error().append(this).append(errorText).append(" : ").append(e).commit();
else
LOGGER.error().append(this).append(errorText).append(" : ").append(e.getMessage()).commit();
disconnect(errorText);
//}
}
protected int processInboundMessages(int bytesRead) throws IOException, InvalidFixMessageException, ConnectionProblemException {
assert bytesRead > 0;
int messageStart = 0;
int readMessageLength;
while ((readMessageLength = bytesRead - messageStart) >= MIN_FIX_MESSAGE_LENGTH) {
parser.set(inboundMessageBuffer, messageStart, readMessageLength);
// All FIX messages begin with 3 required tags: BeginString, BodyLength, and MsgType.
FixCommunicatorHelper.parseBeginString(parser, beginString);
final int bodyLength = FixCommunicatorHelper.parseBodyLength(parser);
final int msgTypeStart = parser.getOffset();
final int lengthOfBeginStringAndBodyLength = msgTypeStart - messageStart;
final int messageLength = lengthOfBeginStringAndBodyLength + bodyLength + CHECKSUM_LENGTH; // BodyLength is the number of characters in the message following the BodyLength field up to, and including, the delimiter immediately preceding the CheckSum tag ("10=123|")
FixCommunicatorHelper.checkMessageLength(messageLength, inboundMessageBuffer.length);
if (readMessageLength < messageLength)
break; // retry after we read full message in the buffer
parser.set(inboundMessageBuffer, msgTypeStart, bodyLength);
if ( ! parser.next())
throw InvalidFixMessageException.MISSING_MSG_TYPE;
parser.getByteSequence(msgType);
if (messageLog != null)
messageLog.log(true, inboundMessageBuffer, messageStart, messageLength);
final int msgSeqNum = FixCommunicatorHelper.findMsgSeqNum(parser);
// set parser limit to consume single message
parser.set(inboundMessageBuffer, messageStart, messageLength);
processInboundMessage(parser, msgType, msgSeqNum);
messageStart += messageLength; // go to next message
}
// Move remaining part at the beginning of buffer
int remainingSize = bytesRead - messageStart;
if (remainingSize > 0 && messageStart != 0)
System.arraycopy(inboundMessageBuffer, messageStart, inboundMessageBuffer, 0, remainingSize);
return remainingSize;
}
/** Send LOGOUT but do not drop socket connection (session enters {@link org.f1x.api.session.SessionStatus#InitiatedLogout} state)*/
@Override
public void logout(CharSequence cause) {
sendLogout(cause);
}
/**
* Terminate socket connection immediately (no LOGOUT message is sent if session is in process).
* Session enters {@link org.f1x.api.session.SessionStatus#Disconnected} state.
* Initiator will try to re-connect after little delay (delay is configurable, subject to session schedule).
*/
@Override
public void disconnect(CharSequence cause) {
LOGGER.info().append(this).append("FIX Disconnect due to ").append(cause).commit();
setSessionStatus(SessionStatus.Disconnected);
long now = timeSource.currentTimeMillis();
sessionState.setLastConnectionTimestamp(now);
try {
if (in != null) { //TODO: Volatile
in.close();
in = null;
}
if (out != null) {
out.close();
out = null;
}
if (messageLog != null) {
messageLog.close();
messageLog = null;
}
} catch (IOException e) {
LOGGER.warn().append(this).append("Error closing socket: ").append(e).commit();
}
}
/** Logout current session (if needed) and terminate socket connection. */
@Override
public void close() {
this.closeInProgress = true;
sendLogout("Goodbye");
}
/**
* Sends a message using next sequence number. This message is persisted in message store
*/
@Override
public void send(MessageBuilder messageBuilder) throws IOException {
long now = timeSource.currentTimeMillis();
synchronized (sendLock) {
int msgSeqNum = sessionState.consumeNextSenderSeqNum();
messageAssembler.send(getSessionID(), msgSeqNum, messageBuilder, messageStore, now, out);
}
sessionState.setLastSentMessageTimestamp(now);
}
/**
* Resend a message with given sequence number. The message is not persisted in message store.
*/
protected void resend(MessageBuilder messageBuilder, int forcedMsgSeqNum) throws IOException {
long now = timeSource.currentTimeMillis();
synchronized (sendLock) {
messageAssembler.send(getSessionID(), forcedMsgSeqNum, messageBuilder, null, now, out);
}
sessionState.setLastSentMessageTimestamp(now);
}
/** @param forceResetSequenceNumbers pass true to force sequence numbers reset, otherwise implementation will rely on {@link org.f1x.api.FixSettings#isResetSequenceNumbersOnEachLogon()} */
protected void sendLogon(boolean forceResetSequenceNumbers) throws IOException {
if ( ! forceResetSequenceNumbers)
forceResetSequenceNumbers = settings.isResetSequenceNumbersOnEachLogon();
synchronized (sessionMessageBuilder) {
sessionMessageBuilder.clear();
sessionMessageBuilder.setMessageType(MsgType.LOGON);
sessionMessageBuilder.add(FixTags.EncryptMethod, EncryptMethod.NONE_OTHER);
sessionMessageBuilder.add(FixTags.HeartBtInt, settings.getHeartBeatIntervalSec());
sessionMessageBuilder.add(FixTags.ResetSeqNumFlag, forceResetSequenceNumbers);
if (settings.isLogonWithNextExpectedMsgSeqNum())
sessionMessageBuilder.add(FixTags.NextExpectedMsgSeqNum, sessionState.getNextTargetSeqNum());
sessionMessageBuilder.add(FixTags.MaxMessageSize, settings.getMaxInboundMessageSize());
synchronized (sendLock) {
if (forceResetSequenceNumbers) {
sessionState.setNextSenderSeqNum(1);
messageStore.clean();
}
send(sessionMessageBuilder);
}
}
}
protected void sendLogout(CharSequence cause) {
if (setSessionStatus(SessionStatus.ApplicationConnected, SessionStatus.InitiatedLogout)) {
LOGGER.info().append(this).append("Initiating LOGOUT: ").append(cause).commit();
try {
synchronized (sessionMessageBuilder) {
sessionMessageBuilder.clear();
sessionMessageBuilder.setMessageType(MsgType.LOGOUT);
if (cause != null)
sessionMessageBuilder.add(FixTags.Text, cause);
send(sessionMessageBuilder);
}
} catch (IOException e) {
LOGGER.warn().append(this).append("Error logging out from FIX session: ").append(e).commit();
}
} else {
LOGGER.info().append(this).append("Skipping LOGOUT (Not connected)").commit();
}
}
/**
* Sends FIX Heartbeat(0) message
* @param testReqId required when heartbeat is sent in response to TestRequest(1)
*/
protected void sendHeartbeat(CharSequence testReqId) throws IOException {
assertSessionStatus(SessionStatus.ApplicationConnected);
synchronized (sessionMessageBuilder) {
sessionMessageBuilder.clear();
sessionMessageBuilder.setMessageType(MsgType.HEARTBEAT);
if (testReqId!= null)
sessionMessageBuilder.add(FixTags.TestReqID, testReqId);
send(sessionMessageBuilder);
}
}
/**
* Sends FIX TestRequest(1) message.
* @param testReqId Verifies that the opposite application is generating the heartbeat as the result of Test Request (1) and not a normal timeout.
* The opposite application includes the TestReqID (112) in the resulting Heartbeat(0).
* Any string can be used as the TestReqID (112) (one suggestion is to use a timestamp string).
*/
protected void sendTestRequest(CharSequence testReqId) throws IOException {
assertSessionStatus(SessionStatus.ApplicationConnected);
synchronized (sessionMessageBuilder) {
sessionMessageBuilder.clear();
sessionMessageBuilder.setMessageType(MsgType.TEST_REQUEST);
sessionMessageBuilder.add(FixTags.TestReqID, testReqId);
send(sessionMessageBuilder);
}
}
/**
* @param rejectedMsgSeqNum MsgSeqNum(34) of rejected message
* This method sends FIX Reject(3).
* @param rejectReason optional reject reason
* @param text optional explanation message
*/
protected void sendReject(int rejectedMsgSeqNum, SessionRejectReason rejectReason, CharSequence text) throws IOException {
assertSessionStatus(SessionStatus.ApplicationConnected);
synchronized (sessionMessageBuilder) {
sessionMessageBuilder.clear();
sessionMessageBuilder.setMessageType(MsgType.REJECT);
sessionMessageBuilder.add(FixTags.RefSeqNum, rejectedMsgSeqNum);
if (rejectReason != null)
sessionMessageBuilder.add(FixTags.SessionRejectReason, rejectReason);
if (text != null)
sessionMessageBuilder.add(FixTags.Text, text);
send(sessionMessageBuilder);
}
}
/**
* This method sends ResendRequest(2).
* @param beginSeqNo start of range to resend (inclusive)
* @param endSeqNo end of range to resend (inclusive). Zero means infinity (resend up to the latest).
*/
protected void sendResendRequest(int beginSeqNo, int endSeqNo) throws IOException {
LOGGER.warn().append(this).append("Requesting RESEND from ").append(beginSeqNo).append(" to ").append(endSeqNo).commit();
assertSessionStatus2(SessionStatus.ApplicationConnected, SessionStatus.InitiatedLogout);
synchronized (sessionMessageBuilder) {
sessionMessageBuilder.clear();
sessionMessageBuilder.setMessageType(MsgType.RESEND_REQUEST);
sessionMessageBuilder.add(FixTags.BeginSeqNo, beginSeqNo);
sessionMessageBuilder.add(FixTags.EndSeqNo, endSeqNo-1);
send(sessionMessageBuilder);
}
}
/**
* Sends SequenceReset(4) in response to ResendRequest when
* resending a range of administrative messages or when resending actual application messages is not appropriate (e.g. stale messages).
*
* @param msgSeqNum message sequence number of this message
* @param newSeqNo new sequence number
*/
protected void sendGapFill(int msgSeqNum, int newSeqNo) throws IOException {
assertSessionStatus2(SessionStatus.ApplicationConnected, SessionStatus.InitiatedLogout);
synchronized (sessionMessageBuilder) {
sessionMessageBuilder.clear();
sessionMessageBuilder.setMessageType(MsgType.SEQUENCE_RESET);
sessionMessageBuilder.add(FixTags.PossDupFlag, true);
sessionMessageBuilder.add(FixTags.NewSeqNo, newSeqNo);
sessionMessageBuilder.add(FixTags.GapFillFlag, true);
resend(sessionMessageBuilder, msgSeqNum);
}
}
/**
* Sends SequenceReset(4) in response to ResendRequest when
* resending a range of administrative messages or when resending actual application messages is not appropriate (e.g. stale messages).
* Sets sender message sequence num to newSeqNo
* @param newSeqNo new sequence number
*/
protected void sendSequenceReset(int newSeqNo) throws IOException {
assertSessionStatus(SessionStatus.ApplicationConnected);
synchronized (sessionMessageBuilder) {
sessionMessageBuilder.clear();
sessionMessageBuilder.setMessageType(MsgType.SEQUENCE_RESET);
sessionMessageBuilder.add(FixTags.NewSeqNo, newSeqNo);
sessionMessageBuilder.add(FixTags.GapFillFlag, false);
synchronized (sendLock) {
sessionState.setNextSenderSeqNum(newSeqNo - 1);
send(sessionMessageBuilder); // In reset mode MsgSeqNum should be ignored
}
}
}
protected void processInboundMessage(MessageParser parser, CharSequence msgType, int msgSeqNumX) throws IOException, InvalidFixMessageException, ConnectionProblemException {
long now = timeSource.currentTimeMillis();
sessionState.setLastReceivedMessageTimestamp(now); //? maybe extract from message SendingTime(52) field?
SessionStatus currentStatus = getSessionStatus();
switch (currentStatus) {
case ApplicationConnected:
case InitiatedLogout:
processInSessionMessage(msgSeqNumX, msgType, parser);
break;
case SocketConnected:
if (FixCommunicatorHelper.isLogon(msgType))
processInboundLogon(msgSeqNumX, parser);
else
throw InvalidFixMessageException.EXPECTING_LOGON_MESSAGE;
break;
case InitiatedLogon:
if(FixCommunicatorHelper.isLogon(msgType))
processInboundLogon(msgSeqNumX, parser);
else if(FixCommunicatorHelper.isLogout(msgType))
processInboundLogout(msgSeqNumX, parser);
else
throw InvalidFixMessageException.EXPECTING_LOGON_MESSAGE;
break;
default:
LOGGER.warn().append(this).append("Received unexpected message (35=").append(msgType).append(") in status ").append(currentStatus).commit();
}
}
private void processInSessionMessage(int msgSeqNumX, CharSequence msgType, MessageParser parser) throws IOException, InvalidFixMessageException, ConnectionProblemException {
boolean processed = true;
if (msgType.length() == 1) { // All session-level messages have MsgType expressed using single char
switch (msgType.charAt(0)) {
case AdminMessageTypes.LOGON:
processInboundLogon(msgSeqNumX, parser); break;
case AdminMessageTypes.LOGOUT:
processInboundLogout(msgSeqNumX, parser); break;
case AdminMessageTypes.HEARTBEAT:
processInboundHeartbeat(msgSeqNumX, parser);
break;
case AdminMessageTypes.TEST:
processInboundTestRequest(msgSeqNumX, parser); break;
case AdminMessageTypes.RESEND:
processInboundResendRequest(msgSeqNumX, parser); break;
case AdminMessageTypes.REJECT:
processInboundReject(msgSeqNumX, parser); break;
case AdminMessageTypes.RESET:
processInboundSequenceReset(msgSeqNumX, parser); break;
default:
processed = false;
}
} else {
processed = false;
}
if ( ! processed)
_processInboundAppMessage(msgType, msgSeqNumX, parser);
}
/**
* @param msgSeqNumX message sequence number (negative for messages that have PossDupFlag=Y).
*/
private void _processInboundAppMessage(CharSequence msgType, int msgSeqNumX, MessageParser parser) throws IOException, InvalidFixMessageException {
LOGGER.debug().append(this).append("Processing inbound message with type: ").append(msgType).commit();
final boolean possDup;
if (msgSeqNumX > 0) { // PossDupFlag=N
int expectedTargetSeqNum = sessionState.getNextTargetSeqNum();
if ( ! checkTargetMsgSeqNum(msgSeqNumX, expectedTargetSeqNum)) //Let's imagine we expected MsgSeqNum=5 but received 10
sendResendRequest(expectedTargetSeqNum, msgSeqNumX - 1); //This will send ResendRequest(5, 0) and set currentResendEndSeqNo=9
sessionState.setNextTargetSeqNum(msgSeqNumX + 1);
possDup = false;
} else {
msgSeqNumX = -msgSeqNumX;
possDup = true;
}
processInboundAppMessage(msgType, msgSeqNumX, possDup, parser);
}
/**
*
* @param msgType type of the message [tag MsgType(35)].
* @param msgSeqNum message sequence number [tag MsgSeqNum(34)].
* @param possDup <code>true</code> if this message is marked as a duplicate using tag PossDupFlag(43)
*/
protected void processInboundAppMessage(CharSequence msgType, int msgSeqNum, boolean possDup, MessageParser parser) throws IOException {
assert msgSeqNum > 0; // by default do nothing
}
/**
* Handle inbound LOGON message depending on FIX session role (acceptor/initator) and current status
*/
protected void processInboundLogon(int msgSeqNum, MessageParser parser) throws IOException, InvalidFixMessageException, ConnectionProblemException {
LOGGER.debug().append(this).append("Processing inbound LOGON(A)").commit();
if (msgSeqNum < 0) {
LOGGER.warn().append(this).append("Received LOGON(A) message with PossDupFlag=Y - ignoring. MsgSeqNum ").append(-msgSeqNum).commit();
return;
}
boolean heartbeatIntervalPresent = false;
boolean resetSeqNum = false;
while (parser.next()) {
switch (parser.getTagNum()) {
case FixTags.HeartBtInt:
if (parser.getIntValue() != settings.getHeartBeatIntervalSec())
throw ConnectionProblemException.HEARTBEAT_INTERVAL_MISMATCH; //TODO: Allow initiator to override heartbeat interval for acceptor
heartbeatIntervalPresent = true;
break;
case FixTags.ResetSeqNumFlag:
resetSeqNum = parser.getBooleanValue();
//if (getSessionStatus() != SessionStatus.ApplicationConnected) // Unless we are dealing with In-Session sequence reset
if(resetSeqNum && msgSeqNum != 1)
throw InvalidFixMessageException.MSG_SEQ_NUM_MUST_BE_ONE;
break;
default:
processCustomLogonTag(parser);
}
}
if (!heartbeatIntervalPresent)
throw InvalidFixMessageException.NO_HEARTBEAT_INTERVAL;
if (resetSeqNum)
sessionState.setNextTargetSeqNum(1); //TODO: This is wrong 141=Y means that both sides should reset sequence numbers
SessionStatus currentStatus = getSessionStatus();
if (currentStatus == SessionStatus.ApplicationConnected && !resetSeqNum)
throw InvalidFixMessageException.IN_SESSION_LOGON_MESSAGE_WITHOUT_MSG_SEQ_RESET_NOT_EXPECTED;
int expectedTargetSeqNum = sessionState.getNextTargetSeqNum();
boolean expectedTargetMsgSeqNum = checkTargetMsgSeqNum(msgSeqNum, expectedTargetSeqNum);
sessionState.setNextTargetSeqNum(msgSeqNum + 1);
if (currentStatus == SessionStatus.SocketConnected) {
setSessionStatus(SessionStatus.ReceivedLogon);
sendLogon(resetSeqNum);
setSessionStatus(SessionStatus.ApplicationConnected);
} else if (currentStatus == SessionStatus.InitiatedLogon) {
setSessionStatus(SessionStatus.ApplicationConnected);
} else if (currentStatus == SessionStatus.ApplicationConnected) {
sendLogon(resetSeqNum);
} else {
LOGGER.warn().append(this).append("Unexpected LOGON(A) in status: ").append(currentStatus).commit();
return;
}
// *After* sending a Logon confirmation back, send a ResendRequest
if ( ! expectedTargetMsgSeqNum)
sendResendRequest(expectedTargetSeqNum, msgSeqNum);
}
/** Can be used to validate custom logon tags (e.g. Password(443) tag) */
protected void processCustomLogonTag(MessageParser parser) {
// by default does nothing
}
@SuppressWarnings("unused")
protected void processInboundLogout(int msgSeqNumX, MessageParser parser) throws IOException, InvalidFixMessageException {
LOGGER.debug().append(this).append("Processing inbound LOGOUT(5)").commit();
if (msgSeqNumX < 0) {
LOGGER.warn().append(this).append("Received LOGOUT(5) message with PossDupFlag=Y - ignoring. MsgSeqNum ").append(-msgSeqNumX).commit();
return;
}
int expectedTargetSeqNum = sessionState.getNextTargetSeqNum();
boolean expectedTargetMsgSeqNum = checkTargetMsgSeqNum(msgSeqNumX, expectedTargetSeqNum);
temporaryByteArrayReference.clear();
while (parser.next()) {
if (parser.getTagNum() == FixTags.Text) {
parser.getByteSequence(temporaryByteArrayReference);
break;
}
}
LOGGER.info().append(this).append("LOGOUT(5) received: ").append(temporaryByteArrayReference).commit();
SessionStatus currentStatus = getSessionStatus();
if (currentStatus == SessionStatus.ApplicationConnected) {
sessionState.setNextTargetSeqNum(msgSeqNumX + 1);
// If a message gap was detected, issue a ResendRequest to retrieve all missing messages followed by a Logout message which serves as a confirmation of the logout request.
// DO NOT terminate the session. The initiator of the Logout sequence has responsibility to terminate the session.
// This allows the Logout initiator to respond to any ResendRequest message.
if ( ! expectedTargetMsgSeqNum)
sendResendRequest(expectedTargetSeqNum, msgSeqNumX);
sendLogout("Responding to LOGOUT(5) request");
if ( expectedTargetMsgSeqNum)
setSessionStatus(SessionStatus.SocketConnected);
} else if (currentStatus == SessionStatus.InitiatedLogout) {
if (expectedTargetMsgSeqNum)
sessionState.consumeNextTargetSeqNum();
// If this side was the initiator of the Logout sequence,
// then this is a Logout confirmation and the session should be immediately terminated upon receipt.
disconnect("Both sides exchanged LOGOUT(5)");
} else if (currentStatus == SessionStatus.InitiatedLogon){
if (expectedTargetMsgSeqNum)
sessionState.consumeNextTargetSeqNum();
disconnect("LOGON(A) rejected");
} else {
LOGGER.info().append(this).append("Unexpected LOGOUT(5) message in status: ").append(currentStatus).commit();
}
}
@SuppressWarnings("unused")
protected void processInboundHeartbeat(int msgSeqNumX, MessageParser parser) throws InvalidFixMessageException, IOException {
LOGGER.debug().append(this).append("Processing Inbound HEARTBEAT(0)").commit();
if (msgSeqNumX < 0) {
LOGGER.warn().append(this).append("Received HEARTBEAT(0) message with PossDupFlag=Y - ignoring. MsgSeqNum ").append(-msgSeqNumX).commit();
return;
}
int expectedTargetSeqNum = sessionState.getNextTargetSeqNum();
if( ! checkTargetMsgSeqNum(msgSeqNumX, expectedTargetSeqNum))
sendResendRequest(expectedTargetSeqNum, msgSeqNumX);
sessionState.setNextTargetSeqNum(msgSeqNumX + 1);
}
protected void processInboundTestRequest(int msgSeqNumX, MessageParser parser) throws IOException, InvalidFixMessageException {
LOGGER.debug().append("Processing inbound TEST(1) request").commit();
if (msgSeqNumX < 0) {
LOGGER.warn().append(this).append("Received TEST(1) request with PossDupFlag=Y - ignoring. MsgSeqNum ").append(-msgSeqNumX).commit();
return;
}
int expectedTargetSeqNum = sessionState.getNextTargetSeqNum();
boolean expectedTargetMsgSeqNum = checkTargetMsgSeqNum(msgSeqNumX, expectedTargetSeqNum);
sessionState.setNextTargetSeqNum(msgSeqNumX + 1);
if (expectedTargetMsgSeqNum) {
temporaryByteArrayReference.clear();
while (parser.next()) {
if (parser.getTagNum() == FixTags.TestReqID) {
parser.getByteSequence(temporaryByteArrayReference);
break;
}
}
if (temporaryByteArrayReference.length() == 0)
sendReject(msgSeqNumX, SessionRejectReason.REQUIRED_TAG_MISSING, "Missing TestReqID(112)");
else
sendHeartbeat(temporaryByteArrayReference);
} else {
sendResendRequest(expectedTargetSeqNum, msgSeqNumX);
}
}
private void processInboundResendRequest(int msgSeqNumX, MessageParser parser) throws IOException, InvalidFixMessageException {
LOGGER.debug().append("Processing inbound RESEND(2) request").commit();
int expectedTargetSeqNum = sessionState.getNextTargetSeqNum();
if (msgSeqNumX < 0) {
LOGGER.warn().append(this).append("Received RESEND(2) message with PossDupFlag=Y - ignoring. MsgSeqNum ").append(-msgSeqNumX).commit();
return;
}
boolean expectedTargetMsgSeqNum = checkTargetMsgSeqNum(msgSeqNumX, expectedTargetSeqNum);
sessionState.setNextTargetSeqNum(msgSeqNumX + 1);
int beginSeqNo = -1;
int endSeqNo = -1;
while (parser.next()) {
switch (parser.getTagNum()) {
case FixTags.BeginSeqNo: // required
beginSeqNo = parser.getIntValue();
break;
case FixTags.EndSeqNo: // required
endSeqNo = parser.getIntValue();
break;
}
}
// If message gap is detected, perform the Resend processing first, followed by a ResendRequest of your own in order to fill the incoming message gap.
if (beginSeqNo == -1)
sendReject(msgSeqNumX, SessionRejectReason.REQUIRED_TAG_MISSING, "Missing BeginSeqNo(7)");
else if (beginSeqNo == 0)
sendReject(msgSeqNumX, SessionRejectReason.VALUE_IS_INCORRECT, "Invalid BeginSeqNo(7)");
else if (endSeqNo == -1)
sendReject(msgSeqNumX, SessionRejectReason.REQUIRED_TAG_MISSING, "Missing EndSeqNo(16)");
else
resendMessages(beginSeqNo, endSeqNo != 0 ? endSeqNo : (sessionState.getNextSenderSeqNum() - 1));
if( ! expectedTargetMsgSeqNum)
sendResendRequest(expectedTargetSeqNum, msgSeqNumX);
}
protected void resendMessages(int beginSeqNo, int endSeqNo) throws IOException {
if(beginSeqNo > endSeqNo){
LOGGER.warn().append(this).append("Resending messages was skipped: beginSeqNo > endSeqNo").commit();
return;
}
MessageStore.MessageStoreIterator iterator = messageStore.iterator(beginSeqNo, endSeqNo);
int msgSeqNumOfLastResentMessage = beginSeqNo - 1;
int msgSeqNum;
while ((msgSeqNum = iterator.next(messageBufferForResend)) > 0) {
if(resend(messageBufferForResend, msgSeqNum, msgSeqNumOfLastResentMessage))
msgSeqNumOfLastResentMessage = msgSeqNum;
}
if (msgSeqNumOfLastResentMessage < endSeqNo)
sendGapFill(msgSeqNumOfLastResentMessage + 1, endSeqNo + 1);
}
/**
* @return true if message was resent otherwise false
*/
private boolean resend(byte[] message, int msgSeqNum, int msgSeqNumOfLastResentMessage) throws IOException {
try {
parserForResend.set(message, 0 , message.length);
FixCommunicatorHelper.parseBeginString(parserForResend, beginString);
final int bodyLength = FixCommunicatorHelper.parseBodyLength(parserForResend);
final int lengthOfBeginStringAndBodyLength = parserForResend.getOffset();
final int messageLength = lengthOfBeginStringAndBodyLength + bodyLength + CHECKSUM_LENGTH;
if (!parserForResend.next())
throw InvalidFixMessageException.MISSING_MSG_TYPE;
parserForResend.getByteSequence(msgTypeForResend);
parserForResend.set(message, 0, messageLength);
messageBuilderForResend.clear();
if( ! onMessageResend(msgTypeForResend, parserForResend, messageBuilderForResend))
return false;
int msgSeqNumGap = msgSeqNum - msgSeqNumOfLastResentMessage;
if(msgSeqNumGap > 1)
sendGapFill(msgSeqNumOfLastResentMessage + 1, msgSeqNum);
resend(messageBuilderForResend, msgSeqNum);
return true;
} catch (InvalidFixMessageException | FixParserException e) {
LOGGER.warn().append(this).append("Got invalid message #").append(msgSeqNum).append(" from message store : ").append(e).commit();
return false;
}
}
/**
* @param parser of resending message
* @param messageBuilder that will be sent
* @return true if a message requires the resending otherwise false
*/
protected boolean onMessageResend(CharSequence msgType, MessageParser parser, MessageBuilder messageBuilder) {
if(!isResendRequired(msgType))
return false;
messageBuilder.setMessageType(msgType);
messageBuilder.add(FixTags.PossDupFlag, true);
while (parser.next()) {
int tagNum = parser.getTagNum();
switch (tagNum) {
case FixTags.MsgType:
case FixTags.MsgSeqNum:
case FixTags.BeginString:
case FixTags.BodyLength:
case FixTags.SenderCompID:
case FixTags.SenderSubID:
case FixTags.TargetCompID:
case FixTags.TargetSubID:
case FixTags.CheckSum:
break;
case FixTags.SendingTime:
messageBuilder.add(FixTags.OrigSendingTime, parser.getCharSequenceValue());
break;
default:
messageBuilder.add(tagNum, parser.getCharSequenceValue());
}
}
return true;
}
/**
* @return true if a message with this msgType requires the resending otherwise false
*/
protected boolean isResendRequired(CharSequence msgType){
return ! AdminMessageTypes.isAdmin(msgType) || msgType.charAt(0) == AdminMessageTypes.REJECT; // do not resend Admin message unless it is a REJECT(3)
}
private void processInboundSequenceReset(int msgSeqNumX, MessageParser parser) throws IOException, InvalidFixMessageException {
LOGGER.debug().append(this).append("Processing inbound Sequence Reset").commit();
boolean isGapFill = false;
int newSeqNum = -1;
while (parser.next()) {
switch (parser.getTagNum()) {
case FixTags.NewSeqNo: // required
newSeqNum = parser.getIntValue();
break;
case FixTags.GapFillFlag:
isGapFill = parser.getBooleanValue();
break;
}
}
if (msgSeqNumX < 0) {
// Normal for gap fill to have PossDupFlag=Y
if (isGapFill)
LOGGER.debug().append(this).append("Ignoring GapFill from").append(-msgSeqNumX).append(" to ").append(newSeqNum).commit();
else
LOGGER.info().append(this).append("Received RESET message with PossDupFlag=Y - ignoring. MsgSeqNum ").append(-msgSeqNumX).commit();
return;
}
LOGGER.info().append(this).append("Processing inbound message sequence reset to ").append(newSeqNum).commit();
//noinspection StatementWithEmptyBody
if (isGapFill) {
int expectedTargetSeqNum = sessionState.getNextTargetSeqNum();
if( ! checkTargetMsgSeqNum(msgSeqNumX, expectedTargetSeqNum)){
sendResendRequest(expectedTargetSeqNum, msgSeqNumX);
}
} else {
// If message gap is detected Ignore the incoming sequence number.
// The NewSeqNo field of the SeqReset message will contain the sequence number of the next message to be transmitted.
}
try {
if (newSeqNum <= sessionState.getNextTargetSeqNum())
throw InvalidFixMessageException.RESET_BELOW_CURRENT_SEQ_LARGE;
sessionState.setNextTargetSeqNum(newSeqNum);
} catch (InvalidFixMessageException e) {
sendReject(msgSeqNumX, SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE, e.getMessage());
}
}
protected void processInboundReject(int msgSeqNumX, MessageParser parser) throws InvalidFixMessageException, IOException {
LOGGER.debug().append(this).append("Processing inbound REJECT(3)").commit();
// Skip sequence number checking if we are dealing with GapFill
if (msgSeqNumX > 0) {
int expectedTargetSeqNum = sessionState.getNextTargetSeqNum();
if ( ! checkTargetMsgSeqNum(msgSeqNumX, expectedTargetSeqNum)) {
sendResendRequest(expectedTargetSeqNum, msgSeqNumX);
}
sessionState.setNextTargetSeqNum(msgSeqNumX + 1);
}
int refSeqNum = -1;
temporaryByteArrayReference.clear();
while (parser.next()) {
switch (parser.getTagNum()) {
case FixTags.RefSeqNum:
refSeqNum = parser.getIntValue();
break;
case FixTags.Text:
parser.getByteSequence(temporaryByteArrayReference);
break;
}
}
// NOTE: some brokers use session-level REJECT to reject abnormal app-level messages
//This default implementation simply log REJECT messages:
if (temporaryByteArrayReference.length() != 0)
LOGGER.warn().append(this).append("Received REJECT(3):").append(refSeqNum).append(": ").append(temporaryByteArrayReference).commit();
else
LOGGER.warn().append(this).append("Received REJECT(3):").append(refSeqNum).commit();
}
/// Timers
/** Schedules a timer to finish current FIX session according to FIX Session Schedule */
protected void scheduleSessionEnd(long timeout) {
SessionEndTask timer = new SessionEndTask(this);
GlobalTimer.getInstance().schedule(timer, timeout);
sessionEndTask.set(timer);
}
/** Cancels a timer that was defined to finish current FIX session (if defined) */
protected void unscheduleSessionEnd() {
TimerTask timer = sessionEndTask.getAndSet(null);
if (timer != null)
timer.cancel();
}
protected synchronized void scheduleSessionMonitoring() {
int checkIntervalMs = settings.getHeartbeatCheckIntervalMs();
if (checkIntervalMs > 0) {
if (sessionMonitoringTask.get() == null) {
SessionMonitoringTask timer = new SessionMonitoringTask(this);
GlobalTimer.getInstance().schedule(timer, checkIntervalMs, checkIntervalMs);
sessionMonitoringTask.set(timer);
} else {
LOGGER.warn().append(this).append("Monitoring task already defined").commit();
}
}
}
protected synchronized void unscheduleSessionMonitoring() {
TimerTask timer = sessionMonitoringTask.getAndSet(null);
if (timer != null)
timer.cancel();
}
SessionState getSessionState() {
return sessionState;
}
TimeSource getTimeSource() {
return timeSource;
}
/**
* <p>Check message sequence number of inbound message. This method is not called for inbound messages that have PossDupFlag(43)=Y.</p>
*
* <p> If the incoming message has a sequence number less than expected and the PossDupFlag is not set, it indicates a serious error.
* It is strongly recommended that the session be terminated and manual intervention be initiated.
* Default implementation throws TARGET_MSG_SEQ_NUM_LESS_EXPECTED exception in this case. </p>
* @return true if actual sequence number match expected or false if Communicator should issue RESEND(2) request from <tt>expected</tt> till <tt>actual</tt>.
*/
protected boolean checkTargetMsgSeqNum(int actual, int expected) throws InvalidFixMessageException, IOException {
if (actual < expected)
throw InvalidFixMessageException.TARGET_MSG_SEQ_NUM_LESS_EXPECTED;
return actual == expected;
}
@Override
public void appendTo(GFLogEntry entry) {
LogUtils.log(getSessionID(), entry);
}
}