Skip to content

Commit dac5858

Browse files
Copilotchrjohn
andcommitted
Add test for thread leak and implement fix in SessionConnector
Co-authored-by: chrjohn <[email protected]>
1 parent 3b41fe0 commit dac5858

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ private String getLogSuffix(SessionID sessionID, IoSession protocolSession) {
311311
}
312312

313313
protected void startSessionTimer() {
314+
// Check if a session timer is already running to avoid creating multiple timers
315+
if (sessionTimerFuture != null && !sessionTimerFuture.isDone()) {
316+
log.debug("SessionTimer already running, not creating a new one");
317+
return;
318+
}
314319
Runnable timerTask = new SessionTimerTask();
315320
if (shortLivedExecutor != null) {
316321
timerTask = new DelegatingTask(timerTask, shortLivedExecutor);
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*******************************************************************************
2+
* Copyright (c) quickfixengine.org All rights reserved.
3+
*
4+
* This file is part of the QuickFIX FIX Engine
5+
*
6+
* This file may be distributed under the terms of the quickfixengine.org
7+
* license as defined by quickfixengine.org and appearing in the file
8+
* LICENSE included in the packaging of this file.
9+
*
10+
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
11+
* THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
12+
* PARTICULAR PURPOSE.
13+
*
14+
* See http://www.quickfixengine.org/LICENSE for licensing information.
15+
*
16+
* Contact [email protected] if any conditions of this licensing
17+
* are not clear to you.
18+
******************************************************************************/
19+
20+
package quickfix.mina.initiator;
21+
22+
import org.junit.After;
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
import quickfix.*;
26+
import quickfix.mina.EventHandlingStrategy;
27+
import quickfix.mina.SessionConnector;
28+
29+
import java.lang.reflect.Field;
30+
import java.util.HashMap;
31+
import java.util.concurrent.ScheduledFuture;
32+
33+
import static org.junit.Assert.*;
34+
35+
/**
36+
* Test for verifying that calling createDynamicSession multiple times does not
37+
* create a thread leak by creating multiple session timers.
38+
*/
39+
public class DynamicSessionThreadLeakTest {
40+
41+
private TestAbstractSocketInitiator initiator;
42+
private SessionSettings settings;
43+
44+
@Before
45+
public void setUp() throws Exception {
46+
SystemTime.setTimeSource(null);
47+
48+
// Set up minimal session settings
49+
settings = new SessionSettings();
50+
settings.setString(Session.SETTING_USE_DATA_DICTIONARY, "N");
51+
settings.setString(Session.SETTING_START_TIME, "00:00:00");
52+
settings.setString(Session.SETTING_END_TIME, "00:00:00");
53+
settings.setLong(Session.SETTING_HEARTBTINT, 100L);
54+
settings.setString(SocketInitiator.SETTING_SOCKET_CONNECT_HOST, "127.0.0.1");
55+
settings.setString(SocketInitiator.SETTING_SOCKET_CONNECT_PORT, "54321");
56+
settings.setString(SessionFactory.SETTING_CONNECTION_TYPE, SessionFactory.INITIATOR_CONNECTION_TYPE);
57+
58+
DefaultSessionFactory sessionFactory = new DefaultSessionFactory(
59+
new UnitTestApplication(),
60+
new MemoryStoreFactory(),
61+
new SLF4JLogFactory(new SessionSettings()));
62+
63+
initiator = new TestAbstractSocketInitiator(settings, sessionFactory);
64+
}
65+
66+
@After
67+
public void tearDown() throws Exception {
68+
if (initiator != null) {
69+
initiator.stop(true);
70+
}
71+
}
72+
73+
/**
74+
* Test that multiple calls to createDynamicSession do not create multiple session timers.
75+
* Only one session timer should be running at a time.
76+
*/
77+
@Test
78+
public void testMultipleCreateDynamicSessionDoesNotLeakThreads() throws Exception {
79+
SessionID sessionID1 = new SessionID(FixVersions.BEGINSTRING_FIX42, "SENDER1", "TARGET1");
80+
SessionID sessionID2 = new SessionID(FixVersions.BEGINSTRING_FIX42, "SENDER2", "TARGET2");
81+
SessionID sessionID3 = new SessionID(FixVersions.BEGINSTRING_FIX42, "SENDER3", "TARGET3");
82+
83+
// Create first dynamic session
84+
initiator.createDynamicSession(sessionID1);
85+
assertTrue("Session timer should be running after first createDynamicSession",
86+
initiator.isSessionTimerRunning());
87+
ScheduledFuture<?> firstTimer = initiator.getSessionTimerFuture();
88+
assertNotNull("Session timer future should not be null", firstTimer);
89+
assertFalse("First timer should not be cancelled", firstTimer.isCancelled());
90+
91+
// Create second dynamic session
92+
initiator.createDynamicSession(sessionID2);
93+
assertTrue("Session timer should still be running after second createDynamicSession",
94+
initiator.isSessionTimerRunning());
95+
ScheduledFuture<?> secondTimer = initiator.getSessionTimerFuture();
96+
assertNotNull("Session timer future should not be null", secondTimer);
97+
98+
// The key assertion: The timer should be the same instance, not a new one
99+
assertSame("Session timer should be reused, not recreated", firstTimer, secondTimer);
100+
assertFalse("First timer should still not be cancelled", firstTimer.isCancelled());
101+
102+
// Create third dynamic session
103+
initiator.createDynamicSession(sessionID3);
104+
assertTrue("Session timer should still be running after third createDynamicSession",
105+
initiator.isSessionTimerRunning());
106+
ScheduledFuture<?> thirdTimer = initiator.getSessionTimerFuture();
107+
assertNotNull("Session timer future should not be null", thirdTimer);
108+
109+
// Verify it's still the same timer
110+
assertSame("Session timer should be reused, not recreated", firstTimer, thirdTimer);
111+
assertFalse("First timer should still not be cancelled", firstTimer.isCancelled());
112+
113+
// Verify that all three sessions were created
114+
assertEquals("Three sessions should be created", 3, initiator.getManagedSessions().size());
115+
116+
// Stop the initiator and verify the timer is cancelled
117+
initiator.stop(true);
118+
assertTrue("Timer should be cancelled after stop", firstTimer.isCancelled());
119+
}
120+
121+
/**
122+
* Test implementation of AbstractSocketInitiator for testing purposes.
123+
*/
124+
private static class TestAbstractSocketInitiator extends AbstractSocketInitiator {
125+
126+
public TestAbstractSocketInitiator(SessionSettings settings, SessionFactory sessionFactory) throws ConfigError {
127+
super(settings, sessionFactory);
128+
}
129+
130+
@Override
131+
public void start() throws ConfigError, RuntimeError {
132+
// No-op for testing
133+
}
134+
135+
@Override
136+
public void stop() {
137+
clearConnectorSessions();
138+
stopInitiators();
139+
}
140+
141+
@Override
142+
public void stop(boolean force) {
143+
clearConnectorSessions();
144+
stopInitiators();
145+
}
146+
147+
@Override
148+
protected EventHandlingStrategy getEventHandlingStrategy() {
149+
return null;
150+
}
151+
152+
/**
153+
* Expose the session timer running status for testing using reflection.
154+
*/
155+
public boolean isSessionTimerRunning() throws Exception {
156+
Field field = SessionConnector.class.getDeclaredField("sessionTimerFuture");
157+
field.setAccessible(true);
158+
ScheduledFuture<?> future = (ScheduledFuture<?>) field.get(this);
159+
return future != null && !future.isDone();
160+
}
161+
162+
/**
163+
* Expose the session timer future for testing using reflection.
164+
*/
165+
public ScheduledFuture<?> getSessionTimerFuture() throws Exception {
166+
Field field = SessionConnector.class.getDeclaredField("sessionTimerFuture");
167+
field.setAccessible(true);
168+
return (ScheduledFuture<?>) field.get(this);
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)