Skip to content

Commit f1901dc

Browse files
authored
[io.net] Fix shutdown hang: keep the shared thread pool alive until both clients are stopped (#5772)
Signed-off-by: Martin Littkovsky <2018turtle@proton.me>
1 parent c5954e9 commit f1901dc

2 files changed

Lines changed: 64 additions & 10 deletions

File tree

bundles/org.openhab.core.io.net/src/main/java/org/openhab/core/io/net/http/internal/WebClientFactoryImpl.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ protected void deactivate() {
136136
commonWebSocketClient = null;
137137
logger.debug("Jetty shared web socket client stopped");
138138
}
139+
if (threadPool != null) {
140+
try {
141+
threadPool.stop();
142+
} catch (Exception e) {
143+
logger.error("error while stopping shared Jetty thread pool", e);
144+
}
145+
}
139146
threadPool = null;
140147
}
141148

@@ -227,15 +234,26 @@ private synchronized void initialize() {
227234
try {
228235
if (threadPool == null) {
229236
threadPool = createThreadPool("common", minThreadsShared, maxThreadsShared, keepAliveTimeoutShared);
237+
// The pool is shared between the http client and the web socket client. Start it
238+
// ourselves so both Jetty containers treat it as an unmanaged bean and neither
239+
// stops it while the other client is still using it (see the shared-bean rule in
240+
// Jetty's ContainerLifeCycle javadoc). It is stopped explicitly in deactivate().
241+
try {
242+
threadPool.start();
243+
// Set the stop timeout right after starting the pool we now own. We need the
244+
// stop timeout in order to prevent blocking the deactivation of this
245+
// component, see https://github.com/eclipse/smarthome/issues/6632
246+
threadPool.setStopTimeout(0);
247+
} catch (Exception e) {
248+
// roll back so a later initialize() retry recreates the pool
249+
// instead of reusing a dead one
250+
threadPool = null;
251+
throw e;
252+
}
230253
}
231254

232255
if (commonHttpClient == null) {
233256
commonHttpClient = createHttpClientInternal("common", null, true, threadPool);
234-
// we need to set the stop timeout AFTER the client has been started, because
235-
// otherwise the Jetty client sets it back to the default value.
236-
// We need the stop timeout in order to prevent blocking the deactivation of this
237-
// component, see https://github.com/eclipse/smarthome/issues/6632
238-
threadPool.setStopTimeout(0);
239257
logger.debug("Jetty shared http client created");
240258
}
241259

bundles/org.openhab.core.io.net/src/test/java/org/openhab/core/io/net/http/internal/WebClientFactoryImplTest.java

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Map;
2727
import java.util.concurrent.ArrayBlockingQueue;
2828
import java.util.concurrent.ExecutionException;
29+
import java.util.concurrent.Executor;
2930
import java.util.concurrent.ThreadPoolExecutor;
3031
import java.util.concurrent.TimeUnit;
3132
import java.util.concurrent.TimeoutException;
@@ -36,6 +37,7 @@
3637
import org.eclipse.jdt.annotation.NonNullByDefault;
3738
import org.eclipse.jetty.client.HttpClient;
3839
import org.eclipse.jetty.client.api.ContentResponse;
40+
import org.eclipse.jetty.util.thread.QueuedThreadPool;
3941
import org.eclipse.jetty.websocket.client.WebSocketClient;
4042
import org.junit.jupiter.api.AfterEach;
4143
import org.junit.jupiter.api.BeforeEach;
@@ -68,12 +70,26 @@ public void setup() {
6870

6971
@AfterEach
7072
public void tearDown() throws InterruptedException {
71-
// Sometimes a java.nio.channels.ClosedSelectorException occurs when the commonWebSocketClient
72-
// is stopped while its threads are still starting. This would cause webClientFactory.deactivate()
73-
// to block forever so continue if it has not completed after 2 seconds.
74-
Thread deactivateThread = new Thread(() -> webClientFactory.deactivate());
73+
// Regression check for the former shutdown hang: stopping the common HTTP client used to
74+
// tear down the shared thread pool underneath the common WebSocketClient, whose selector
75+
// then died with a ClosedSelectorException and deactivate() blocked forever.
76+
// Deactivation must always complete.
77+
deactivateWithTimeout();
78+
}
79+
80+
private void deactivateWithTimeout() throws InterruptedException {
81+
Thread deactivateThread = new Thread(() -> webClientFactory.deactivate(), "webClientFactory-deactivate");
82+
deactivateThread.setDaemon(true);
7583
deactivateThread.start();
76-
deactivateThread.join(2000);
84+
deactivateThread.join(10_000);
85+
86+
// Capture the verdict before interrupting: the interrupt is cleanup, so an interrupt that
87+
// happens to unblock the deactivation must not turn an observed timeout into a pass.
88+
boolean timedOut = deactivateThread.isAlive();
89+
if (timedOut) {
90+
deactivateThread.interrupt();
91+
}
92+
assertThat("deactivate() did not complete", timedOut, is(false));
7793
}
7894

7995
@Test
@@ -85,6 +101,26 @@ public void testGetClients() throws Exception {
85101
assertThat(webSocketClient, is(notNullValue()));
86102
}
87103

104+
@Test
105+
public void testSharedThreadPoolIsUnmanagedAndStoppedByDeactivate() throws Exception {
106+
HttpClient httpClient = webClientFactory.getCommonHttpClient();
107+
WebSocketClient webSocketClient = webClientFactory.getCommonWebSocketClient();
108+
109+
Executor executor = httpClient.getExecutor();
110+
assertThat(executor, is(instanceOf(QueuedThreadPool.class)));
111+
QueuedThreadPool sharedPool = (QueuedThreadPool) executor;
112+
113+
// both clients use the same pool, but neither container manages its lifecycle
114+
assertThat(webSocketClient.getHttpClient().getExecutor(), is(sameInstance(executor)));
115+
assertThat(httpClient.isManaged(sharedPool), is(false));
116+
assertThat(webSocketClient.getHttpClient().isManaged(sharedPool), is(false));
117+
assertThat(sharedPool.isRunning(), is(true));
118+
119+
// deactivation stops the pool itself, after both clients
120+
deactivateWithTimeout();
121+
assertThat(sharedPool.isRunning(), is(false));
122+
}
123+
88124
@Disabled("connecting to the outside world makes this test flaky")
89125
@Test
90126
public void testCommonClientUsesExtensibleTrustManager() throws Exception {

0 commit comments

Comments
 (0)