55 * you may not use this file except in compliance with the License.
66 * You may obtain a copy of the License at
77 *
8- * http://www.apache.org/licenses/LICENSE-2.0
8+ * http://www.apache.org/licenses/LICENSE-2.0
99 *
1010 * Unless required by applicable law or agreed to in writing, software
1111 * distributed under the License is distributed on an "AS IS" BASIS,
@@ -115,7 +115,7 @@ private TestBench(
115115 String dockerImageName ,
116116 String dockerImageTag ,
117117 String containerName ) {
118- this .ignorePullError = true ;
118+ this .ignorePullError = ignorePullError ;
119119 this .baseUri = baseUri ;
120120 this .gRPCBaseUri = gRPCBaseUri ;
121121 this .dockerImageName = dockerImageName ;
@@ -133,6 +133,10 @@ private TestBench(
133133 .getHeaders ()
134134 .setUserAgent (
135135 String .format (Locale .US , "%s/ test-bench/" , this .containerName ));
136+
137+ // ADDED: Prevent client-side infinite hangs if the server is unresponsive
138+ request .setConnectTimeout (15000 );
139+ request .setReadTimeout (15000 );
136140 });
137141 }
138142
@@ -210,19 +214,27 @@ public void start() {
210214 // expected when the server isn't running already
211215 }
212216 try {
213- tempDirectory = Files .createTempDirectory (containerName );
214- outPath = tempDirectory .resolve ("stdout" );
215- errPath = tempDirectory .resolve ("stderr" );
217+ // ADDED: Route logs to Bazel/Sponge artifact directories so Test Fusion can see them
218+ String bazelOutputDir = System .getenv ("TEST_UNDECLARED_OUTPUTS_DIR" );
219+ Path baseArtifactDir ;
220+ if (bazelOutputDir != null && !bazelOutputDir .isEmpty ()) {
221+ baseArtifactDir = java .nio .file .Paths .get (bazelOutputDir );
222+ } else {
223+ baseArtifactDir = java .nio .file .Paths .get ("target" , "testbench-logs" );
224+ }
225+
226+ tempDirectory = baseArtifactDir .resolve (containerName );
227+ Files .createDirectories (tempDirectory );
228+ outPath = tempDirectory .resolve ("gunicorn-stdout.log" );
229+ errPath = tempDirectory .resolve ("gunicorn-stderr.log" );
216230
217231 File outFile = outPath .toFile ();
218232 File errFile = errPath .toFile ();
219- LOGGER .info ("Redirecting server stdout to: {}" , outFile .getAbsolutePath ());
220- LOGGER .info ("Redirecting server stderr to: {}" , errFile .getAbsolutePath ());
233+ LOGGER .info ("Redirecting server stdout to artifact: {}" , outFile .getAbsolutePath ());
234+ LOGGER .info ("Redirecting server stderr to artifact: {}" , errFile .getAbsolutePath ());
235+
221236 String dockerImage = String .format (Locale .US , "%s:%s" , dockerImageName , dockerImageTag );
222- // First try and pull the docker image, this validates docker is available and running
223- // on the host, as well as gives time for the image to be downloaded independently of
224- // trying to start the container. (Below, when we first start the container we then attempt
225- // to issue a call against the api before we yield to run our tests.)
237+ // First try and pull the docker image
226238 try {
227239 Process p =
228240 new ProcessBuilder ()
@@ -248,6 +260,8 @@ public void start() {
248260
249261 int port = URI .create (baseUri ).getPort ();
250262 int gRPCPort = URI .create (gRPCBaseUri ).getPort ();
263+
264+ // ADDED: gthread, 40 threads, and debug logging
251265 final List <String > command =
252266 ImmutableList .of (
253267 "docker" ,
@@ -263,20 +277,34 @@ public void start() {
263277 "gunicorn" ,
264278 "--bind=0.0.0.0:9000" ,
265279 "--worker-class=gthread" ,
266- "--threads=10 " ,
280+ "--threads=40 " ,
267281 "--access-logfile=-" ,
282+ "--error-logfile=-" ,
283+ "--log-level=debug" ,
268284 "--keep-alive=0" ,
269285 "testbench:run()" );
286+
270287 process =
271288 new ProcessBuilder ()
272289 .command (command )
273290 .redirectOutput (outFile )
274291 .redirectError (errFile )
275292 .start ();
276293 LOGGER .info (command .toString ());
294+
277295 try {
278296 // wait a small amount of time for the server to come up before probing
279297 Thread .sleep (500 );
298+
299+ // ADDED: Fail fast if container crashed due to port collision
300+ if (!process .isAlive ()) {
301+ dumpServerLogs (outPath , errPath );
302+ throw new IllegalStateException (
303+ "TestBench Docker container died immediately. Exit code: "
304+ + process .exitValue ()
305+ + ". Probable port collision." );
306+ }
307+
280308 // wait for the server to come up
281309 List <RetryTestResource > existingResources =
282310 runWithRetries (
@@ -370,13 +398,16 @@ public boolean shouldRetry(Throwable previousThrowable, List<?> previousResponse
370398 }
371399 },
372400 NanoClock .getDefaultClock ());
373- try {
374- Files .delete (errPath );
375- Files .delete (outPath );
376- Files .delete (tempDirectory );
377- } catch (IOException e ) {
378- throw new RuntimeException (e );
379- }
401+
402+ // ADDED: Commented out file deletion so Test Fusion can preserve artifacts
403+ // try {
404+ // Files.delete(errPath);
405+ // Files.delete(outPath);
406+ // Files.delete(tempDirectory);
407+ // } catch (IOException e) {
408+ // throw new RuntimeException(e);
409+ // }
410+ LOGGER .info ("Skipping artifact deletion to preserve logs for Test Fusion." );
380411 } catch (InterruptedException | IOException e ) {
381412 throw new RuntimeException (e );
382413 }
@@ -393,6 +424,7 @@ private void dumpServerLogs(Path outFile, Path errFile) throws IOException {
393424 }
394425
395426 private void dumpServerLog (String prefix , File out ) throws IOException {
427+ if (!out .exists ()) return ;
396428 try (BufferedReader reader = new BufferedReader (new FileReader (out ))) {
397429 String line ;
398430 while ((line = reader .readLine ()) != null ) {
@@ -401,9 +433,11 @@ private void dumpServerLog(String prefix, File out) throws IOException {
401433 }
402434 }
403435
436+ // ADDED: Fixed findFreePort to properly apply setReuseAddress
404437 private static int findFreePort () {
405- try (java .net .ServerSocket socket = new java .net .ServerSocket (0 )) {
438+ try (java .net .ServerSocket socket = new java .net .ServerSocket ()) {
406439 socket .setReuseAddress (true );
440+ socket .bind (new java .net .InetSocketAddress (0 ));
407441 return socket .getLocalPort ();
408442 } catch (java .io .IOException e ) {
409443 throw new RuntimeException ("Failed to find a free port" , e );
@@ -503,17 +537,15 @@ static final class Builder {
503537 private String dockerImageTag ;
504538 private String containerName ;
505539
540+ // ADDED: Refactored constructor to prevent uninitialized variables
506541 private Builder () {
507- int httpPort = findFreePort ();
508- int grpcPort = findFreePort ();
509- String uuid = java .util .UUID .randomUUID ().toString ().substring (0 , 8 );
510-
511- this .ignorePullError = false ;
512- this .baseUri = "http://127.0.0.1:" + httpPort ;
513- this .gRPCBaseUri = "http://127.0.0.1:" + grpcPort ;
514- this .dockerImageName = DEFAULT_IMAGE_NAME ;
515- this .dockerImageTag = DEFAULT_IMAGE_TAG ;
516- this .containerName = DEFAULT_CONTAINER_NAME + "_" + uuid ;
542+ this (
543+ false ,
544+ "http://127.0.0.1:" + findFreePort (),
545+ "http://127.0.0.1:" + findFreePort (),
546+ DEFAULT_IMAGE_NAME ,
547+ DEFAULT_IMAGE_TAG ,
548+ DEFAULT_CONTAINER_NAME + "_" + java .util .UUID .randomUUID ().toString ().substring (0 , 8 ));
517549 }
518550
519551 private Builder (
@@ -529,6 +561,13 @@ private Builder(
529561 this .dockerImageName = dockerImageName ;
530562 this .dockerImageTag = dockerImageTag ;
531563 this .containerName = containerName ;
564+
565+ // ADDED: Trace logging for port assignments to verify collisions in CI
566+ LOGGER .info (
567+ "DEBUG-BUILDER: Initialized testbench config -> Container: {}, HTTP: {}, GRPC: {}" ,
568+ containerName ,
569+ baseUri ,
570+ gRPCBaseUri );
532571 }
533572
534573 public Builder setIgnorePullError (boolean ignorePullError ) {
0 commit comments