2121import static org .mockito .Mockito .verify ;
2222
2323import java .io .File ;
24+ import java .io .IOException ;
2425import java .io .InputStream ;
2526import java .nio .charset .StandardCharsets ;
2627import java .nio .file .Files ;
2728import java .nio .file .Path ;
29+ import java .util .ArrayList ;
2830import java .util .HashSet ;
31+ import java .util .List ;
2932import java .util .Optional ;
3033import java .util .Set ;
3134import java .util .concurrent .ExecutorService ;
4043import org .junit .jupiter .api .Test ;
4144import org .junit .jupiter .api .Timeout ;
4245import org .junit .jupiter .api .io .TempDir ;
46+ import org .junit .jupiter .api .parallel .Execution ;
47+ import org .junit .jupiter .api .parallel .ExecutionMode ;
4348
4449import org .apache .http .HttpEntity ;
4550
5863import io .micrometer .tracing .propagation .Propagator ;
5964import io .micrometer .tracing .test .simple .SimpleTracer ;
6065
66+ // The methods below start a WireMock server each and assert on wall-clock deadlines. Running
67+ // them concurrently (junit-platform.properties sets parallel.mode.default=concurrent) puts as
68+ // many Jetty startups as there are cores on top of each other, which on a loaded CI runner
69+ // delays the first response past the socket timeout and fails the test with a spurious
70+ // "Failed to communicate with Extender service".
71+ @ Execution (ExecutionMode .SAME_THREAD )
6172public class RemoteEngineBuilderTest {
6273
6374 private static final long BUILD_SLEEP_TIMEOUT = 100 ;
6475 private static final long BUILD_RESULT_WAIT_TIMEOUT = 10_000 ;
65- private static final int SOCKET_TIMEOUT = 2_000 ;
76+ // Generous enough that a busy CI runner is never mistaken for a network fault: a
77+ // SocketTimeoutException is not retried by the client and marks the build as failed.
78+ private static final int SOCKET_TIMEOUT = 30_000 ;
79+ // Only for the test that has to give up on a deliberately stalled response.
80+ private static final int SHORT_SOCKET_TIMEOUT = 2_000 ;
6681
6782 @ TempDir
6883 Path tmpDir ;
@@ -71,6 +86,7 @@ public class RemoteEngineBuilderTest {
7186 private ExecutorService executor ;
7287 private Path resultLocation ;
7388 private SimpleMeterRegistry meterRegistry ;
89+ private final List <RemoteEngineBuilder > builders = new ArrayList <>();
7490
7591 @ BeforeEach
7692 public void setUp () throws Exception {
@@ -88,6 +104,16 @@ public void setUp() throws Exception {
88104 @ AfterEach
89105 public void tearDown () {
90106 executor .shutdownNow ();
107+ // Each client owns connection eviction threads, which would otherwise leak for the
108+ // remainder of the suite. Closing is cleanup only, so it never fails a test.
109+ for (RemoteEngineBuilder builder : builders ) {
110+ try {
111+ builder .httpClient .close ();
112+ } catch (IOException e ) {
113+ // ignore
114+ }
115+ }
116+ builders .clear ();
91117 builderMock .stop ();
92118 }
93119
@@ -101,20 +127,28 @@ private RemoteEngineBuilder createBuilder(int resultDownloadRetries, long buildR
101127
102128 private RemoteEngineBuilder createBuilder (Optional <GCPInstanceService > instanceService ,
103129 int resultDownloadRetries , long buildResultWaitTimeout ) {
104- return new RemoteEngineBuilder (
130+ return createBuilder (instanceService , resultDownloadRetries , buildResultWaitTimeout , SOCKET_TIMEOUT );
131+ }
132+
133+ private RemoteEngineBuilder createBuilder (Optional <GCPInstanceService > instanceService ,
134+ int resultDownloadRetries , long buildResultWaitTimeout ,
135+ int socketTimeout ) {
136+ RemoteEngineBuilder builder = new RemoteEngineBuilder (
105137 instanceService ,
106138 resultLocation .toString (),
107139 BUILD_SLEEP_TIMEOUT ,
108140 buildResultWaitTimeout ,
109- SOCKET_TIMEOUT ,
110- SOCKET_TIMEOUT ,
111- SOCKET_TIMEOUT ,
141+ socketTimeout ,
142+ socketTimeout ,
143+ socketTimeout ,
112144 35 ,
113145 resultDownloadRetries ,
114146 100 ,
115147 meterRegistry ,
116148 new SimpleTracer (),
117149 Propagator .NOOP );
150+ builders .add (builder );
151+ return builder ;
118152 }
119153
120154 private double reconnectCount (String operation ) {
@@ -170,7 +204,10 @@ public void stalledResultDownloadMustNotBlockOtherBuilds() throws Exception {
170204 builderMock .stubFor (get (urlPathEqualTo ("/job_result" ))
171205 .willReturn (aResponse ().withStatus (200 ).withFixedDelay (30_000 ).withBody (new byte [] {0x42 })));
172206
173- RemoteEngineBuilder builder = createBuilder (1 );
207+ // The short socket timeout is what lets the stuck downloads give up on the 30s delayed
208+ // response inside this test's deadline.
209+ RemoteEngineBuilder builder = createBuilder (Optional .empty (), 1 , BUILD_RESULT_WAIT_TIMEOUT ,
210+ SHORT_SOCKET_TIMEOUT );
174211
175212 // Occupy the connection pool with builds stuck downloading their result
176213 Path stalledResult1 = submitBuild (builder , "stalled1" );
0 commit comments