From f7cdaf70c884282e514a456fa7f07b99852b94c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:41:55 +0000 Subject: [PATCH 1/5] Initial plan From 80b87618e5e1038776b44d8a00a35f927d656eac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:51:56 +0000 Subject: [PATCH 2/5] Add failing test for ResendRequest bug #1114 Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../SessionResendRequestFailureTest.java | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java diff --git a/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java b/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java new file mode 100644 index 000000000..264b7ead4 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java @@ -0,0 +1,215 @@ +package quickfix; + +import org.junit.Test; +import quickfix.field.*; +import quickfix.fix44.Heartbeat; +import quickfix.fix44.Logon; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import static org.junit.Assert.*; + +/** + * Test for issue #1114 - ResendRequest never re-sent after re-logon + * when previous request failed due to responder == null + */ +public class SessionResendRequestFailureTest { + + /** + * Test demonstrates the problem: + * 1. Session connects and logs on + * 2. Receives message with high sequence number, triggering ResendRequest + * 3. Session disconnects BEFORE the ResendRequest is actually sent (responder becomes null) + * 4. ResendRange is still marked as "sent" even though sendRaw returned false + * 5. On reconnection, ResendRequest is NOT re-sent because it's marked as already sent + */ + @Test + public void testResendRequestNotResentAfterDisconnectBeforeSend() throws Exception { + // Setup + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final UnitTestApplication application = new UnitTestApplication(); + final FailingResponder responder = new FailingResponder(); + + final SessionSettings settings = new SessionSettings(); + settings.setString(sessionID, "BeginString", FixVersions.BEGINSTRING_FIX44); + settings.setString(sessionID, "SenderCompID", "SENDER"); + settings.setString(sessionID, "TargetCompID", "TARGET"); + settings.setString(sessionID, "ConnectionType", "acceptor"); + settings.setString(sessionID, "StartTime", "00:00:00"); + settings.setString(sessionID, "EndTime", "00:00:00"); + settings.setString(sessionID, "HeartBtInt", "30"); + + final DefaultSessionFactory factory = new DefaultSessionFactory( + application, + new MemoryStoreFactory(), + new ScreenLogFactory(true, true, true)); + + try (Session session = factory.create(sessionID, settings)) { + session.setResponder(responder); + + // Step 1: Logon normally + logonTo(session, 1); + assertTrue("Session should be logged on", session.isLoggedOn()); + + // Step 2: Receive a message with sequence number gap (e.g., expect 2, get 10) + // This should trigger a ResendRequest + final SessionState state = getSessionState(session); + assertEquals("Expected target seq num should be 2", 2, state.getNextTargetMsgSeqNum()); + + // Step 3: Configure responder to fail (simulating disconnect before send completes) + responder.setShouldFail(true); + + // Receive heartbeat with seq num 10 (gap from 2 to 9) + // This should trigger ResendRequest, but it will fail to send + processMessage(session, createHeartbeatMessage(10)); + + // Step 4: Verify that ResendRange is marked as sent even though sendRaw failed + assertTrue("ResendRange should be marked as requested (BUG!)", state.isResendRequested()); + SessionState.ResendRange resendRange = state.getResendRange(); + assertEquals("ResendRange begin should be 2", 2, resendRange.getBeginSeqNo()); + assertEquals("ResendRange end should be 9", 9, resendRange.getEndSeqNo()); + + // Step 5: Disconnect and reconnect + session.disconnect("Simulating disconnect", false); + assertFalse("Session should be disconnected", session.isLoggedOn()); + + // Reconnect with a fresh responder + final UnitTestResponder freshResponder = new UnitTestResponder(); + session.setResponder(freshResponder); + logonTo(session, 11); // Logon with next sequence number + + // Step 6: Send another message that should trigger the ResendRequest again + processMessage(session, createHeartbeatMessage(12)); + + // Expected: ResendRequest should be sent again because the previous one failed + // Actual (BUG): ResendRequest is NOT sent because ResendRange is still marked as "sent" + + // This assertion will FAIL with the current bug - demonstrating the problem + assertFalse("EXPECTED: ResendRequest should have been re-sent and satisfied, " + + "but it was NOT due to the bug. ResendRange is still marked as requested.", + state.isResendRequested()); + } + } + + /** + * Responder that can be configured to fail (return false) on send operations + * This simulates a scenario where responder becomes null or disconnects during send + */ + private static class FailingResponder implements Responder { + private boolean shouldFail = false; + private String lastSentMessage = null; + + public void setShouldFail(boolean shouldFail) { + this.shouldFail = shouldFail; + } + + @Override + public boolean send(String data) { + if (shouldFail) { + // Simulate failure - message not sent + return false; + } + lastSentMessage = data; + return true; + } + + @Override + public void disconnect() { + // No-op for test + } + + @Override + public String getRemoteAddress() { + return "127.0.0.1:1234"; + } + + public String getLastSentMessage() { + return lastSentMessage; + } + } + + /** + * Simple test responder that always succeeds + */ + private static class UnitTestResponder implements Responder { + private String lastSentMessage; + + @Override + public boolean send(String data) { + lastSentMessage = data; + return true; + } + + @Override + public void disconnect() { + } + + @Override + public String getRemoteAddress() { + return "127.0.0.1:1234"; + } + + public String getLastSentMessage() { + return lastSentMessage; + } + } + + private void logonTo(Session session, int sequence) throws Exception { + final Logon logon = new Logon(); + logon.getHeader().setString(SenderCompID.FIELD, "TARGET"); + logon.getHeader().setString(TargetCompID.FIELD, "SENDER"); + logon.getHeader().setInt(MsgSeqNum.FIELD, sequence); + logon.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now(ZoneOffset.UTC)); + logon.setInt(HeartBtInt.FIELD, 30); + logon.setInt(EncryptMethod.FIELD, 0); + logon.toString(); // Calculate length and checksum + processMessage(session, logon); + } + + private Message createHeartbeatMessage(int sequence) { + final Heartbeat heartbeat = new Heartbeat(); + heartbeat.getHeader().setString(SenderCompID.FIELD, "TARGET"); + heartbeat.getHeader().setString(TargetCompID.FIELD, "SENDER"); + heartbeat.getHeader().setInt(MsgSeqNum.FIELD, sequence); + heartbeat.getHeader().setUtcTimeStamp(SendingTime.FIELD, LocalDateTime.now(ZoneOffset.UTC)); + heartbeat.toString(); // Calculate length and checksum + return heartbeat; + } + + private void processMessage(Session session, Message message) throws Exception { + session.next(message); + } + + private SessionState getSessionState(Session session) throws Exception { + final java.lang.reflect.Field field = Session.class.getDeclaredField("state"); + field.setAccessible(true); + return (SessionState) field.get(session); + } + + /** + * Simple test application + */ + private static class UnitTestApplication extends ApplicationAdapter { + private Message lastToAdminMessage; + private Message lastToAppMessage; + + @Override + public void toAdmin(Message message, SessionID sessionId) { + lastToAdminMessage = message; + } + + @Override + public void toApp(Message message, SessionID sessionId) throws DoNotSend { + lastToAppMessage = message; + } + + public Message lastToAdminMessage() { + return lastToAdminMessage; + } + + public Message lastToAppMessage() { + return lastToAppMessage; + } + } +} From 01e257e3a9f12ac9630d0cc80a8fbc8756961096 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:53:31 +0000 Subject: [PATCH 3/5] Address code review: use specific exceptions in getSessionState Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../src/test/java/quickfix/SessionResendRequestFailureTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java b/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java index 264b7ead4..d1a247834 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java @@ -181,7 +181,7 @@ private void processMessage(Session session, Message message) throws Exception { session.next(message); } - private SessionState getSessionState(Session session) throws Exception { + private SessionState getSessionState(Session session) throws NoSuchFieldException, IllegalAccessException { final java.lang.reflect.Field field = Session.class.getDeclaredField("state"); field.setAccessible(true); return (SessionState) field.get(session); From 38b54daa35fcbb12bdf46b42a3582c79df9af030 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:43:47 +0000 Subject: [PATCH 4/5] Address review feedback: remove unnecessary lines and add log outputs Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../quickfix/SessionResendRequestFailureTest.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java b/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java index d1a247834..caac56785 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java @@ -74,13 +74,19 @@ public void testResendRequestNotResentAfterDisconnectBeforeSend() throws Excepti session.disconnect("Simulating disconnect", false); assertFalse("Session should be disconnected", session.isLoggedOn()); + // Log ResendRange after disconnect + System.out.println("ResendRange after disconnect: begin=" + state.getResendRange().getBeginSeqNo() + + ", end=" + state.getResendRange().getEndSeqNo()); + // Reconnect with a fresh responder final UnitTestResponder freshResponder = new UnitTestResponder(); session.setResponder(freshResponder); - logonTo(session, 11); // Logon with next sequence number - // Step 6: Send another message that should trigger the ResendRequest again - processMessage(session, createHeartbeatMessage(12)); + // Log ResendRange before reconnect logon + System.out.println("ResendRange before reconnect logon: begin=" + state.getResendRange().getBeginSeqNo() + + ", end=" + state.getResendRange().getEndSeqNo()); + + logonTo(session, 11); // Logon with next sequence number - this should trigger ResendRequest // Expected: ResendRequest should be sent again because the previous one failed // Actual (BUG): ResendRequest is NOT sent because ResendRange is still marked as "sent" From f910c2f1334d20953f876fbc58c59cd940605f12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:12:48 +0000 Subject: [PATCH 5/5] Call generateResendRequest() directly to demonstrate the bug Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../SessionResendRequestFailureTest.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java b/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java index caac56785..524376d89 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionResendRequestFailureTest.java @@ -19,8 +19,8 @@ public class SessionResendRequestFailureTest { /** * Test demonstrates the problem: * 1. Session connects and logs on - * 2. Receives message with high sequence number, triggering ResendRequest - * 3. Session disconnects BEFORE the ResendRequest is actually sent (responder becomes null) + * 2. Session disconnects + * 3. generateResendRequest() is called while responder.send() returns false * 4. ResendRange is still marked as "sent" even though sendRaw returned false * 5. On reconnection, ResendRequest is NOT re-sent because it's marked as already sent */ @@ -52,33 +52,33 @@ public void testResendRequestNotResentAfterDisconnectBeforeSend() throws Excepti logonTo(session, 1); assertTrue("Session should be logged on", session.isLoggedOn()); - // Step 2: Receive a message with sequence number gap (e.g., expect 2, get 10) - // This should trigger a ResendRequest + // Step 2: Get session state and verify initial sequence number final SessionState state = getSessionState(session); assertEquals("Expected target seq num should be 2", 2, state.getNextTargetMsgSeqNum()); - // Step 3: Configure responder to fail (simulating disconnect before send completes) + // Step 3: Disconnect (simulating connection loss) + session.disconnect("Simulating disconnect", false); + assertFalse("Session should be disconnected", session.isLoggedOn()); + + // Step 4: Configure responder to fail (simulating scenario where responder becomes null) responder.setShouldFail(true); - // Receive heartbeat with seq num 10 (gap from 2 to 9) - // This should trigger ResendRequest, but it will fail to send - processMessage(session, createHeartbeatMessage(10)); + // Step 5: Call generateResendRequest directly to trigger ResendRequest + // This will call sendResendRequest() which calls sendRaw(), but sendRaw will fail + // because responder.send() returns false + callGenerateResendRequest(session, FixVersions.BEGINSTRING_FIX44, 10); - // Step 4: Verify that ResendRange is marked as sent even though sendRaw failed + // Step 6: Verify that ResendRange is marked as sent even though sendRaw failed (BUG!) assertTrue("ResendRange should be marked as requested (BUG!)", state.isResendRequested()); SessionState.ResendRange resendRange = state.getResendRange(); assertEquals("ResendRange begin should be 2", 2, resendRange.getBeginSeqNo()); assertEquals("ResendRange end should be 9", 9, resendRange.getEndSeqNo()); - // Step 5: Disconnect and reconnect - session.disconnect("Simulating disconnect", false); - assertFalse("Session should be disconnected", session.isLoggedOn()); - - // Log ResendRange after disconnect - System.out.println("ResendRange after disconnect: begin=" + state.getResendRange().getBeginSeqNo() + // Log ResendRange after failed send + System.out.println("ResendRange after failed send: begin=" + state.getResendRange().getBeginSeqNo() + ", end=" + state.getResendRange().getEndSeqNo()); - // Reconnect with a fresh responder + // Step 7: Reconnect with a fresh responder final UnitTestResponder freshResponder = new UnitTestResponder(); session.setResponder(freshResponder); @@ -193,6 +193,12 @@ private SessionState getSessionState(Session session) throws NoSuchFieldExceptio return (SessionState) field.get(session); } + private void callGenerateResendRequest(Session session, String beginString, int msgSeqNum) throws Exception { + final java.lang.reflect.Method method = Session.class.getDeclaredMethod("generateResendRequest", String.class, int.class); + method.setAccessible(true); + method.invoke(session, beginString, msgSeqNum); + } + /** * Simple test application */