@@ -3239,4 +3239,148 @@ public void testResendAbortsWhenSendReturnsFalse() throws Exception {
32393239 assertEquals ("Only 2 messages should succeed" , 2 , responder .sentMessages .size ());
32403240 }
32413241 }
3242+
3243+ // Test for issue #597: Session-level messages should not be resent when ForceResendWhenCorruptedStore is enabled
3244+ @ Test
3245+ public void testResendDoesNotResendSessionLevelMessagesExceptReject () throws Exception {
3246+ final UnitTestApplication application = new UnitTestApplication ();
3247+ final SessionID sessionID = new SessionID (FixVersions .BEGINSTRING_FIX44 , "SENDER" , "TARGET" );
3248+
3249+ // Create a session with ForceResendWhenCorruptedStore enabled
3250+ final Session session = new SessionFactoryTestSupport .Builder ()
3251+ .setSessionId (sessionID )
3252+ .setApplication (application )
3253+ .setIsInitiator (false )
3254+ .setPersistMessages (true )
3255+ .setForceResendWhenCorruptedStore (true )
3256+ .build ();
3257+
3258+ // Use a capturing responder to track all sent messages
3259+ final List <String > sentMessages = new ArrayList <>();
3260+ final Responder capturingResponder = new Responder () {
3261+ @ Override
3262+ public boolean send (String data ) {
3263+ sentMessages .add (data );
3264+ return true ;
3265+ }
3266+
3267+ @ Override
3268+ public String getRemoteAddress () {
3269+ return null ;
3270+ }
3271+
3272+ @ Override
3273+ public void disconnect () {
3274+ }
3275+ };
3276+ session .setResponder (capturingResponder );
3277+
3278+ try {
3279+ // Logon to establish session
3280+ logonTo (session );
3281+ assertTrue (session .isLoggedOn ());
3282+
3283+ // Clear lists after logon
3284+ application .clear ();
3285+ sentMessages .clear ();
3286+
3287+ // Get the message store
3288+ final MessageStore messageStore = session .getStore ();
3289+
3290+ // Store some messages in the message store:
3291+ // Seq 2: Heartbeat (session-level, should NOT be resent)
3292+ String heartbeatMsg = "8=FIX.4.4\001 9=60\001 35=0\001 34=2\001 49=SENDER\001 56=TARGET\001 " +
3293+ "52=" + UtcTimestampConverter .convert (LocalDateTime .now (ZoneOffset .UTC ), UtcTimestampPrecision .MILLIS ) + "\001 10=000\001 " ;
3294+ messageStore .set (2 , heartbeatMsg );
3295+
3296+ // Seq 3: Application message (should be resent)
3297+ String appMsg1 = "8=FIX.4.4\001 9=100\001 35=D\001 34=3\001 49=SENDER\001 56=TARGET\001 " +
3298+ "52=" + UtcTimestampConverter .convert (LocalDateTime .now (ZoneOffset .UTC ), UtcTimestampPrecision .MILLIS ) +
3299+ "\001 55=EUR/USD\001 54=1\001 38=1000000\001 40=1\001 10=000\001 " ;
3300+ messageStore .set (3 , appMsg1 );
3301+
3302+ // Seq 4: Logout (session-level, should NOT be resent)
3303+ String logoutMsg = "8=FIX.4.4\001 9=60\001 35=5\001 34=4\001 49=SENDER\001 56=TARGET\001 " +
3304+ "52=" + UtcTimestampConverter .convert (LocalDateTime .now (ZoneOffset .UTC ), UtcTimestampPrecision .MILLIS ) + "\001 10=000\001 " ;
3305+ messageStore .set (4 , logoutMsg );
3306+
3307+ // Seq 5: Reject (session-level, SHOULD be resent)
3308+ String rejectMsg = "8=FIX.4.4\001 9=80\001 35=3\001 34=5\001 49=SENDER\001 56=TARGET\001 " +
3309+ "52=" + UtcTimestampConverter .convert (LocalDateTime .now (ZoneOffset .UTC ), UtcTimestampPrecision .MILLIS ) +
3310+ "\001 45=100\001 58=Invalid message\001 10=000\001 " ;
3311+ messageStore .set (5 , rejectMsg );
3312+
3313+ // Seq 6: Application message (should be resent)
3314+ String appMsg2 = "8=FIX.4.4\001 9=100\001 35=D\001 34=6\001 49=SENDER\001 56=TARGET\001 " +
3315+ "52=" + UtcTimestampConverter .convert (LocalDateTime .now (ZoneOffset .UTC ), UtcTimestampPrecision .MILLIS ) +
3316+ "\001 55=GBP/USD\001 54=2\001 38=2000000\001 40=1\001 10=000\001 " ;
3317+ messageStore .set (6 , appMsg2 );
3318+
3319+ // Update the next sender sequence number to 7
3320+ messageStore .setNextSenderMsgSeqNum (7 );
3321+
3322+ // Receive a ResendRequest for messages 2-6
3323+ final ResendRequest resendRequest = new ResendRequest ();
3324+ resendRequest .getHeader ().setString (SenderCompID .FIELD , "TARGET" );
3325+ resendRequest .getHeader ().setString (TargetCompID .FIELD , "SENDER" );
3326+ resendRequest .getHeader ().setInt (MsgSeqNum .FIELD , 2 );
3327+ resendRequest .getHeader ().setUtcTimeStamp (SendingTime .FIELD , LocalDateTime .now (ZoneOffset .UTC ));
3328+ resendRequest .setInt (BeginSeqNo .FIELD , 2 );
3329+ resendRequest .setInt (EndSeqNo .FIELD , 6 );
3330+ resendRequest .toString (); // calculate length/checksum
3331+
3332+ session .next (resendRequest );
3333+
3334+ // Verify expectations:
3335+ // 1. toApp() should only be called for application messages (seq 3 and 6)
3336+ assertEquals ("toApp should be called twice for app messages" , 2 , application .toAppMessages .size ());
3337+
3338+ // 2. Parse all sent messages and verify what was sent
3339+ boolean rejectWasResent = false ;
3340+ boolean heartbeatWasResent = false ;
3341+ boolean logoutWasResent = false ;
3342+ int sequenceResetCount = 0 ;
3343+ int appMessageCount = 0 ;
3344+
3345+ for (String sentMsg : sentMessages ) {
3346+ try {
3347+ Message msg = new Message (sentMsg );
3348+ String msgType = msg .getHeader ().getString (MsgType .FIELD );
3349+ int seqNum = msg .getHeader ().getInt (MsgSeqNum .FIELD );
3350+
3351+ if (msgType .equals (MsgType .SEQUENCE_RESET )) {
3352+ sequenceResetCount ++;
3353+ } else if (msgType .equals (MsgType .REJECT ) && seqNum == 5 ) {
3354+ rejectWasResent = true ;
3355+ // Verify Reject has PossDupFlag
3356+ assertTrue ("Reject should have PossDupFlag set" ,
3357+ msg .getHeader ().isSetField (PossDupFlag .FIELD ) &&
3358+ msg .getHeader ().getBoolean (PossDupFlag .FIELD ));
3359+ } else if (msgType .equals (MsgType .HEARTBEAT ) && seqNum == 2 ) {
3360+ heartbeatWasResent = true ;
3361+ } else if (msgType .equals (MsgType .LOGOUT ) && seqNum == 4 ) {
3362+ logoutWasResent = true ;
3363+ } else if (msgType .equals ("D" )) { // NewOrderSingle
3364+ appMessageCount ++;
3365+ // Verify app messages have PossDupFlag
3366+ assertTrue ("Application messages should have PossDupFlag set" ,
3367+ msg .getHeader ().isSetField (PossDupFlag .FIELD ) &&
3368+ msg .getHeader ().getBoolean (PossDupFlag .FIELD ));
3369+ }
3370+ } catch (Exception e ) {
3371+ // Skip unparseable messages
3372+ }
3373+ }
3374+
3375+ // Verify results
3376+ assertTrue ("Reject message should have been resent" , rejectWasResent );
3377+ assertFalse ("Heartbeat should NOT have been resent" , heartbeatWasResent );
3378+ assertFalse ("Logout should NOT have been resent" , logoutWasResent );
3379+ assertEquals ("Two application messages should have been resent" , 2 , appMessageCount );
3380+ assertTrue ("At least one SequenceReset should be sent for skipped admin messages" , sequenceResetCount >= 1 );
3381+
3382+ } finally {
3383+ session .close ();
3384+ }
3385+ }
32423386}
0 commit comments