Skip to content

Commit 4bd79e3

Browse files
Merge pull request #768 from quickfixgo/resends-cannot-interleave-with-live-traffic
Resend Requests should not interleave with live traffic
2 parents c786b92 + 027f25a commit 4bd79e3

2 files changed

Lines changed: 78 additions & 12 deletions

File tree

‎in_session_test.go‎

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
package quickfix
1717

1818
import (
19+
"bytes"
1920
"testing"
2021
"time"
2122

22-
"github.com/stretchr/testify/mock"
2323
"github.com/stretchr/testify/suite"
2424

2525
"github.com/quickfixgo/quickfix/internal"
@@ -374,23 +374,84 @@ func (s *InSessionTestSuite) TestFIXMsgInResendRequestDoNotSendApp() {
374374
s.State(inSession{})
375375
}
376376

377-
func (s *InSessionTestSuite) TestFIXMsgInResendRequestBlocksSend() {
377+
// resendInterceptStore delegates to a real store but hooks IterateMessages so a
378+
// test can simulate an application goroutine sending live traffic mid-replay.
379+
type resendInterceptStore struct {
380+
MessageStore
381+
afterFirstMsg func()
382+
}
383+
384+
func (s *resendInterceptStore) IterateMessages(beginSeqNum, endSeqNum int, cb func([]byte) error) error {
385+
n := 0
386+
return s.MessageStore.IterateMessages(beginSeqNum, endSeqNum, func(msgBytes []byte) error {
387+
err := cb(msgBytes)
388+
if n++; n == 1 && s.afterFirstMsg != nil {
389+
s.afterFirstMsg()
390+
}
391+
return err
392+
})
393+
}
394+
395+
func (s *InSessionTestSuite) TestFIXMsgInResendRequestLiveMessagesDoNotInterleave() {
378396
s.MockApp.On("ToApp").Return(nil)
379-
s.Require().Nil(s.session.send(s.NewOrderSingle()))
380-
s.LastToAppMessageSent()
381-
s.MockApp.AssertNumberOfCalls(s.T(), "ToApp", 1)
382-
s.NextSenderMsgSeqNum(2)
397+
for i := 0; i < 3; i++ {
398+
s.Require().Nil(s.session.send(s.NewOrderSingle()))
399+
}
400+
s.NextSenderMsgSeqNum(4)
401+
for {
402+
if msgBytes, _ := s.Receiver.LastMessage(); msgBytes == nil {
403+
break
404+
}
405+
}
383406

384-
s.MockStore.On("IterateMessages", mock.Anything, mock.Anything, mock.AnythingOfType("func([]byte) error")).
385-
Run(func(_ mock.Arguments) {
386-
s.Require().Nil(s.session.send(s.NewOrderSingle()))
387-
}).
388-
Return(nil)
407+
// While the replay is in progress, an application goroutine sends a live
408+
// message the same way SendToTarget does. The send must block until the
409+
// replay completes so the message cannot reach the wire mid-replay.
410+
liveSent := make(chan error, 1)
411+
s.session.store = &resendInterceptStore{
412+
MessageStore: &s.MockStore,
413+
afterFirstMsg: func() {
414+
go func() {
415+
liveSent <- s.session.queueForSend(s.NewOrderSingle())
416+
}()
417+
select {
418+
case err := <-liveSent:
419+
s.Failf("", "live send completed during replay (err=%v)", err)
420+
case <-time.After(20 * time.Millisecond):
421+
}
422+
},
423+
}
389424

390425
s.MockApp.On("FromAdmin").Return(nil)
391426
s.fixMsgIn(s.session, s.ResendRequest(1))
392427

393-
s.NextSenderMsgSeqNum(2)
428+
select {
429+
case err := <-liveSent:
430+
s.Require().Nil(err)
431+
case <-time.After(5 * time.Second):
432+
s.FailNow("live send never completed after the replay finished")
433+
}
434+
435+
for expectedSeqNum := 1; expectedSeqNum <= 3; expectedSeqNum++ {
436+
msgBytes, _ := s.Receiver.LastMessage()
437+
s.Require().NotNil(msgBytes)
438+
msg := NewMessage()
439+
s.Require().Nil(ParseMessage(msg, bytes.NewBuffer(msgBytes)))
440+
s.FieldEquals(tagMsgSeqNum, expectedSeqNum, msg.Header)
441+
s.FieldEquals(tagPossDupFlag, true, msg.Header)
442+
}
443+
msgBytes, _ := s.Receiver.LastMessage()
444+
s.Nil(msgBytes, "live message must not be sent during the replay")
445+
446+
// The live message stays queued and goes out once the event loop resumes.
447+
s.session.SendAppMessages(s.session)
448+
msgBytes, _ = s.Receiver.LastMessage()
449+
s.Require().NotNil(msgBytes)
450+
msg := NewMessage()
451+
s.Require().Nil(ParseMessage(msg, bytes.NewBuffer(msgBytes)))
452+
s.FieldEquals(tagMsgSeqNum, 4, msg.Header)
453+
s.False(msg.Header.Has(tagPossDupFlag))
454+
s.NextSenderMsgSeqNum(5)
394455
}
395456

396457
func (s *InSessionTestSuite) TestFIXMsgInTargetTooLow() {

‎session.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,11 @@ func (s *session) resend(msg *Message) bool {
275275

276276
// queueForSend will validate, persist, and queue the message for send.
277277
func (s *session) queueForSend(msg *Message) error {
278+
// resendMutex must always be locked before sendMutex to prevent a potential deadlock.
279+
// Makes sure that live traffic cannot interleave with replayed messages while processing a ResendRequest.
280+
s.resendMutex.RLock()
281+
defer s.resendMutex.RUnlock()
282+
278283
s.sendMutex.Lock()
279284
defer s.sendMutex.Unlock()
280285

0 commit comments

Comments
 (0)