From 98188940db5412ca7e98c1c0429753a0f502d97e Mon Sep 17 00:00:00 2001 From: Douglas Muraoka Date: Sun, 10 Dec 2023 17:46:40 -0300 Subject: [PATCH] fix: gap fill with wrong seq num & not inflating if end seq is 0 --- src/store/fix-msg-ascii-store-resend.ts | 28 +++++-- src/test/ascii/ascii-store-replay.test.ts | 3 +- src/test/ascii/session.test.ts | 93 +++++++++++++++++++---- src/transport/ascii/ascii-session.ts | 6 +- 4 files changed, 107 insertions(+), 23 deletions(-) diff --git a/src/store/fix-msg-ascii-store-resend.ts b/src/store/fix-msg-ascii-store-resend.ts index e3070905..7ed165b1 100644 --- a/src/store/fix-msg-ascii-store-resend.ts +++ b/src/store/fix-msg-ascii-store-resend.ts @@ -28,6 +28,12 @@ export class FixMsgAsciiStoreResend { private inflateRange (startSeq: number, endSeq: number, input: IFixMsgStoreRecord[]): IFixMsgStoreRecord[] { const toResend: IFixMsgStoreRecord[] = [] + // If no records for this given sequence number range, returns a single gap fill + if (input.length === 0) { + this.gap(startSeq, endSeq + 1, toResend) + return toResend + } + let expected = startSeq for (let i = 0; i < input.length; ++i) { const record = this.prepareRecordForRetransmission(input[i]) @@ -48,16 +54,16 @@ export class FixMsgAsciiStoreResend { return toResend } - public gap (beginGap: number, seqNum: number, arr: IFixMsgStoreRecord[]): void { + private gap (beginGap: number, newSeq: number, arr: IFixMsgStoreRecord[]): void { if (beginGap > 0) { - arr.push(this.sequenceResetGap(beginGap, seqNum)) + arr.push(this.sequenceResetGap(beginGap, newSeq)) } } // if records were sent as encoded text then inflate back to object // so can be resent or examined - public inflate (record: IFixMsgStoreRecord): void { + private inflate (record: IFixMsgStoreRecord): void { if (record.obj) return if (!record.encoded) return const parser = this.parser @@ -71,18 +77,26 @@ export class FixMsgAsciiStoreResend { parser.parseText(record.encoded) } - public sequenceResetGap (startGap: number, newSeq: number): IFixMsgStoreRecord { + /** + * A continuous sequence of messages not being retransmitted should be skipped over using a + * single SequenceReset(35=4) message with GapFillFlag(123) set to “Y” and MsgSeqNum(34) set + * to the sequence number of the first skipped message and NewSeqNo(36) must always be set + * to the value of the next sequence number to be expected by the peer immediately following + * the messages being skipped. + */ + private sequenceResetGap (startGap: number, newSeq: number): IFixMsgStoreRecord { const factory = this.config.factory const gapFill: ISequenceReset = factory?.sequenceReset(newSeq, true) as ISequenceReset gapFill.StandardHeader = factory?.header(MsgType.SequenceReset, startGap) as IStandardHeader gapFill.StandardHeader.PossDupFlag = true - gapFill.NewSeqNo = newSeq + return new FixMsgStoreRecord( MsgType.SequenceReset, new Date(), - newSeq, + startGap, gapFill, - null) + null, + ) } /** diff --git a/src/test/ascii/ascii-store-replay.test.ts b/src/test/ascii/ascii-store-replay.test.ts index d1826b51..e0978b4c 100644 --- a/src/test/ascii/ascii-store-replay.test.ts +++ b/src/test/ascii/ascii-store-replay.test.ts @@ -120,7 +120,8 @@ function checkSeqReset (rec: IFixMsgStoreRecord, from: number, to: number): void const reset: ISequenceReset = rec.obj as ISequenceReset expect(rec.msgType).toEqual(MsgType.SequenceReset) expect(rec.obj).toBeTruthy() - expect(rec.seqNum).toEqual(to) + expect(rec.seqNum).toEqual(from) + expect(reset.NewSeqNo).toEqual(to) expect(reset.GapFillFlag).toBeTruthy() expect(reset.StandardHeader.MsgType).toEqual(MsgType.SequenceReset) expect(reset.StandardHeader.PossDupFlag).toBeTruthy() diff --git a/src/test/ascii/session.test.ts b/src/test/ascii/session.test.ts index d6ba19cf..1c861e2e 100644 --- a/src/test/ascii/session.test.ts +++ b/src/test/ascii/session.test.ts @@ -72,23 +72,88 @@ test('end to end logon', async () => { test('session send resendRequest when logged on', async () => { const runner: SkeletonRunner = new SkeletonRunner(experiment, 2) const factory = experiment.client.config.factory - const resend = factory?.resendRequest(1, 2) + const resend = factory?.resendRequest(1, 1) expect(resend).toBeTruthy() if (!resend) return runner.sendMsg(MsgType.ResendRequest, resend) - try { - const cViews = experiment.client.views - const sViews = experiment.server.views - await runner.wait() - const last = experiment.client.views[experiment.client.views.length - 1] - expect(last).toBeTruthy() - const clientResets = countOfType('SequenceReset', cViews) - const serverResets = countOfType('SequenceReset', sViews) - expect(clientResets).toEqual(1) - expect(serverResets).toEqual(0) - } catch (e) { - expect(true).toEqual(false) - } + + const cViews = experiment.client.views + const sViews = experiment.server.views + await runner.wait() + + expect(cViews).toHaveLength(3) + expect(sViews).toHaveLength(3) + + const resendRequestView = sViews[1] + expect(resendRequestView.segment.name).toBe('ResendRequest') + expect(resendRequestView.getTyped('MsgSeqNum')).toBe(2) + expect(resendRequestView.getTyped('BeginSeqNo')).toBe(1) + expect(resendRequestView.getTyped('EndSeqNo')).toBe(1) + + const seqResetView = cViews[1] + expect(seqResetView.segment.name).toBe('SequenceReset') + expect(seqResetView.getTyped('MsgSeqNum')).toBe(1) + expect(seqResetView.getTyped('NewSeqNo')).toBe(2) + expect(seqResetView.getTyped('GapFillFlag')).toBe(true) + expect(seqResetView.getTyped('PossDupFlag')).toBe(true) + + const logoutSView = sViews[2] + expect(logoutSView.segment.name).toBe('Logout') + expect(logoutSView.getTyped('MsgSeqNum')).toBe(3) + + const logoutCView = cViews[2] + expect(logoutCView.segment.name).toBe('Logout') + expect(logoutCView.getTyped('MsgSeqNum')).toBe(2) + + const clientResets = countOfType('SequenceReset', cViews) + const serverResets = countOfType('SequenceReset', sViews) + console.log('SERVER VIEWS', sViews.map((a => a.toJson()))); + console.log('CLIENT VIEWS', cViews.map((a => a.toJson()))); + expect(clientResets).toEqual(1) + expect(serverResets).toEqual(0) +}) + +test('session send resendRequest with endSeqNo = 0 when logged on', async () => { + const runner: SkeletonRunner = new SkeletonRunner(experiment, 2) + const factory = experiment.client.config.factory + + const resend = factory?.resendRequest(1, 0) + expect(resend).toBeTruthy() + if (!resend) return + runner.sendMsg(MsgType.ResendRequest, resend) + + const cViews = experiment.client.views + const sViews = experiment.server.views + await runner.wait() + + expect(cViews).toHaveLength(3) + expect(sViews).toHaveLength(3) + + const resendRequestView = sViews[1] + expect(resendRequestView.segment.name).toBe('ResendRequest') + expect(resendRequestView.getTyped('MsgSeqNum')).toBe(2) + expect(resendRequestView.getTyped('BeginSeqNo')).toBe(1) + expect(resendRequestView.getTyped('EndSeqNo')).toBe(0) + + const seqResetView = cViews[1] + expect(seqResetView.segment.name).toBe('SequenceReset') + expect(seqResetView.getTyped('MsgSeqNum')).toBe(1) + expect(seqResetView.getTyped('NewSeqNo')).toBe(2) + expect(seqResetView.getTyped('GapFillFlag')).toBe(true) + expect(seqResetView.getTyped('PossDupFlag')).toBe(true) + + const logoutSView = sViews[2] + expect(logoutSView.segment.name).toBe('Logout') + expect(logoutSView.getTyped('MsgSeqNum')).toBe(3) + + const logoutCView = cViews[2] + expect(logoutCView.segment.name).toBe('Logout') + expect(logoutCView.getTyped('MsgSeqNum')).toBe(2) + + const clientResets = countOfType('SequenceReset', cViews) + const serverResets = countOfType('SequenceReset', sViews) + expect(clientResets).toEqual(1) + expect(serverResets).toEqual(0) }) test('session send logon when logged on', async () => { diff --git a/src/transport/ascii/ascii-session.ts b/src/transport/ascii/ascii-session.ts index 4fe787ca..65c80947 100644 --- a/src/transport/ascii/ascii-session.ts +++ b/src/transport/ascii/ascii-session.ts @@ -160,7 +160,11 @@ export abstract class AsciiSession extends FixSession { protected onResendRequest (view: MsgView): void { // if no records are in store then send a gap fill for entire sequence this.setState(SessionState.HandleResendRequest) - const [beginSeqNo, endSeqNo] = view.getTypedTags([MsgTag.BeginSeqNo, MsgTag.EndSeqNo]) + const [beginSeqNo, requestedEndSeqNo] = view.getTypedTags([MsgTag.BeginSeqNo, MsgTag.EndSeqNo]) + const endSeqNo = requestedEndSeqNo === 0 + ? this.sessionState.lastSentSeqNum() + : requestedEndSeqNo + this.sessionLogger.info(`onResendRequest getResendRequest beginSeqNo = ${beginSeqNo}, endSeqNo = ${endSeqNo}`) this.resender.getResendRequest(beginSeqNo as number, endSeqNo as number).then((records: IFixMsgStoreRecord[]) => { const validRecords = records.filter(rec => rec.obj !== null)