Skip to content

Commit 4bae039

Browse files
authored
Merge pull request #957 from gbirchmeier/i309-fix
solve issue #309: obey a GapFill even if it replaces a message that was processed off the queue
2 parents b035b26 + 07f8a7a commit 4bae039

File tree

6 files changed

+162
-6
lines changed

6 files changed

+162
-6
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# issue 309
2+
# When a GapFill skips over seqnos past the ExpectedNextTargetSeqNum,
3+
# the engine needs to skip with it.
4+
5+
iCONNECT
6+
I8=FIX.4.435=A34=149=TW52=<TIME>56=ISLD98=0108=2
7+
E8=FIX.4.435=A34=149=ISLD52=00000000-00:00:00.00056=TW98=0108=210=0
8+
9+
# News msg to bump the seq up
10+
I8=FIX.4.435=B34=249=TW52=<TIME>56=ISLD148=no_echo
11+
12+
# Heartbeat with seq 5 (when 3 is expected)
13+
I8=FIX.4.435=034=549=TW52=<TIME>56=ISLD
14+
15+
# Expect ResendRequest for 3+
16+
E8=FIX.4.435=234=249=ISLD52=00000000-00:00:00.00056=TW7=316=010=0
17+
18+
# Resend 3 and 4
19+
I8=FIX.4.435=B34=343=Y49=TW52=<TIME>56=ISLD148=no_echo
20+
I8=FIX.4.435=B34=443=Y49=TW52=<TIME>56=ISLD148=no_echo
21+
sleep(0.2)
22+
23+
# Session now processes the Heartbeat (seq=5) from its queue
24+
E8=FIX.4.435=034=349=ISLD52=00000000-00:00:00.00056=TW10=0
25+
26+
# Counterparty is still resending - it is on 5 now.
27+
# 5 is that heartbeat, an admin msg, so send a GapFill instead.
28+
# The EndSeqNo is 7, completely skipping over 6.
29+
I8=FIX.4.435=434=549=TW52=<TIME>56=ISLD43=Y122=<TIME-1>36=7123=Y
30+
31+
# Do an echo to ensure that client obeyed the SequenceReset and set NextExpected=7
32+
I8=FIX.4.435=B34=749=TW52=<TIME>56=ISLD148=echo: NextExpected/7
33+
E8=FIX.4.435=B34=449=ISLD52=00000000-00:00:00.00056=TW148=NextExpected/710=0

QuickFIXn/Session.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ public void Next()
523523
public void Next(string msgStr)
524524
{
525525
NextMessage(msgStr);
526+
_state.LastProcessedMessageWasQueued = false;
526527
NextQueued();
527528
}
528529

@@ -962,8 +963,20 @@ public bool Verify(Message msg, bool checkTooHigh = true, bool checkTooLow = tru
962963
}
963964
if (checkTooLow && IsTargetTooLow(msgSeqNum))
964965
{
965-
DoTargetTooLow(msg, msgSeqNum);
966-
return false;
966+
if (_state.LastProcessedMessageWasQueued
967+
&& msg.Header.GetString(35) == MsgType.SEQUENCE_RESET
968+
&& msg.GetBoolean(Tags.GapFillFlag))
969+
{
970+
Log.Log(LogLevel.Warning,
971+
"SequenceReset-GapFill 34={MsgSeqNum} is too low (expected {NextTargetMsgSeqNum}), but in this case I'm going to obey it anyway",
972+
msgSeqNum, _state.NextTargetMsgSeqNum);
973+
// This is an uncommon situation, see #309
974+
}
975+
else
976+
{
977+
DoTargetTooLow(msg, msgSeqNum);
978+
return false;
979+
}
967980
}
968981

969982
if (IsResendRequested)
@@ -1594,6 +1607,7 @@ protected bool NextQueued(SeqNumType num)
15941607
{
15951608
NextMessage(msg.ConstructString());
15961609
}
1610+
_state.LastProcessedMessageWasQueued = true;
15971611
return true;
15981612
}
15991613
return false;

QuickFIXn/SessionState.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ public bool IsInitiator
4848

4949
public ILogger Log { get; }
5050

51+
/// <summary>
52+
/// True if the last message processed was an admin message from the queue.
53+
/// Needed for an obscure SequenceReset scenario (see issue #390).
54+
/// </summary>
55+
internal bool LastProcessedMessageWasQueued { get; set; }
56+
5157
#endregion
5258

5359
#region Synchronized Properties

RELEASE_NOTES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ What's New
4545
* #939 - minor checkTooHigh/checkTooLow refactor in Session.cs (gbirchmeier)
4646
* #941 - clarify ResendRequest-related log message, add UT coverage for Session (gbirchmeier)
4747
* #895 - fix: When SSLCACertificate is empty an error is logged and it fails to start (dckorben)
48-
* #942 - fix #942: field 369 (LastMsgSeqNumProcessed) wrong in ResendRequest message (gbirchmeier)
48+
* #942 - fix: field 369 (LastMsgSeqNumProcessed) wrong in ResendRequest message (gbirchmeier)
4949
* #940 - Create an alternate CharEncoding.GetBytes impl which uses ArrayPool to improve memory performance (VAllens)
5050
* #951 - fix: restore Session disconnect during SocketInitiatorThread.Read exception (gbirchmeier/trevor-bush)
5151
* #963 - fix: concurrency bug with NonSessionLog on Windows (gbirchmeier)
@@ -57,6 +57,8 @@ What's New
5757
* #969 - correct LinesOfText in DDs to not be required; make ATs not auto-echo News (gbirchmeier)
5858
* #965 - Reusing StringBuilder with Object Pooling (VAllens)
5959
* #980 - fix: ToJSON returns invalid json when content contains newlines/tabs/etc (Rob-Hague)
60+
* #309 - fix: obey a SeqReset-GapFill even if it 'replaces' a message that was processed off
61+
queue in a ResendRequest series (gbirchmeier/oclancy)
6062

6163
### v1.13.1
6264
* backport #951 to 1.13

UnitTests/SessionTest.cs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,4 +762,103 @@ public void TestBasicResendRequest_WithMax() {
762762
Assert.That(_session.IsResendRequested, Is.False, $"seq num {i}");
763763
}
764764
}
765+
766+
[Test]
767+
public void TestSequenceResetNoGapFillIsProcessed()
768+
{
769+
Assert.That(_session!.IgnorePossDupResendRequests, Is.EqualTo(false));
770+
Logon();
771+
SendNOSMessage();
772+
SendNOSMessage();
773+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(4));
774+
775+
QuickFix.FIX42.SequenceReset sr = new(new NewSeqNo(150));
776+
SendTheMessage(sr);
777+
778+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(150));
779+
}
780+
781+
[Test]
782+
public void TestSequenceResetWithGapFillIsProcessed()
783+
{
784+
Assert.That(_session!.IgnorePossDupResendRequests, Is.EqualTo(false));
785+
Logon();
786+
SendNOSMessage();
787+
SendNOSMessage();
788+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(4));
789+
790+
QuickFix.FIX42.SequenceReset sr = new(new NewSeqNo(150));
791+
sr.GapFillFlag = new GapFillFlag(true);
792+
SendTheMessage(sr);
793+
794+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(150));
795+
}
796+
797+
[Test]
798+
public void TestSequenceResetDuringResendRequestIsProcessed()
799+
{
800+
Assert.That(_session!.IgnorePossDupResendRequests, Is.EqualTo(false));
801+
_session.RequiresOrigSendingTime = false; // default is true
802+
Logon();
803+
SendNOSMessage(); // seq 2
804+
SendNOSMessage(10); // causes ResendRequest to be sent
805+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(3));
806+
807+
var resendRequest =
808+
(QuickFix.FIX42.ResendRequest)_responder.MsgLookup[MsgType.RESEND_REQUEST].Dequeue();
809+
Assert.That(resendRequest.BeginSeqNo.Value, Is.EqualTo(3));
810+
Assert.That(resendRequest.EndSeqNo.Value, Is.EqualTo(0));
811+
812+
// Resend & GapFill through seq=9
813+
SendNOSMessage(3);
814+
SendNOSMessage(4);
815+
QuickFix.FIX42.SequenceReset sr = new(new NewSeqNo(9));
816+
SendTheMessage(sr, 5);
817+
SendNOSMessage(9);
818+
819+
// We already processed 10, so the next one is 11
820+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(11));
821+
}
822+
823+
[Test]
824+
public void TestObeyGapFillIfItReplacesAMessageOffTheQueue()
825+
{
826+
// issue #309: If GapFill has the same seqno of a message that was
827+
// processed off the queue, obey it anyway
828+
829+
Assert.That(_session!.IgnorePossDupResendRequests, Is.EqualTo(false));
830+
_session.RequiresOrigSendingTime = false; // default is true
831+
Logon();
832+
SendNOSMessage(); // seq 2
833+
834+
QuickFix.FIX42.Heartbeat hb = new();
835+
SendTheMessage(hb, 5); // seq=5, causes ResendRequest to be sent
836+
837+
// Verify the ResendRequest that was just sent
838+
var resendRequest =
839+
(QuickFix.FIX42.ResendRequest)_responder.MsgLookup[MsgType.RESEND_REQUEST].Dequeue();
840+
Assert.That(resendRequest.BeginSeqNo.Value, Is.EqualTo(3));
841+
Assert.That(resendRequest.EndSeqNo.Value, Is.EqualTo(0));
842+
843+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(3));
844+
Assert.That(_session.IsResendRequested);
845+
846+
// Resends
847+
SendNOSMessage(3, possDupFlag: true);
848+
SendNOSMessage(4, possDupFlag: true);
849+
// Now the session processes the Heartbeat/seq=5 from its queue.
850+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(6));
851+
852+
// But the counterparty still needs to resend the seq=5!
853+
// The 5 is a Heartbeat, an admin message, so it's a gapfill.
854+
// The gapfill goes to 7, omitting ever sending a 6.
855+
QuickFix.FIX42.SequenceReset sr = new(new NewSeqNo(7));
856+
sr.Header.SetField(new PossDupFlag(true));
857+
sr.SetField(new GapFillFlag(true));
858+
SendTheMessage(sr, 5);
859+
860+
// Even though client already processed the original Heartbeat/seq=5,
861+
// it must not discard the GapFill 5.
862+
Assert.That(_session.NextTargetMsgSeqNum, Is.EqualTo(7));
863+
}
765864
}

UnitTests/SessionTestBase.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,12 @@ public void SendNOSMessage()
183183
_session!.Next(CreateNOSMessage(_seqNum++).ConstructString());
184184
}
185185

186-
public void SendNOSMessage(SeqNumType n)
186+
public void SendNOSMessage(SeqNumType n, bool possDupFlag=false)
187187
{
188-
_session!.Next(CreateNOSMessage(n).ConstructString());
188+
var msg = CreateNOSMessage(n);
189+
if (possDupFlag)
190+
msg.Header.SetField(new QuickFix.Fields.PossDupFlag(possDupFlag));
191+
_session!.Next(msg.ConstructString());
189192
}
190193

191194
public void SendResendRequest(SeqNumType begin, SeqNumType end)
@@ -207,7 +210,6 @@ protected void SendTheMessage(QuickFix.Message msg, SeqNumType n = 0)
207210
msg.Header.SetField(new QuickFix.Fields.TargetCompID(_sessionId.SenderCompID));
208211
msg.Header.SetField(new QuickFix.Fields.SenderCompID(_sessionId.TargetCompID));
209212
msg.Header.SetField(new QuickFix.Fields.MsgSeqNum(n == 0 ? _seqNum++ : n));
210-
211213
_session!.Next(msg.ConstructString());
212214
}
213215
}

0 commit comments

Comments
 (0)