Skip to content

Commit 38a02bd

Browse files
ludochgae-java-bot
authored andcommitted
Add retry logic for starting test servers and runtimes. and use the staging phase for head for the end to end springboot test instead of a version of Cloud CLI which we do not control. This way, we can detect regressions earlier.
PiperOrigin-RevId: 854212113 Change-Id: I18497b05ccce026adfa691980b6dbd0e52cd1e3f
1 parent 65e8023 commit 38a02bd

6 files changed

Lines changed: 257 additions & 67 deletions

File tree

api_dev/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@
158158
<artifactId>truth-java8-extension</artifactId>
159159
<scope>test</scope>
160160
</dependency>
161+
<dependency>
162+
<groupId>com.google.flogger</groupId>
163+
<artifactId>flogger-system-backend</artifactId>
164+
<scope>test</scope>
165+
</dependency>
166+
<dependency>
167+
<groupId>com.google.flogger</groupId>
168+
<artifactId>google-extensions</artifactId>
169+
<scope>test</scope>
170+
</dependency>
161171
<dependency>
162172
<groupId>joda-time</groupId>
163173
<artifactId>joda-time</artifactId>

api_dev/src/test/java/com/google/appengine/api/urlfetch/dev/LocalURLFetchServiceIntegrationTest.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.google.common.collect.ImmutableMap;
4343
import com.google.common.collect.ImmutableSet;
4444
import com.google.common.io.CharStreams;
45+
import com.google.common.flogger.GoogleLogger;
4546
import java.io.BufferedReader;
4647
import java.io.IOException;
4748
import java.io.PrintWriter;
@@ -75,7 +76,10 @@
7576
@RunWith(JUnit4.class)
7677
public class LocalURLFetchServiceIntegrationTest {
7778

79+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
80+
7881
private static final String GSE_URL_FORMAT = "http://localhost:%s";
82+
private static final int MAX_RETRIES = 3;
7983

8084
private static final PortPicker portPicker = PortPicker.create();
8185

@@ -746,7 +750,21 @@ private void verifyHeaders(Map<String, String> expected, Map<String, String> act
746750
}
747751

748752
private void startServer() throws Exception {
749-
server.start();
753+
for (int i = 0; i < MAX_RETRIES; i++) {
754+
try {
755+
server.start();
756+
return;
757+
} catch (Exception e) {
758+
if (i == MAX_RETRIES - 1) {
759+
throw e;
760+
}
761+
logger.atWarning().withCause(e).log("Server start failed on port %d, retrying", port);
762+
// Retry with a new port
763+
port = portPicker.pickUnusedPort();
764+
server = new Server(port);
765+
server.setHandler(servletHandler);
766+
}
767+
}
750768
}
751769

752770
private void addServlet(String servletPath, HttpServlet servlet) {
@@ -789,7 +807,6 @@ protected void service(
789807
}
790808

791809
@Override
792-
@SuppressWarnings("unchecked")
793810
protected void doPost(
794811
HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
795812
throws ServletException, IOException {

runtime/test/src/test/java/com/google/apphosting/runtime/jetty9/JavaRuntimeViaHttpBase.java

Lines changed: 162 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@
6565
import java.nio.file.Files;
6666
import java.nio.file.Path;
6767
import java.nio.file.Paths;
68+
import java.time.Duration;
69+
import java.util.ArrayList;
6870
import java.util.Arrays;
71+
import java.util.Collections;
6972
import java.util.List;
7073
import java.util.concurrent.BlockingQueue;
7174
import java.util.concurrent.Executors;
@@ -87,8 +90,10 @@
8790
import org.junit.rules.TemporaryFolder;
8891

8992
public 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\nLast 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;

runtime/test/src/test/java/com/google/apphosting/runtime/jetty9/RemoteAddressTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ public void before() throws Exception {
7171
@After
7272
public void after() throws Exception {
7373
httpClient.stop();
74-
runtime.close();
74+
if (runtime != null) {
75+
runtime.close();
76+
}
7577
}
7678

7779
@Test

runtime/test/src/test/java/com/google/apphosting/runtime/jetty9/ServletContextListenerTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ public void before() throws Exception {
7575
@After
7676
public void after() throws Exception {
7777
httpClient.stop();
78-
runtime.close();
78+
if (runtime != null) {
79+
runtime.close();
80+
}
7981
}
8082

8183
@Test

0 commit comments

Comments
 (0)