Skip to content

Commit ceb4702

Browse files
committed
Issue artiofix#503: SessionProxy.sendSequenceReset() not called
The test also tries to reproduce an inversion of SendSequenceReset and ResendRequest messages, but it is not reproduced (it happens only when a SessionProxy causes the ResendRequest to be sent asynchronously).
1 parent e1961a0 commit ceb4702

File tree

4 files changed

+423
-0
lines changed

4 files changed

+423
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package uk.co.real_logic.artio.system_tests;
2+
3+
import org.agrona.ErrorHandler;
4+
import org.agrona.concurrent.EpochNanoClock;
5+
import org.junit.Test;
6+
import uk.co.real_logic.artio.dictionary.generation.Exceptions;
7+
import uk.co.real_logic.artio.engine.EngineConfiguration;
8+
import uk.co.real_logic.artio.engine.FixEngine;
9+
import uk.co.real_logic.artio.fields.EpochFractionFormat;
10+
import uk.co.real_logic.artio.library.LibraryConfiguration;
11+
import uk.co.real_logic.artio.protocol.GatewayPublication;
12+
import uk.co.real_logic.artio.session.DirectSessionProxy;
13+
import uk.co.real_logic.artio.session.SessionCustomisationStrategy;
14+
import uk.co.real_logic.artio.session.SessionIdStrategy;
15+
import uk.co.real_logic.artio.session.SessionProxy;
16+
import uk.co.real_logic.artio.util.DebugFIXClient;
17+
import uk.co.real_logic.artio.util.DebugServer;
18+
19+
import java.io.IOException;
20+
21+
import static org.junit.Assert.assertEquals;
22+
import static org.junit.Assert.assertTrue;
23+
import static uk.co.real_logic.artio.TestFixtures.launchMediaDriver;
24+
import static uk.co.real_logic.artio.messages.InitialAcceptedSessionOwner.SOLE_LIBRARY;
25+
import static uk.co.real_logic.artio.system_tests.SystemTestUtil.ACCEPTOR_ID;
26+
import static uk.co.real_logic.artio.system_tests.SystemTestUtil.INITIATOR_ID;
27+
import static uk.co.real_logic.artio.system_tests.SystemTestUtil.acceptingConfig;
28+
import static uk.co.real_logic.artio.system_tests.SystemTestUtil.acceptingLibraryConfig;
29+
import static uk.co.real_logic.artio.system_tests.SystemTestUtil.connect;
30+
import static uk.co.real_logic.artio.system_tests.SystemTestUtil.initiatingConfig;
31+
import static uk.co.real_logic.artio.system_tests.SystemTestUtil.initiatingLibraryConfig;
32+
33+
/**
34+
* Try reproducing race between sent ResendRequest and ResetSequence message when both
35+
* parties request a resend. Also checks that SessionProxy is invoked when a ResetSequence
36+
* message must be sent.
37+
*/
38+
public class RaceResendResetTest extends AbstractGatewayToGatewaySystemTest {
39+
40+
private boolean sendResendRequestCalled;
41+
private boolean sendSequenceResetCalled;
42+
private boolean useProxy;
43+
44+
private void launch() {
45+
mediaDriver = launchMediaDriver();
46+
launchAccepting();
47+
launchInitiating();
48+
testSystem = new TestSystem(acceptingLibrary, initiatingLibrary);
49+
}
50+
51+
private void launchInitiating() {
52+
final EngineConfiguration initiatingConfig =
53+
initiatingConfig(libraryAeronPort, nanoClock)
54+
.deleteLogFileDirOnStart(true)
55+
.initialAcceptedSessionOwner(SOLE_LIBRARY);
56+
initiatingEngine = FixEngine.launch(initiatingConfig);
57+
LibraryConfiguration lib = initiatingLibraryConfig(libraryAeronPort, initiatingHandler, nanoClock);
58+
if (useProxy)
59+
lib.sessionProxyFactory(this::sessionProxyFactory);
60+
initiatingLibrary = connect(lib
61+
);
62+
}
63+
64+
private void launchAccepting() {
65+
final EngineConfiguration acceptingConfig = acceptingConfig(port, ACCEPTOR_ID, INITIATOR_ID, nanoClock)
66+
.deleteLogFileDirOnStart(true)
67+
.initialAcceptedSessionOwner(SOLE_LIBRARY);
68+
acceptingEngine = FixEngine.launch(acceptingConfig);
69+
70+
final LibraryConfiguration acceptingLibraryConfig = acceptingLibraryConfig(acceptingHandler, nanoClock);
71+
acceptingLibrary = connect(acceptingLibraryConfig);
72+
}
73+
74+
/**
75+
* Sanity check that we can connect Artio to a debug server with canned messages.
76+
*/
77+
@Test
78+
public void testDebugServer() throws IOException {
79+
DebugServer srv = new DebugServer(port);
80+
srv.setWaitForData(true);
81+
srv.addFIXResponse("8=FIX.4.4|9=94|35=A|49=acceptor|56=initiator|34=1|52=20240315-10:52:24.098|98=0|108=10|141=N|35002=0|35003=0|10=024|");
82+
srv.start();
83+
84+
mediaDriver = launchMediaDriver();
85+
launchInitiating();
86+
testSystem = new TestSystem(initiatingLibrary);
87+
connectAndAcquire();
88+
}
89+
90+
private SessionProxy sessionProxyFactory(
91+
final int sessionBufferSize,
92+
final GatewayPublication gatewayPublication,
93+
final SessionIdStrategy sessionIdStrategy,
94+
final SessionCustomisationStrategy customisationStrategy,
95+
final EpochNanoClock clock,
96+
final long connectionId,
97+
final int libraryId,
98+
final ErrorHandler errorHandler,
99+
final EpochFractionFormat epochFractionPrecision) {
100+
return new DirectSessionProxy(sessionBufferSize, gatewayPublication, sessionIdStrategy, customisationStrategy,
101+
clock, connectionId, libraryId, errorHandler, epochFractionPrecision) {
102+
@Override
103+
public long sendResendRequest(int msgSeqNo, int beginSeqNo, int endSeqNo, int sequenceIndex, int lastMsgSeqNumProcessed) {
104+
sendResendRequestCalled = true;
105+
// try {
106+
// Thread.sleep(10);
107+
// } catch (InterruptedException ignored) {
108+
// }
109+
return super.sendResendRequest(msgSeqNo, beginSeqNo, endSeqNo, sequenceIndex, lastMsgSeqNumProcessed);
110+
}
111+
112+
@Override
113+
public long sendSequenceReset(int msgSeqNo, int newSeqNo, int sequenceIndex, int lastMsgSeqNumProcessed) {
114+
sendSequenceResetCalled = true;
115+
return super.sendSequenceReset(msgSeqNo, newSeqNo, sequenceIndex, lastMsgSeqNumProcessed);
116+
}
117+
};
118+
}
119+
120+
@Test(timeout = TEST_TIMEOUT_IN_MS)
121+
public void shouldNotInvertResendAndReset() throws Exception {
122+
useProxy = false;
123+
reconnectTest();
124+
}
125+
126+
@Test(timeout = TEST_TIMEOUT_IN_MS)
127+
public void shouldCallProxySendSequenceReset() throws Exception {
128+
useProxy = true;
129+
reconnectTest();
130+
}
131+
132+
private void reconnectTest() throws Exception {
133+
launch();
134+
135+
connectAndAcquire();
136+
137+
messagesCanBeExchanged();
138+
139+
disconnectSessions();
140+
Exceptions.closeAll(this::closeAcceptingEngine);
141+
142+
assertEquals(3, acceptingSession.lastReceivedMsgSeqNum());
143+
assertEquals(3, initiatingSession.lastReceivedMsgSeqNum());
144+
145+
DebugServer srv = new DebugServer(port);
146+
srv.setWaitForData(true);
147+
srv.addFIXResponse("8=FIX.4.4|9=94|35=A|49=acceptor|56=initiator|34=5|52=***|98=0|108=10|141=N|35002=0|35003=0|10=024|");
148+
srv.addFIXResponse("8=FIX.4.4|9=94|35=2|49=acceptor|56=initiator|34=6|52=***|7=4|16=0|10=024|");
149+
srv.start();
150+
151+
connectPersistentSessions(4, 4, false);
152+
153+
DebugFIXClient exchange = new DebugFIXClient(srv.popClient(5000));
154+
exchange.popAndAssert("35=A 34=4");
155+
exchange.popAndAssert("35=2 34=5 7=4 16=0");
156+
exchange.popAndAssert("35=4 34=4 36=6");
157+
158+
exchange.close();
159+
srv.stop();
160+
Exceptions.closeAll(this::closeInitiatingEngine, mediaDriver);
161+
162+
if (useProxy) {
163+
assertTrue("SessionProxy.sendResendRequest() not called", sendResendRequestCalled);
164+
assertTrue("SessionProxy.sendSequenceReset() not called", sendSequenceResetCalled);
165+
}
166+
}
167+
168+
private void connectAndAcquire() {
169+
connectSessions();
170+
acceptingSession = acceptingHandler.lastSession();
171+
}
172+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package uk.co.real_logic.artio.util;
2+
3+
import org.junit.Assert;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
import java.util.Objects;
8+
import java.util.Scanner;
9+
import java.util.concurrent.BlockingQueue;
10+
import java.util.concurrent.LinkedBlockingQueue;
11+
import java.util.concurrent.TimeUnit;
12+
13+
/**
14+
* Helper to pop FIX messages received on a socket.
15+
*
16+
* @see DebugServer
17+
*/
18+
public class DebugFIXClient {
19+
private final DebugServer.HasIOStream io;
20+
private final Thread thread;
21+
22+
private final BlockingQueue<Map<String, String>> messages = new LinkedBlockingQueue<>();
23+
private volatile boolean disposed;
24+
private String prefix = " <<< ";
25+
26+
public DebugFIXClient(DebugServer.HasIOStream io) {
27+
this.io = Objects.requireNonNull(io);
28+
thread = new Thread(this::run, "DebugFIXClient");
29+
thread.start();
30+
}
31+
32+
public void close() throws Exception {
33+
disposed = true;
34+
io.in.close();
35+
io.in.close();
36+
thread.interrupt();
37+
thread.join();
38+
}
39+
40+
private void run() {
41+
StringBuilder s = new StringBuilder(128);
42+
while (!disposed) {
43+
Scanner scanner = new Scanner(io.in).useDelimiter("\u0001");
44+
Map<String, String> msg = new HashMap<>();
45+
while (scanner.hasNext()) {
46+
String fld = scanner.next();
47+
s.append(fld).append('|');
48+
int eq = fld.indexOf('=');
49+
String tag = fld.substring(0, eq);
50+
msg.put(tag, fld.substring(eq + 1));
51+
if (tag.equals("10")) {
52+
messages.add(msg);
53+
msg = new HashMap<>();
54+
System.out.println(prefix + s);
55+
s.setLength(0);
56+
}
57+
}
58+
}
59+
}
60+
61+
public Map<String, String> popMessage() throws InterruptedException {
62+
return messages.poll(5, TimeUnit.SECONDS);
63+
}
64+
65+
public void popAndAssert(String tagValues) throws InterruptedException {
66+
Map<String, String> map = popMessage();
67+
for (String rule : tagValues.split(" ")) {
68+
String tag = rule.substring(0, rule.indexOf('='));
69+
if (map == null)
70+
throw new AssertionError("No message received");
71+
String value = map.get(tag);
72+
Assert.assertEquals(rule, tag + "=" + value);
73+
}
74+
}
75+
76+
public void setPrefix(String prefix) {
77+
this.prefix = prefix;
78+
}
79+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package uk.co.real_logic.artio.util;
2+
3+
import java.io.BufferedInputStream;
4+
import java.io.BufferedOutputStream;
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.io.OutputStream;
8+
import java.net.ServerSocket;
9+
import java.net.Socket;
10+
import java.util.Queue;
11+
import java.util.concurrent.BlockingQueue;
12+
import java.util.concurrent.ConcurrentLinkedQueue;
13+
import java.util.concurrent.LinkedBlockingQueue;
14+
import java.util.concurrent.TimeUnit;
15+
16+
/**
17+
* A server that accepts TCP connections and is able to reply automatically with canned
18+
* data. It can be used to simulate a FIX server in order to quickly sent specific messages.
19+
*/
20+
public class DebugServer {
21+
22+
private final int port;
23+
private final Queue<byte[]> connectResponses;
24+
private final BlockingQueue<HasIOStream> clients;
25+
private final ServerSocket serverSocket;
26+
27+
/**
28+
* If true, wait until some data is received before sending prepared messages.
29+
*/
30+
private boolean waitForData;
31+
32+
/**
33+
* Creates a debug server listening on specified port.
34+
*/
35+
public DebugServer(int port) throws IOException {
36+
this.port = port;
37+
this.connectResponses = new ConcurrentLinkedQueue<>();
38+
this.clients = new LinkedBlockingQueue<>();
39+
this.serverSocket = new ServerSocket(port);
40+
}
41+
42+
/**
43+
* Adds a message that must be directly sent to connecting clients. Messages
44+
* are sent in the same order they were added.
45+
*/
46+
public void addConnectResponse(byte[] message) {
47+
connectResponses.add(message);
48+
}
49+
50+
/**
51+
* Warning: causes problems because SendingTime and checksum needs to be regenerated
52+
* and they are not.
53+
*/
54+
public void addFIXResponse(String msg) {
55+
addConnectResponse(FixMessageTweak.recycle(msg));
56+
}
57+
58+
/**
59+
* Starts the debug server, accepting incoming connections and sending
60+
* prepared data.
61+
*/
62+
public void start() throws IOException {
63+
new Thread("DebugServer-" + port) {
64+
@Override
65+
public void run() {
66+
try {
67+
while (!serverSocket.isClosed()) {
68+
Socket s = serverSocket.accept();
69+
System.out.println("Connection accepted from " + s.getInetAddress());
70+
try {
71+
BufferedInputStream in = new BufferedInputStream(s.getInputStream());
72+
BufferedOutputStream out = new BufferedOutputStream(s.getOutputStream());
73+
74+
if (!connectResponses.isEmpty() && waitForData) {
75+
in.mark(0);
76+
in.read();
77+
in.reset();
78+
}
79+
80+
HasIOStream client = new HasIOStream(in, out);
81+
sendResponses(client.out);
82+
clients.add(client);
83+
} catch (IOException e) {
84+
e.printStackTrace();
85+
}
86+
}
87+
} catch (IOException e) {
88+
if (!serverSocket.isClosed())
89+
e.printStackTrace();
90+
}
91+
}
92+
}.start();
93+
}
94+
95+
public void stop() throws IOException {
96+
serverSocket.close();
97+
}
98+
99+
/**
100+
* Sends prepared data to the client.
101+
*/
102+
private void sendResponses(OutputStream outputStream) throws IOException {
103+
for (byte[] response : connectResponses) {
104+
outputStream.write(response);
105+
outputStream.flush();
106+
}
107+
}
108+
109+
public HasIOStream popClient(long timeoutMs) throws InterruptedException {
110+
return clients.poll(timeoutMs, TimeUnit.MILLISECONDS);
111+
}
112+
113+
public int getPort() {
114+
return port;
115+
}
116+
117+
public void setWaitForData(boolean waitForData) {
118+
this.waitForData = waitForData;
119+
}
120+
121+
public static class HasIOStream {
122+
123+
public final InputStream in;
124+
public final OutputStream out;
125+
126+
public HasIOStream(InputStream in, OutputStream out) {
127+
this.in = in;
128+
this.out = out;
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)