Skip to content

Commit 9625b0e

Browse files
authored
Merge pull request #1372 from myronkscott/test_help
Make Inline servers behave during shutdown
2 parents 3f93509 + 9bfb624 commit 9625b0e

File tree

19 files changed

+529
-57
lines changed

19 files changed

+529
-57
lines changed

common/src/main/java/com/tc/lang/TCThreadGroup.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public TCThreadGroup(ThrowableHandler throwableHandler, String name) {
4646
}
4747

4848
public TCThreadGroup(ThrowableHandler throwableHandler, String name, boolean stoppable) {
49-
this(throwableHandler, name, stoppable, !stoppable);
49+
this(throwableHandler, name, stoppable, false);
5050
}
5151

5252
public TCThreadGroup(ThrowableHandler throwableHandler, String name, boolean stoppable, boolean ignorePool) {
@@ -76,6 +76,7 @@ public boolean isStoppable() {
7676
public void printLiveThreads(Consumer<String> reporter) {
7777
for (Thread t : threads()) {
7878
if (t != null && t != Thread.currentThread()) {
79+
reporter.accept(t.getThreadGroup().getName() + " - " + t.getName());
7980
reporter.accept(ThreadDumpUtil.getThreadDump(t));
8081
}
8182
}
@@ -121,7 +122,7 @@ private boolean lookForThreadExit(Thread t, Consumer<InterruptedException> inter
121122
}
122123
}
123124

124-
private synchronized List<Thread> threads() {
125+
private List<Thread> threads() {
125126
int ac = activeCount();
126127
Thread[] list = new Thread[ac];
127128
enumerate(list, true);

common/src/main/java/com/tc/net/protocol/tcm/CommunicationsManagerImpl.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
import com.tc.net.protocol.transport.WireProtocolMessageSink;
5555
import com.tc.net.core.ProductID;
5656
import com.tc.util.Assert;
57-
import com.tc.util.TCTimeoutException;
5857
import com.tc.util.concurrent.SetOnceFlag;
5958

6059
import java.io.IOException;
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
/*
2+
* Copyright Terracotta, Inc.
3+
* Copyright IBM Corp. 2024, 2025
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
package com.tc.lang;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.concurrent.CountDownLatch;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.concurrent.atomic.AtomicBoolean;
25+
import java.util.concurrent.atomic.AtomicInteger;
26+
import java.util.concurrent.locks.LockSupport;
27+
28+
import org.junit.After;
29+
import static org.junit.Assert.assertEquals;
30+
import static org.junit.Assert.assertFalse;
31+
import static org.junit.Assert.assertTrue;
32+
import static org.junit.Assert.fail;
33+
import org.junit.Before;
34+
import org.junit.Test;
35+
import org.slf4j.LoggerFactory;
36+
37+
public class TCThreadGroupTest {
38+
39+
private ThrowableHandler throwableHandler;
40+
private List<Thread> createdThreads;
41+
private AtomicInteger interruptCount;
42+
43+
@Before
44+
public void setUp() {
45+
throwableHandler = new ThrowableHandlerImpl(LoggerFactory.getLogger(TCThreadGroupTest.class)) {
46+
@Override
47+
protected synchronized void exit(int status) {
48+
// do not exit in test
49+
}
50+
};
51+
createdThreads = new ArrayList<>();
52+
interruptCount = new AtomicInteger(0);
53+
}
54+
55+
@After
56+
public void tearDown() {
57+
// Ensure all threads are interrupted to clean up
58+
for (Thread t : createdThreads) {
59+
t.interrupt();
60+
}
61+
62+
// Wait for threads to terminate
63+
for (Thread t : createdThreads) {
64+
try {
65+
t.join(1000);
66+
} catch (InterruptedException e) {
67+
// Ignore
68+
}
69+
}
70+
}
71+
72+
/**
73+
* Test that a stoppable thread group can retire threads normally.
74+
*/
75+
@Test
76+
public void testRetireNormalThreads() throws Exception {
77+
TCThreadGroup group = new TCThreadGroup(throwableHandler, "test-normal", true);
78+
79+
// Create some worker threads that will exit quickly when interrupted
80+
int threadCount = 5;
81+
CountDownLatch threadsStarted = new CountDownLatch(threadCount);
82+
83+
for (int i = 0; i < threadCount; i++) {
84+
Thread t = new Thread(group, () -> {
85+
threadsStarted.countDown();
86+
try {
87+
Thread.sleep(Long.MAX_VALUE);
88+
} catch (InterruptedException e) {
89+
// Expected - exit on interrupt
90+
interruptCount.incrementAndGet();
91+
}
92+
}, "worker-" + i);
93+
createdThreads.add(t);
94+
t.start();
95+
}
96+
97+
// Wait for all threads to start
98+
assertTrue("Threads did not start in time", threadsStarted.await(5, TimeUnit.SECONDS));
99+
100+
// Retire the threads
101+
group.interrupt();
102+
boolean retired = group.retire(2000, e -> {
103+
// Just log the interruption
104+
System.out.println("Interrupted: " + e.getMessage());
105+
});
106+
107+
assertTrue("Failed to retire threads", retired);
108+
assertEquals("Not all threads were interrupted", threadCount, interruptCount.get());
109+
}
110+
111+
/**
112+
* Test that a non-stoppable thread group returns true immediately from retire().
113+
*/
114+
@Test
115+
public void testRetireNonStoppableThreadGroup() throws Exception {
116+
TCThreadGroup group = new TCThreadGroup(throwableHandler, "test-non-stoppable", false);
117+
118+
// Create a worker thread that would normally block
119+
Thread t = new Thread(group, () -> {
120+
try {
121+
Thread.sleep(Long.MAX_VALUE);
122+
} catch (InterruptedException e) {
123+
interruptCount.incrementAndGet();
124+
}
125+
}, "non-stoppable-worker");
126+
createdThreads.add(t);
127+
t.start();
128+
129+
// Give thread time to start
130+
Thread.sleep(100);
131+
132+
// Retire should return true immediately for non-stoppable groups
133+
long startTime = System.currentTimeMillis();
134+
boolean retired = group.retire(5000, e -> {
135+
fail("Should not be interrupted for non-stoppable group");
136+
});
137+
long duration = System.currentTimeMillis() - startTime;
138+
139+
assertTrue("Non-stoppable group should return true from retire()", retired);
140+
assertTrue("Retire should return quickly for non-stoppable groups", duration < 1000);
141+
assertEquals("Thread should not be interrupted", 0, interruptCount.get());
142+
}
143+
144+
/**
145+
* Test behavior with threads that ignore interrupts.
146+
*/
147+
@Test
148+
public void testRetireWithUninterruptibleThreads() throws Exception {
149+
TCThreadGroup group = new TCThreadGroup(throwableHandler, "test-uninterruptible", true);
150+
151+
// Create a thread that ignores interrupts
152+
AtomicBoolean threadShouldExit = new AtomicBoolean(false);
153+
CountDownLatch threadStarted = new CountDownLatch(1);
154+
155+
Thread t = new Thread(group, () -> {
156+
threadStarted.countDown();
157+
while (!threadShouldExit.get()) {
158+
// Ignore interrupts and keep running
159+
if (Thread.interrupted()) {
160+
interruptCount.incrementAndGet();
161+
}
162+
// Busy wait
163+
LockSupport.parkNanos(1000000); // 1ms
164+
}
165+
}, "uninterruptible-worker");
166+
createdThreads.add(t);
167+
t.start();
168+
169+
// Wait for thread to start
170+
assertTrue("Thread did not start in time", threadStarted.await(5, TimeUnit.SECONDS));
171+
172+
// Try to retire - should fail because thread ignores interrupts
173+
group.interrupt();
174+
boolean retired = group.retire(1000, e -> {
175+
System.out.println("Interrupted: " + e.getMessage());
176+
});
177+
178+
// Verify retire failed
179+
assertFalse("Retire should fail with uninterruptible threads", retired);
180+
assertTrue("Thread should have been interrupted at least once", interruptCount.get() > 0);
181+
182+
// Allow thread to exit and try again
183+
threadShouldExit.set(true);
184+
Thread.sleep(100);
185+
186+
// Now retire should succeed
187+
retired = group.retire(1000, e -> {
188+
System.out.println("Interrupted: " + e.getMessage());
189+
});
190+
191+
assertTrue("Retire should succeed after thread exits", retired);
192+
}
193+
194+
/**
195+
* Test behavior with threads that are blocked on I/O or other uninterruptible operations.
196+
* This simulates threads that might be stuck during server shutdown.
197+
*/
198+
@Test
199+
public void testRetireWithBlockedThreads() throws Exception {
200+
TCThreadGroup group = new TCThreadGroup(throwableHandler, "test-blocked", true);
201+
202+
// Create a thread that simulates being blocked on something that can't be interrupted
203+
// In a real scenario, this might be a thread blocked on native I/O
204+
CountDownLatch threadStarted = new CountDownLatch(1);
205+
AtomicBoolean threadIsBlocked = new AtomicBoolean(false);
206+
AtomicBoolean threadShouldUnblock = new AtomicBoolean(false);
207+
208+
Thread t = new Thread(group, () -> {
209+
threadStarted.countDown();
210+
211+
// Simulate entering a blocked state
212+
threadIsBlocked.set(true);
213+
214+
// Wait until explicitly unblocked
215+
while (!threadShouldUnblock.get()) {
216+
// Check for interruption but don't exit
217+
if (Thread.interrupted()) {
218+
interruptCount.incrementAndGet();
219+
}
220+
LockParker.parkNanos(1000000); // 1ms
221+
}
222+
}, "blocked-worker");
223+
createdThreads.add(t);
224+
t.start();
225+
226+
// Wait for thread to start and become blocked
227+
assertTrue("Thread did not start in time", threadStarted.await(5, TimeUnit.SECONDS));
228+
while (!threadIsBlocked.get()) {
229+
Thread.sleep(10);
230+
}
231+
232+
// Try to retire - should fail because thread is "blocked"
233+
group.interrupt();
234+
boolean retired = group.retire(1000, e -> {
235+
System.out.println("Interrupted: " + e.getMessage());
236+
});
237+
238+
// Verify retire failed
239+
assertFalse("Retire should fail with blocked threads", retired);
240+
assertTrue("Thread should have been interrupted at least once", interruptCount.get() > 0);
241+
242+
// Unblock the thread and try again
243+
threadShouldUnblock.set(true);
244+
Thread.sleep(100);
245+
246+
// Now retire should succeed
247+
retired = group.retire(1000, e -> {
248+
System.out.println("Interrupted: " + e.getMessage());
249+
});
250+
251+
assertTrue("Retire should succeed after thread unblocks", retired);
252+
}
253+
254+
/**
255+
* Test that ignorePoolThreads parameter works correctly.
256+
*/
257+
@Test
258+
public void testIgnorePoolThreads() throws Exception {
259+
// Create a thread group that ignores pool threads
260+
TCThreadGroup group = new TCThreadGroup(throwableHandler, "test-ignore-pool", true, true);
261+
262+
// Create a thread with a pool-like name
263+
CountDownLatch threadStarted = new CountDownLatch(1);
264+
Thread t = new Thread(group, () -> {
265+
threadStarted.countDown();
266+
try {
267+
Thread.sleep(Long.MAX_VALUE);
268+
} catch (InterruptedException e) {
269+
interruptCount.incrementAndGet();
270+
}
271+
}, "pool-1-thread-1");
272+
createdThreads.add(t);
273+
t.start();
274+
275+
// Wait for thread to start
276+
assertTrue("Thread did not start in time", threadStarted.await(5, TimeUnit.SECONDS));
277+
278+
// Retire should succeed immediately because pool threads are ignored
279+
boolean retired = group.retire(1000, e -> {
280+
fail("Should not be interrupted when ignoring pool threads");
281+
});
282+
283+
assertTrue("Retire should succeed when ignoring pool threads", retired);
284+
assertEquals("Pool thread should not be interrupted", 0, interruptCount.get());
285+
286+
// Create a non-pool thread in the same group
287+
CountDownLatch thread2Started = new CountDownLatch(1);
288+
Thread t2 = new Thread(group, () -> {
289+
thread2Started.countDown();
290+
try {
291+
Thread.sleep(Long.MAX_VALUE);
292+
} catch (InterruptedException e) {
293+
interruptCount.incrementAndGet();
294+
}
295+
}, "regular-thread");
296+
createdThreads.add(t2);
297+
t2.start();
298+
299+
// Wait for thread to start
300+
assertTrue("Thread did not start in time", thread2Started.await(5, TimeUnit.SECONDS));
301+
302+
// Now retire should interrupt the regular thread but not the pool thread
303+
group.interrupt();
304+
retired = group.retire(1000, e -> {
305+
System.out.println("Interrupted: " + e.getMessage());
306+
});
307+
308+
assertTrue("Retire should succeed", retired);
309+
assertEquals("Only the regular thread should be interrupted", 2, interruptCount.get());
310+
}
311+
312+
/**
313+
* Helper class to simulate LockSupport without actually using it
314+
* (to avoid potential test interference)
315+
*/
316+
private static class LockParker {
317+
public static void parkNanos(long nanos) {
318+
try {
319+
Thread.sleep(0, (int)(nanos / 1000));
320+
} catch (InterruptedException e) {
321+
Thread.currentThread().interrupt();
322+
}
323+
}
324+
}
325+
}
326+
327+
// Made with Bob

galvan-support/src/main/java/org/terracotta/testing/rules/BasicExternalCluster.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ private void internalStart(CompletableFuture<Void> checker) throws Throwable {
247247
ServerInstance serverProcess = !inline ?
248248
new ServerProcess(serverName, server, serverWorkingDir, serverHeapSize, debugPort, systemProperties, parentOutput, builder.build())
249249
:
250-
new InlineServer(serverName, server, serverWorkingDir, systemProperties, parentOutput, builder.build());
250+
new InlineServer(serverName, server, serverWorkingDir, serverHeapSize, debugPort, systemProperties, parentOutput, builder.build());
251251

252252
stripeInstaller.installNewServer(serverProcess);
253253
}

galvan-support/src/test/java/org/terracotta/testing/rules/SimpleActivePassiveWithClassRuleIT.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
import org.junit.ClassRule;
2222
import org.junit.Test;
23-
import org.terracotta.passthrough.IClusterControl;
2423

2524

2625
/**

galvan/src/main/java/org/terracotta/testing/master/IGalvanServer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ public interface IGalvanServer {
3939
void waitForTermination();
4040

4141
void waitForState(ServerMode mode) throws InterruptedException;
42+
43+
IGalvanServer newInstance();
4244
}

0 commit comments

Comments
 (0)