From d723e171f405d39e74938ef92755ec7c388791e3 Mon Sep 17 00:00:00 2001 From: Mike Gatny Date: Fri, 11 Aug 2023 10:50:52 -0400 Subject: [PATCH 1/2] Adds exception DoNotFulfillResendRequest Applications can throw this exception to abort the fulfillment of an incoming ResendRequest. Example use case: the counterparty sends an excessive number of ResendRequests in error, and we want to prevent quickfix from fulfilling them, spiking cpu and disk I/O, overwhelming the server. Further action, such as sending Rejects to alert the counterparty to the problem, are left up to Applications. --- .../src/main/java/quickfix/Application.java | 2 +- .../quickfix/DoNotFulfillResendRequest.java | 42 +++++++++++++++++++ .../src/main/java/quickfix/Session.java | 14 ++++++- 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 quickfixj-core/src/main/java/quickfix/DoNotFulfillResendRequest.java diff --git a/quickfixj-core/src/main/java/quickfix/Application.java b/quickfixj-core/src/main/java/quickfix/Application.java index 4d6379197f..6e1c3ec282 100644 --- a/quickfixj-core/src/main/java/quickfix/Application.java +++ b/quickfixj-core/src/main/java/quickfix/Application.java @@ -79,7 +79,7 @@ public interface Application { * @throws RejectLogon causes a logon reject */ void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, - IncorrectTagValue, RejectLogon; + IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest; /** * This is a callback for application messages that you are sending to a diff --git a/quickfixj-core/src/main/java/quickfix/DoNotFulfillResendRequest.java b/quickfixj-core/src/main/java/quickfix/DoNotFulfillResendRequest.java new file mode 100644 index 0000000000..58bedbce4a --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/DoNotFulfillResendRequest.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) quickfixengine.org All rights reserved. + * + * This file is part of the QuickFIX FIX Engine + * + * This file may be distributed under the terms of the quickfixengine.org + * license as defined by quickfixengine.org and appearing in the file + * LICENSE included in the packaging of this file. + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING + * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE. + * + * See http://www.quickfixengine.org/LICENSE for licensing information. + * + * Contact ask@quickfixengine.org if any conditions of this licensing + * are not clear to you. + ******************************************************************************/ + +package quickfix; + +/** + * Applications can throw this exception to abort the fulfillment of an + * incoming ResendRequest. + * + * Example use case: the counterparty sends an excessive number of + * ResendRequests in error, and we want to prevent quickfix from fulfilling + * them, spiking cpu and disk I/O, overwhelming the server. Further action, + * such as sending Rejects to alert the counterparty to the problem, are left + * up to Applications. + */ +public class DoNotFulfillResendRequest extends Exception { + private final static String defaultMsg = "Fulfillment of ResendRequest aborted by Application"; + + public DoNotFulfillResendRequest() { + super(defaultMsg); + } + + public DoNotFulfillResendRequest(String msg) { + super(defaultMsg + ": " + msg); + } +} diff --git a/quickfixj-core/src/main/java/quickfix/Session.java b/quickfixj-core/src/main/java/quickfix/Session.java index 7e52be07bf..c4308b866e 100644 --- a/quickfixj-core/src/main/java/quickfix/Session.java +++ b/quickfixj-core/src/main/java/quickfix/Session.java @@ -1856,7 +1856,17 @@ private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow) return false; } - fromCallback(msgType, msg, sessionID); + try { + fromCallback(msgType, msg, sessionID); + } catch (final DoNotFulfillResendRequest e) { + getLog().onErrorEvent(e.getClass().getName() + ": " + e.getMessage()); + // Only increment seqnum if we are at the expected seqnum + if (getExpectedTargetNum() == msg.getHeader().getInt(MsgSeqNum.FIELD)) { + state.incrNextTargetMsgSeqNum(); + } + return false; + } + return true; } @@ -1904,7 +1914,7 @@ private boolean isGoodTime(Message message) throws FieldNotFound { private void fromCallback(String msgType, Message msg, SessionID sessionID2) throws RejectLogon, FieldNotFound, IncorrectDataFormat, IncorrectTagValue, - UnsupportedMessageType { + UnsupportedMessageType, DoNotFulfillResendRequest { // Application exceptions will prevent the incoming sequence number from being incremented // and may result in resend requests and the next startup. This way, a buggy application // can be fixed and then reprocess previously sent messages. From fef0a05e034bf63e8e762a3f9d9f4daa1f452546 Mon Sep 17 00:00:00 2001 From: Mike Gatny Date: Tue, 15 Aug 2023 11:49:07 -0400 Subject: [PATCH 2/2] Add unit test for DoNotFulfillResendRequest --- .../src/test/java/quickfix/SessionTest.java | 55 ++++++++++++++++--- .../java/quickfix/UnitTestApplication.java | 2 +- ...adPerSessionEventHandlingStrategyTest.java | 5 +- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/quickfixj-core/src/test/java/quickfix/SessionTest.java b/quickfixj-core/src/test/java/quickfix/SessionTest.java index 7fb7627e4e..45bc2b88d0 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionTest.java @@ -1137,7 +1137,7 @@ public void testRejectLogon() throws Exception { @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, - IncorrectTagValue, RejectLogon { + IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); throw new RejectLogon("FOR TEST"); } @@ -1176,7 +1176,7 @@ public void testRejectLogonWithSessionStatus() throws Exception { @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, - IncorrectTagValue, RejectLogon { + IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); throw new RejectLogon("FOR TEST", SessionStatus.SESSION_ACTIVE); } @@ -1192,7 +1192,7 @@ public void fromAdmin(Message message, SessionID sessionId) @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, - IncorrectTagValue, RejectLogon { + IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); throw new RejectLogon("FOR TEST", -1); } @@ -1266,7 +1266,7 @@ public void fromApp(Message message, SessionID sessionId) @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, - IncorrectTagValue, RejectLogon { + IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); if (message.getHeader().getString(MsgType.FIELD) .equals(MsgType.LOGON)) { @@ -1757,7 +1757,7 @@ public void testCatchErrorsFromCallbackAndSendReject() throws Exception { @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, - IncorrectTagValue, RejectLogon { + IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); final String msgType = message.getHeader().getString( MsgType.FIELD); @@ -2460,7 +2460,7 @@ public void testResendSeqWithReject() throws Exception { @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, RejectLogon { + IncorrectDataFormat, IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); if (message.getHeader().getString(MsgType.FIELD).equals(Logon.MSGTYPE)) { logonCount += 1; @@ -2772,7 +2772,7 @@ public void msgseqnum_getting_reset_in_rejected_logon_scenario_fix() throws Exce int logonCount = 0; @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, RejectLogon { + IncorrectDataFormat, IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); if (message.getHeader().getString(MsgType.FIELD).equals(Logon.MSGTYPE)) { logonCount += 1; @@ -3145,4 +3145,45 @@ public void testSend_ShouldKeepPossDupFlagAndOrigSendingTime_GivenAllowPosDupCon assertTrue(sentMessage.getHeader().isSetField(PossDupFlag.FIELD)); assertTrue(sentMessage.getHeader().isSetField(OrigSendingTime.FIELD)); } + + @Test + public void testDoNotFulfillResendRequest() throws Exception { + // Given an application that does not fulfill ResendRequests + final UnitTestApplication application = new UnitTestApplication() { + @Override + public void fromAdmin(quickfix.Message message, SessionID sessionID) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { + if ("2".contentEquals(message.getHeader().getString(35))) { + throw new DoNotFulfillResendRequest("No resend for you!"); + } + } + }; + + // And the session is logged on + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + Session session = SessionFactoryTestSupport.createSession(sessionID, application, false, false, true, true, null); + UnitTestResponder responder = new UnitTestResponder(); + session.setResponder(responder); + final SessionState state = getSessionState(session); + logonTo(session, 1); + assertEquals(2, state.getNextTargetMsgSeqNum()); + assertEquals(true, session.isLoggedOn()); + + // And the following app-level messages have been sent + session.send(createAppMessage(2)); + session.send(createAppMessage(3)); + session.send(createAppMessage(4)); + assertEquals(3, application.toAppMessages.size()); + + // When the counterparty requests resend + Message incomingResendRequest = createResendRequest(2, 1); + incomingResendRequest.toString(); // calculate length/checksum + processMessage(session, incomingResendRequest); + + // Then no messages should be resent + assertEquals(3, application.toAppMessages.size()); + + // And the session should remain logged on + assertEquals(true, session.isLoggedOn()); + session.close(); + } } diff --git a/quickfixj-core/src/test/java/quickfix/UnitTestApplication.java b/quickfixj-core/src/test/java/quickfix/UnitTestApplication.java index 656dbc5b92..d4dd715db6 100644 --- a/quickfixj-core/src/test/java/quickfix/UnitTestApplication.java +++ b/quickfixj-core/src/test/java/quickfix/UnitTestApplication.java @@ -59,7 +59,7 @@ public void toApp(Message message, SessionID sessionId) throws DoNotSend { @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, RejectLogon { + IncorrectDataFormat, IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { log.info("from admin [{}] {}", sessionId, message); fromAdminMessages.add(message); } diff --git a/quickfixj-core/src/test/java/quickfix/mina/ThreadPerSessionEventHandlingStrategyTest.java b/quickfixj-core/src/test/java/quickfix/mina/ThreadPerSessionEventHandlingStrategyTest.java index 1989ebd551..26578fbb98 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ThreadPerSessionEventHandlingStrategyTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ThreadPerSessionEventHandlingStrategyTest.java @@ -31,6 +31,7 @@ import quickfix.MemoryStoreFactory; import quickfix.Message; import quickfix.RejectLogon; +import quickfix.DoNotFulfillResendRequest; import quickfix.Responder; import quickfix.SLF4JLogFactory; import quickfix.Session; @@ -116,7 +117,7 @@ public void testEventHandling() throws Exception { final UnitTestApplication application = new UnitTestApplication() { @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, RejectLogon { + IncorrectDataFormat, IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); latch.countDown(); } @@ -185,7 +186,7 @@ public void testEventHandlingOnDisconnect() throws Exception { final UnitTestApplication application = new UnitTestApplication() { @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, RejectLogon { + IncorrectDataFormat, IncorrectTagValue, RejectLogon, DoNotFulfillResendRequest { super.fromAdmin(message, sessionId); latch.countDown(); }