2626import java .util .Map ;
2727import java .util .concurrent .ArrayBlockingQueue ;
2828import java .util .concurrent .ExecutionException ;
29+ import java .util .concurrent .Executor ;
2930import java .util .concurrent .ThreadPoolExecutor ;
3031import java .util .concurrent .TimeUnit ;
3132import java .util .concurrent .TimeoutException ;
3637import org .eclipse .jdt .annotation .NonNullByDefault ;
3738import org .eclipse .jetty .client .HttpClient ;
3839import org .eclipse .jetty .client .api .ContentResponse ;
40+ import org .eclipse .jetty .util .thread .QueuedThreadPool ;
3941import org .eclipse .jetty .websocket .client .WebSocketClient ;
4042import org .junit .jupiter .api .AfterEach ;
4143import 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