6565import java .nio .file .Files ;
6666import java .nio .file .Path ;
6767import java .nio .file .Paths ;
68+ import java .time .Duration ;
69+ import java .util .ArrayList ;
6870import java .util .Arrays ;
71+ import java .util .Collections ;
6972import java .util .List ;
7073import java .util .concurrent .BlockingQueue ;
7174import java .util .concurrent .Executors ;
8790import org .junit .rules .TemporaryFolder ;
8891
8992public abstract class JavaRuntimeViaHttpBase {
93+ private static final GoogleLogger logger = GoogleLogger .forEnclosingClass ();
9094 @ ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder ();
9195 private static final String RUNTIME_LOCATION_ROOT = "java/com/google/apphosting" ;
96+ private static final int MAX_RETRIES = 3 ;
9297 static final int RESPONSE_200 = 200 ;
9398
9499 @ FunctionalInterface
@@ -239,9 +244,8 @@ public boolean isJakarta() {
239244
240245 public <ApiServerT extends Closeable > RuntimeContext <ApiServerT > createRuntimeContext (
241246 RuntimeContext .Config <ApiServerT > config ) throws IOException , InterruptedException {
247+ assertThat (MAX_RETRIES ).isGreaterThan (0 );
242248 PortPicker portPicker = PortPicker .create ();
243- int jettyPort = portPicker .pickUnusedPort ();
244- int apiPort = portPicker .pickUnusedPort ();
245249 String runtimeDirProperty = System .getProperty ("appengine.runtime.dir" );
246250 File runtimeDir =
247251 (runtimeDirProperty == null )
@@ -250,62 +254,129 @@ public <ApiServerT extends Closeable> RuntimeContext<ApiServerT> createRuntimeCo
250254 assertWithMessage ("Runtime directory %s should exist and be a directory" , runtimeDir )
251255 .that (runtimeDir .isDirectory ())
252256 .isTrue ();
253- InetSocketAddress apiSocketAddress = new InetSocketAddress ( InetAddress . getLoopbackAddress (), apiPort );
254- ImmutableList .Builder <String > builder = ImmutableList .< String > builder ();
255- builder .add (JAVA_HOME .value () + "/bin/java" );
257+
258+ ImmutableList .Builder <String > baseArgsBuilder = ImmutableList .builder ();
259+ baseArgsBuilder .add (JAVA_HOME .value () + "/bin/java" );
256260 Integer debugPort = Integer .getInteger ("appengine.debug.port" );
257261 if (debugPort != null ) {
258- builder .add ("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:" + debugPort );
259- }
260- ImmutableList <String > runtimeArgs =
261- builder
262- .add (
263- "-Dcom.google.apphosting.runtime.jetty94.LEGACY_MODE=" + legacyMode ,
264- "-Dappengine.use.EE8=" + jakartaVersion .equals ("EE8" ),
265- "-Dappengine.use.EE10=" + jakartaVersion .equals ("EE10" ),
266- "-Dappengine.use.EE11=" + jakartaVersion .equals ("EE11" ),
267- "-Dappengine.use.jetty121=" + jettyVersion .equals ("12.1" ),
268- "-DGAE_RUNTIME=" + runtimeVersion ,
269- "-Dappengine.use.HttpConnector=" + useHttpConnector ,
270- "-Dappengine.ignore.responseSizeLimit="
271- + Boolean .getBoolean ("appengine.ignore.responseSizeLimit" ),
272- "-Djetty.server.dumpAfterStart="
273- + Boolean .getBoolean ("jetty.server.dumpAfterStart" ),
274- "-Duse.mavenjars=" + useMavenJars (),
275- "-cp" ,
276- useMavenJars ()
277- ? new File (runtimeDir , "jars/runtime-main.jar" ).getAbsolutePath ()
278- : new File (runtimeDir , "runtime-main.jar" ).getAbsolutePath ())
279- .addAll (RuntimeContext .optionalFlags ())
280- .addAll (RuntimeContext .jvmFlagsFromEnvironment (config .environmentEntries ()))
281- .add (
282- "com.google.apphosting.runtime.JavaRuntimeMainWithDefaults" ,
283- "--jetty_http_port=" + jettyPort ,
284- "--port=" + apiPort ,
285- "--trusted_host="
286- + HostAndPort .fromParts (apiSocketAddress .getAddress ().getHostAddress (), apiPort ),
287- runtimeDir .getAbsolutePath ())
288- .addAll (config .launcherFlags ())
289- .build ();
290- System .out .println ("ARGS=" + runtimeArgs );
291- Process runtimeProcess = RuntimeContext .launchRuntime (runtimeArgs , config .environmentEntries ());
292- OutputPump outPump = new OutputPump (runtimeProcess .getInputStream (), "[stdout] " );
293- OutputPump errPump = new OutputPump (runtimeProcess .getErrorStream (), "[stderr] " );
294- new Thread (outPump ).start ();
295- new Thread (errPump ).start ();
296- await ().atMost (30 , SECONDS ).until (() -> RuntimeContext .isPortAvailable ("localhost" , jettyPort ));
297- int timeoutMillis = 30_000 ;
298- RequestConfig requestConfig =
299- RequestConfig .custom ()
300- .setConnectTimeout (timeoutMillis )
301- .setConnectionRequestTimeout (timeoutMillis )
302- .setSocketTimeout (timeoutMillis )
303- .build ();
304- HttpClient httpClient =
305- HttpClientBuilder .create ().setDefaultRequestConfig (requestConfig ).build ();
306- ApiServerT httpApiServer = config .apiServerFactory ().newApiServer (apiPort , jettyPort );
307- return new RuntimeContext <>(
308- runtimeProcess , httpApiServer , httpClient , jettyPort , outPump , errPump );
262+ baseArgsBuilder .add ("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address:*:" + debugPort );
263+ }
264+ baseArgsBuilder
265+ .add (
266+ "-Dcom.google.apphosting.runtime.jetty94.LEGACY_MODE=" + legacyMode ,
267+ "-Dappengine.use.EE8=" + jakartaVersion .equals ("EE8" ),
268+ "-Dappengine.use.EE10=" + jakartaVersion .equals ("EE10" ),
269+ "-Dappengine.use.EE11=" + jakartaVersion .equals ("EE11" ),
270+ "-Dappengine.use.jetty121=" + jettyVersion .equals ("12.1" ),
271+ "-DGAE_RUNTIME=" + runtimeVersion ,
272+ "-Dappengine.use.HttpConnector=" + useHttpConnector ,
273+ "-Dappengine.ignore.responseSizeLimit="
274+ + Boolean .getBoolean ("appengine.ignore.responseSizeLimit" ),
275+ "-Djetty.server.dumpAfterStart=" + Boolean .getBoolean ("jetty.server.dumpAfterStart" ),
276+ "-Duse.mavenjars=" + useMavenJars (),
277+ "-cp" ,
278+ useMavenJars ()
279+ ? new File (runtimeDir , "jars/runtime-main.jar" ).getAbsolutePath ()
280+ : new File (runtimeDir , "runtime-main.jar" ).getAbsolutePath ())
281+ .addAll (RuntimeContext .optionalFlags ())
282+ .addAll (RuntimeContext .jvmFlagsFromEnvironment (config .environmentEntries ()));
283+
284+ ImmutableList <String > commonRuntimeArgs = baseArgsBuilder .build ();
285+
286+ for (int i = 0 ; i < MAX_RETRIES ; i ++) {
287+ OutputPump outPump = null ;
288+ OutputPump errPump = null ;
289+ ApiServerT httpApiServer = null ;
290+ int jettyPort = portPicker .pickUnusedPort ();
291+ int apiPort = portPicker .pickUnusedPort ();
292+ InetSocketAddress apiSocketAddress =
293+ new InetSocketAddress (InetAddress .getLoopbackAddress (), apiPort );
294+ String apiHost = apiSocketAddress .getAddress ().getHostAddress ();
295+ int apiPortNum = apiPort ;
296+
297+ ImmutableList <String > runtimeArgs =
298+ ImmutableList .<String >builder ()
299+ .addAll (commonRuntimeArgs )
300+ .add (
301+ "com.google.apphosting.runtime.JavaRuntimeMainWithDefaults" ,
302+ "--jetty_http_port=" + jettyPort ,
303+ "--port=" + apiPort ,
304+ "--trusted_host=" + HostAndPort .fromParts (apiHost , apiPortNum ),
305+ runtimeDir .getAbsolutePath ())
306+ .addAll (config .launcherFlags ())
307+ .build ();
308+
309+ System .out .println ("ARGS=" + runtimeArgs );
310+ Process runtimeProcess =
311+ RuntimeContext .launchRuntime (runtimeArgs , config .environmentEntries ());
312+ try {
313+ outPump = new OutputPump (runtimeProcess .getInputStream (), "[stdout] " );
314+ errPump = new OutputPump (runtimeProcess .getErrorStream (), "[stderr] " );
315+ OutputPump finalErrPump = errPump ;
316+ new Thread (outPump ).start ();
317+ new Thread (errPump ).start ();
318+ await ()
319+ .atMost (Duration .ofSeconds (30 ))
320+ .until (
321+ () -> {
322+ if (!runtimeProcess .isAlive ()) {
323+ List <String > lastStderrLines = finalErrPump .getLastLines (20 );
324+ throw new IllegalStateException (
325+ String .format (
326+ "Process died with exit code %d\n Last 20 lines of stderr:\n %s" ,
327+ runtimeProcess .exitValue (), String .join ("\n " , lastStderrLines )));
328+ }
329+ return RuntimeContext .isPortAvailable ("localhost" , jettyPort );
330+ });
331+ int timeoutMillis = 30_000 ;
332+ RequestConfig requestConfig =
333+ RequestConfig .custom ()
334+ .setConnectTimeout (timeoutMillis )
335+ .setConnectionRequestTimeout (timeoutMillis )
336+ .setSocketTimeout (timeoutMillis )
337+ .build ();
338+ HttpClient httpClient =
339+ HttpClientBuilder .create ().setDefaultRequestConfig (requestConfig ).build ();
340+ httpApiServer = config .apiServerFactory ().newApiServer (apiPort , jettyPort );
341+ return new RuntimeContext <>(
342+ runtimeProcess , httpApiServer , httpClient , jettyPort , outPump , errPump );
343+ } catch (Throwable t ) {
344+ // Log and clean up before retrying
345+ logger .atWarning ().withCause (t ).log ("Failed to start runtime, attempt %d of %d" , i + 1 , MAX_RETRIES );
346+ runtimeProcess .destroyForcibly ();
347+ try {
348+ runtimeProcess .waitFor (5 , SECONDS );
349+ } catch (InterruptedException e ) {
350+ Thread .currentThread ().interrupt ();
351+ }
352+ // Close pumps if they were initialized
353+ if (outPump != null ) {
354+ try {
355+ outPump .close ();
356+ } catch (IOException e ) {
357+ logger .atWarning ().withCause (e ).log ("Failed to close stdout pump" );
358+ }
359+ }
360+ if (errPump != null ) {
361+ try {
362+ errPump .close ();
363+ } catch (IOException e ) {
364+ logger .atWarning ().withCause (e ).log ("Failed to close stderr pump" );
365+ }
366+ }
367+ if (httpApiServer != null ) {
368+ try {
369+ httpApiServer .close ();
370+ } catch (IOException e ) {
371+ logger .atWarning ().withCause (e ).log ("Failed to close httpApiServer" );
372+ }
373+ }
374+ if (i == MAX_RETRIES - 1 ) {
375+ throw t ;
376+ }
377+ }
378+ }
379+ throw new IllegalStateException ("Should be unreachable" );
309380 }
310381
311382 public static class RuntimeContext <ApiServerT extends Closeable > implements AutoCloseable {
@@ -532,16 +603,36 @@ public Process runtimeProcess() {
532603
533604 @ Override
534605 public void close () throws IOException {
535- runtimeProcess .destroy ();
536- httpApiServer .close ();
606+ runtimeProcess .destroyForcibly ();
607+ try {
608+ runtimeProcess .waitFor (5 , SECONDS );
609+ } catch (InterruptedException e ) {
610+ Thread .currentThread ().interrupt ();
611+ }
612+ // Close resources, logging any exceptions but ensuring all are attempted to be closed.
613+ try {
614+ outPump .close ();
615+ } catch (IOException e ) {
616+ logger .atWarning ().withCause (e ).log ("Failed to close stdout pump" );
617+ }
618+ try {
619+ errPump .close ();
620+ } catch (IOException e ) {
621+ logger .atWarning ().withCause (e ).log ("Failed to close stderr pump" );
622+ }
623+ try {
624+ httpApiServer .close ();
625+ } catch (IOException e ) {
626+ logger .atWarning ().withCause (e ).log ("Failed to close httpApiServer" );
627+ }
537628 }
538629 }
539630
540631 static boolean useMavenJars () {
541632 return Boolean .getBoolean ("use.mavenjars" );
542633 }
543634
544- static class OutputPump implements Runnable {
635+ static class OutputPump implements Runnable , AutoCloseable {
545636 private final BufferedReader stream ;
546637 private final String echoPrefix ;
547638 private final BlockingQueue <String > outputQueue = new LinkedBlockingQueue <>();
@@ -564,6 +655,18 @@ public void run() {
564655 }
565656 }
566657
658+ @ Override
659+ public void close () throws IOException {
660+ stream .close ();
661+ }
662+
663+ /** Returns the last {@code n} lines captured by this pump. */
664+ List <String > getLastLines (int n ) {
665+ List <String > allLines = new ArrayList <>(outputQueue );
666+ int start = Math .max (0 , allLines .size () - n );
667+ return Collections .unmodifiableList (allLines .subList (start , allLines .size ()));
668+ }
669+
567670 void awaitOutputLineMatching (String pattern , long timeoutSeconds ) throws InterruptedException {
568671 long timeoutMillis = MILLISECONDS .convert (timeoutSeconds , SECONDS );
569672 long deadline = System .currentTimeMillis () + timeoutMillis ;
0 commit comments