/** * a multicast socket session implementation for FastFix @WARN CHANGES TO THIS CLASS SHOULD BE * CHECKED AGAINST NonBlockingFastFixSocketSession */ public abstract class FastFixSocketSession extends AbstractSocketSession { protected static final Logger _log = LoggerFactory.create(FastFixSocketSession.class); private static final int MIN_BYTES = 30; public static int getDataOffset(String name, boolean isInbound) { return AbstractSession.getLogHdrLen(name, isInbound); } public FastFixSocketSession( String name, MessageRouter inboundRouter, SocketConfig socketConfig, MessageDispatcher dispatcher, Encoder encoder, Decoder decoder, ThreadPriority receiverPriority) { super( name, inboundRouter, socketConfig, dispatcher, encoder, decoder, new DummyDecoder(), receiverPriority); } @Override public final void processNextInbound() throws Exception { int preBuffered = _inPreBuffered; Message msg; _inPreBuffered = 0; // reset the preBuffer .. incase of exception final int startPos = _inHdrLen + preBuffered; _inByteBuffer.limit(_inByteBuffer.capacity()); _inByteBuffer.position(startPos); final int bytesRead = initialChannelRead(preBuffered, MIN_BYTES); if (bytesRead == 0) { _inPreBuffered = preBuffered; return; } // time starts from when we have read the full message off the socket if (_logStats) _decoder.setReceived(Utils.nanoTime()); _inByteBuffer.position(_inHdrLen); final int maxIdx = _inHdrLen + bytesRead; _inByteBuffer.limit(maxIdx); if (_stopping) return; _inLogBuf.setLength(maxIdx); msg = decode(_inHdrLen, bytesRead); final int msgLen = _decoder.getLength(); int extraBytes = bytesRead; if (msg != null) { extraBytes = bytesRead - msgLen; logInEvent(null); logInEventPojo(msg); // message decoded, shift left any extra bytes before invoke controller as that can throw // exception .. avoids extra try-finally invokeController(msg); } else { // no partial messages in UDP ... could also be duplicate in any case discard extraBytes = 0; } if (extraBytes > 0) { _inPreBuffered = extraBytes; final int nextMsgStart = _inHdrLen + msgLen; shiftInBufferLeft(extraBytes, nextMsgStart); } } @Override public final void handleForSync(Message msg) { // } @Override public final void persistLastInboundMesssage() { // N/A } @Override public final void logInboundError(Exception e) { super.logInboundError(e); ((FastFixDecoder) _decoder).logLastMsg(); } protected abstract void invokeController(Message msg); protected abstract void dispatchMsgGap(int channelId, int lastSeqNum, int seqNum); @Override protected final synchronized Message recoveryDecode( byte[] buf, int offset, int len, boolean inBound) { return null; } @Override protected final Message recoveryDecodeWithContext( byte[] buf, int offset, int len, byte[] opt, int optOffset, int optLen, boolean inBound) { return null; } @Override protected final void sendNow(Message msg) throws IOException { _encoder.encode(msg); int length = _encoder.getLength(); if (length > 0) { final int lastIdx = length + _encoder.getOffset(); _outByteBuffer.clear(); _outByteBuffer.limit(lastIdx); _outByteBuffer.position(_encoder.getOffset()); blockingWriteSocket(); logOutEvent(null); logOutEventPojo(msg); if (_stopping) return; } } @Override public final boolean discardOnDisconnect(Message msg) { return false; } @Override public final boolean canHandle() { return isConnected(); } @Override protected final void sendChain(Message msg, boolean canRecycle) { if (_chainSession != null && _chainSession.isConnected()) { _chainSession.handle(msg); } else if (canRecycle) { outboundRecycle(msg); } } @Override protected final void logInEvent(ZString event) { if (_logEvents) { ((FastFixDecoder) _decoder).logLastMsg(); } } @Override protected final void logOutEvent(ZString event) { if (_logEvents) { ((FastFixEncoder) _encoder).logLastMsg(); } } }
/** * a multi session threaded dispatcher for session outbound messages * * <p>does NOT own spinning control thread, that is shared and which round robins sessions * * <p>if a session cannot send a message eg socket blocked due to slow consumer then the * NonBlockingSession returns immediately that session will have to wait its turn to retry writing * remaining data to the socket * * <p>Currently max sockets is 32 but this could easily be changed to 64 with minimal delay impact * IF using OS bypass eg Solarflare OpenOnload */ public final class MultiSessionThreadedDispatcher implements MultiSessionDispatcher, ExecutableElement { private static final Logger _log = LoggerFactory.create(MultiSessionThreadedDispatcher.class); private static final ErrorCode MISSING_HANDLER = new ErrorCode("MST100", "No registered dispatcher for message to session "); static final ZString DISCONNECTED = new ViewString("Unable to dispatch as destination Session Disconnected"); private static final ZString DROP_MSG = new ViewString("Dropping session message as not logged in, type="); private class SessionWrapper { final MessageQueue _queue; final MessageQueue _syncQueue; final MessageQueue _preQueue; // when disconnected flushed events that need to be kept go here final NonBlockingSession _session; boolean _connected = false; SessionWrapper(NonBlockingSession session, MessageQueue queue, MessageQueue syncQueue) { _queue = queue; _syncQueue = syncQueue; _session = session; _preQueue = new SimpleMessageQueue(); } } private SessionWrapper[] _sessions = new SessionWrapper[0]; private int _nextSession = 0; private final ReusableString _logMsg = new ReusableString(50); private boolean _allDisconnected = true; // not volatile as mem barrier already occurs when get msg off queue private final Object _disconnectLock = new Object(); private final ControlThread _ctl; private Message _curMsg = null; private SessionWrapper _curSessW = null; private volatile boolean _fullyFlushed = false; private AtomicBoolean _stopping = new AtomicBoolean(false); private final String _id; // array index assigned to session to save map lookup public MultiSessionThreadedDispatcher(String id, ControlThread ctl) { _ctl = ctl; _id = id; ctl.register(this); } @Override public void threadedInit() { for (int idx = 0; idx < _sessions.length; ++idx) { _sessions[idx]._session.threadedInit(); } } @Override public String getComponentId() { return _id; } @Override public void notReady() { disconnectedFlushAll(); } @Override public void execute() throws Exception { _curSessW = _sessions[_nextSession]; final MessageQueue queue = _curSessW._queue; final MessageQueue preQueue = _curSessW._preQueue; final NonBlockingSession sess = _curSessW._session; if (++_nextSession >= _sessions.length) _nextSession = 0; if (_curSessW._connected && sess.canHandle()) { if (sess.isLoggedIn()) { if (sess.isMsgPendingWrite()) { sess.retryCompleteWrite(); } else if (preQueue.isEmpty()) { _curMsg = queue.poll(); // POLL = non blocking, causes MEM_READ barrier if (_curMsg != null && _curMsg.getReusableType() != CoreReusableType.NullMessage) { sess.handleNow(_curMsg); } } else { // QUEUED MESSAGES FROM PREVIOUS FLUSH CALLS _curMsg = preQueue.next(); if (_curMsg.getReusableType() != CoreReusableType.NullMessage) { sess.handleNow(_curMsg); } } } else { // SYNC mode final MessageQueue syncQueue = _curSessW._syncQueue; if (sess.isMsgPendingWrite()) { sess.retryCompleteWrite(); } else if (!syncQueue.isEmpty()) { _curMsg = syncQueue.next(); if (_curMsg.getReusableType() != CoreReusableType.NullMessage) { sess.handleNow(_curMsg); } } } } else { flush(_curSessW); } } @Override public void handleExecutionException(Exception ex) { final NonBlockingSession sess = _curSessW._session; if (_curMsg != null && sess != null) { _log.warn( "SessionThreadedDispatcher " + getComponentId() + ", msgSeqNum=" + _curMsg.getMsgSeqNum() + ", sess=" + sess.getComponentId() + " exception " + ex.getMessage()); } flush(_curSessW); // some problem, possibly disconnect, poke controller to wake up anything waiting on controller // passive lock _ctl.statusChange(); // Mem barrier } @Override public boolean checkReady() { return (_allDisconnected == false); } @Override public synchronized void start() { _ctl.start(); } @Override public void setHandler(MessageHandler handler) { throw new SMTRuntimeException( "MultiSessionThreadedDispatcher MISCONFIGURATION : only for use with multi session and addSession()"); } /** * @param session - a non blocking session ie an NIO one that wont block if socket cant read/write */ @Override public synchronized void addSession(final NonBlockingSession session) { SessionWrapper[] newSessions = new SessionWrapper[_sessions.length + 1]; int idx = 0; while (idx < _sessions.length) { newSessions[idx] = _sessions[idx]; ++idx; } newSessions[idx] = new SessionWrapper(session, session.getSendQueue(), session.getSendSyncQueue()); _sessions = newSessions; _fullyFlushed = false; } @Override public void dispatch(final Message msg) { if (msg != null) { final MessageHandler handler = msg.getMessageHandler(); final NonBlockingSession session = (NonBlockingSession) handler; final MessageQueue queue = session.getSendQueue(); if (queue != null) { queue.add(msg); } else { // should NEVER happen ReusableString s = TLC.instance().pop(); s.copy(((Session) handler).getComponentId()) .append(": Missing Queue, unable to dispatch : "); msg.dump(s); _log.error(MISSING_HANDLER, s); TLC.instance().pushback(s); } } } @Override public void setStopping() { // dont actually stop, but wake up to force flush final int numSessions = _sessions.length; for (int i = 0; i < numSessions; i++) { final SessionWrapper s = _sessions[i]; MessageQueue q = s._queue; q.add(new NullMessage()); // wake up queue } _fullyFlushed = false; } @Override public void stop() { if (_stopping.compareAndSet(false, true)) { _ctl.setStopping(true); final int numSessions = _sessions.length; for (int i = 0; i < numSessions; i++) { final SessionWrapper s = _sessions[i]; MessageQueue q = s._queue; q.add(new NullMessage()); // wake up queue } _fullyFlushed = false; } } @Override public void handlerStatusChange(MessageHandler handler, boolean connected) { final int numSessions = _sessions.length; boolean allDisconnected = true; for (int i = 0; i < numSessions; i++) { SessionWrapper sessW = _sessions[i]; if (sessW._session == handler) { if (connected != sessW._connected) { final NonBlockingSession sess = sessW._session; _log.info( "MultiSession OutDispatcher " + getComponentId() + " : " + ((connected) ? "CONNECTED" : "DISCONNECTED") + " with " + sess.getComponentId() + ", canHandle=" + sess.canHandle() + ", isLoggedIn=" + sess.isLoggedIn()); sessW._connected = connected; } } if (sessW._connected) { allDisconnected = false; } } _fullyFlushed = false; synchronized (_disconnectLock) { // force mem barrier _allDisconnected = allDisconnected; } _ctl.statusChange(); } @Override public boolean canQueue() { return true; } @Override public String info() { return "MultiSessionThreadedDispatcher( " + _id + " )"; } @Override public void dispatchForSync(Message msg) { if (msg != null) { final MessageHandler handler = msg.getMessageHandler(); // cant be null NonBlockingSession session = (NonBlockingSession) handler; MessageQueue queue = session.getSendSyncQueue(); if (queue != null) { queue.add(msg); } else { // @TODO add ReusableString write( ReusableString buf ) to Message and log details // should NEVER happen _log.error(MISSING_HANDLER, ((Session) handler).getComponentId()); } } } private void disconnectedFlushAll() { if (!_fullyFlushed) { final int numSessions = _sessions.length; for (int i = 0; i < numSessions; i++) { flush(_sessions[i]); } _fullyFlushed = true; } } // @NOTE keep flush private the preQ is not threadsafe private void flush(SessionWrapper sessW) { // disconnected drop any session messages // optionally keep any other messages or reject back upstream final Session session = sessW._session; final MessageQueue queue = sessW._queue; final MessageQueue preQ = sessW._preQueue; final MessageQueue syncQ = sessW._syncQueue; while (!syncQ.isEmpty()) { syncQ.next(); // DISCARD } Message head = null; Message tail = null; while (!queue.isEmpty()) { Message msg = queue.next(); if (msg.getReusableType() == CoreReusableType.NullMessage) break; if (session.discardOnDisconnect(msg) == false) { if (session.rejectMessageUpstream(msg, DISCONNECTED)) { // message recycled by successful reject processing } else { if (tail == null) { head = msg; tail = msg; } else { tail.attachQueue(msg); tail = msg; } } } else { _logMsg.copy(DROP_MSG).append(msg.getReusableType().toString()); _log.info(_logMsg); session.outboundRecycle(msg); } } // move remaining messages to the preQ if (head != null) { Message tmp = head; while (tmp != null) { Message next = tmp.getNextQueueEntry(); tmp.detachQueue(); preQ.add(tmp); tmp = next; } } } @Override public void init(SMTStartContext ctx) { // nothing } @Override public void prepare() { // nothing } @Override public void startWork() { start(); } @Override public void stopWork() { stop(); } }
public class EmeaDmaValidator implements EventValidator { private static final Logger _log = LoggerFactory.create(EmeaDmaValidator.class); private static final ZString MISSING_CLORDID = new ViewString("Missing clOrdId "); private static final ZString UNSUPPORTED = new ViewString("Unsupported attribute value "); private static final ZString UNABLE_TO_CHANGE_CCY = new ViewString("Unable to change the currency "); private static final ZString UNABLE_TO_CHANGE_ORDTYPE = new ViewString("Unable to change the order type "); private static final ZString UNABLE_TO_CHANGE_SIDE = new ViewString("Unable to change the side "); private static final ZString UNABLE_TO_CHANGE_SYM = new ViewString("Unable to change the symbol "); private static final ZString CANNOT_AMEND_BELOW_CQTY = new ViewString("Cannot amend qty below cumQty, qty="); private static final ZString MAJOR_FIELDS_UNCHANGED = new ViewString("At least one of Qty/Price/TIF must change on an amend"); private static final ZString REQ_TOO_OLD = new ViewString("Request is older than max allowed seconds "); private static final ZString EXDEST_MISMATCH = new ViewString("EXDEST doesnt match the instrument REC, received "); private static final ZString SECEX_MISMATCH = new ViewString("Security Exchange doesnt match the instrument REC, received "); private static final ZString INVALID_PRICE = new ViewString("Failed tick validation "); private static final ZString MISSING_TICK = new ViewString("Missing tick type for instrument, RIC="); private static final ZString RESTRICTED = new ViewString("Cant trade restricted stock, bookingType="); private static final ZString INSTRUMENT_DISABLED = new ViewString("Instrument is disabled, RIC="); private final ReusableString _err = new ReusableString(256); private final int _maxAgeMS; public EmeaDmaValidator(int maxAge) { _maxAgeMS = maxAge; } /** dont do validation that the exchange validator is doing */ @Override public boolean validate(NewOrderSingle msg, Order order) { reset(); final Instrument inst = msg.getInstrument(); final Exchange ex = inst.getExchange(); if (!inst.isTestInstrument()) { // dont validate test instruments at ALL final long now = System.currentTimeMillis(); commonValidation(ex, msg, order, now); final OMExchangeValidator exchangeValidator = (OMExchangeValidator) ex.getExchangeEventValidator(); // qty validation done in exchangevalidator if (exchangeValidator != null) exchangeValidator.validate(msg, _err, now); } return _err.length() == 0; } @Override public boolean validate(CancelReplaceRequest newVersion, Order order) { reset(); final OrderVersion lastAcc = order.getLastAckedVerion(); final OrderRequest previous = (OrderRequest) lastAcc.getBaseOrderRequest(); final int cumQty = order.getLastAckedVerion().getCumQty(); final Instrument inst = newVersion.getInstrument(); final Exchange ex = inst.getExchange(); if (!inst.isTestInstrument()) { // dont validate test instruments at ALL final long now = System.currentTimeMillis(); final int newOrdQty = newVersion.getOrderQty(); commonValidation(ex, newVersion, order, now); if (newVersion.getCurrency() != previous.getCurrency()) { // CANT CHANGE CCY delim() .append(UNABLE_TO_CHANGE_CCY) .append(Strings.FROM) .append(previous.getCurrency().toString()) .append(Strings.TO) .append(newVersion.getCurrency().toString()); } if (newVersion.getOrdType() != previous.getOrdType()) { delim() .append(UNABLE_TO_CHANGE_ORDTYPE) .append(Strings.FROM) .append(previous.getOrdType().toString()) .append(Strings.TO) .append(newVersion.getOrdType().toString()); } if (newVersion.getSide() != previous.getSide()) { delim() .append(UNABLE_TO_CHANGE_SIDE) .append(Strings.FROM) .append(previous.getSide().toString()) .append(Strings.TO) .append(newVersion.getSide().toString()); } if (!newVersion.getSymbol().equals(previous.getSymbol())) { delim() .append(UNABLE_TO_CHANGE_SYM) .append(Strings.FROM) .append(previous.getSymbol()) .append(Strings.TO) .append(newVersion.getSymbol()); } if (newVersion.getPrice() == previous.getPrice() && newOrdQty == previous.getOrderQty() && newVersion.getTimeInForce() == previous.getTimeInForce()) { delim().append(MAJOR_FIELDS_UNCHANGED); } if (newOrdQty < cumQty) { delim() .append(CANNOT_AMEND_BELOW_CQTY) .append(newOrdQty) .append(Strings.CUMQTY) .append(cumQty); } final OMExchangeValidator exchangeValidator = (OMExchangeValidator) ex.getExchangeEventValidator(); if (exchangeValidator != null) { exchangeValidator.validate(newVersion, _err, now); } } return _err.length() == 0; } @Override public CxlRejReason getReplaceRejectReason() { return CxlRejReason.Other; } @Override public OrdRejReason getOrdRejectReason() { return OrdRejReason.UnsupOrdCharacteristic; } @Override public ViewString getRejectReason() { return _err; } private ReusableString delim() { if (_err.length() > 0) { _err.append(Strings.DELIM); } return _err; } private void addErrorUnsupported(Enum<?> val) { delim() .append(UNSUPPORTED) .append(val.toString()) .append(Strings.TYPE) .append(val.getClass().getSimpleName()); } private final void reset() { _err.reset(); } private void validateHandlingInstruction(HandlInst handlInst) { if (handlInst == HandlInst.ManualBestExec) { addErrorUnsupported(handlInst); } } private void validateAge(int transactTime, long now) { if ((TimeZoneCalculator.instance().getTimeUTC(now) - transactTime) > _maxAgeMS) { delim().append(REQ_TOO_OLD).append(_maxAgeMS / 1000); } } private void commonValidation(Exchange ex, OrderRequest req, Order order, long now) { if (req.getClOrdId().length() == 0) addError(MISSING_CLORDID); validateHandlingInstruction(req.getHandlInst()); validateAge(req.getTransactTime(), now); Instrument inst = req.getInstrument(); final ViewString exDest = req.getExDest(); final ViewString secEx = req.getSecurityExchange(); final double price = order.getPendingVersion().getMarketPrice(); if (exDest.length() > 0 && !ex.getRecCode().equals(exDest)) { delim() .append(EXDEST_MISMATCH) .append(exDest) .append(Strings.EXPECTED) .append(ex.getRecCode()); } if (secEx.length() > 0 && !ex.getRecCode().equals(secEx)) { delim().append(SECEX_MISMATCH).append(secEx).append(Strings.EXPECTED).append(ex.getRecCode()); } validateTicksize(inst, price); if (inst.isRestricted() && !canTradeRestricted(req.getClient(), req.getBookingType(), req.getOrderCapacity())) { delim() .append(RESTRICTED) .append(req.getBookingType()) .append(Strings.ORDCAP) .append(req.getOrderCapacity()); } if (!inst.isEnabled()) { delim().append(INSTRUMENT_DISABLED).append(inst.getRIC()); } TradingRange band = inst.getValidTradingRange(); band.valid(price, req.getSide().getIsBuySide(), _err); } private boolean canTradeRestricted( ClientProfile client, BookingType bookingType, OrderCapacity orderCapacity) { // TODO THIS will be client specific return false; } private void addError(ZString msg) { delim().append(msg); } private void validateTicksize(Instrument instrument, double price) { TickType ts = instrument.getTickscale(); if (ts.canVerifyPrice()) { if (!ts.isValid(price)) { delim().append(INVALID_PRICE); ts.writeError(price, _err); } } else { ReusableString msg = TLC.instance().pop(); msg.append(MISSING_TICK).append(instrument.getRIC()); _log.warn(msg); TLC.instance().pushback(msg); } } }