From 88187cd18cd5cdebd70a0f5194ac18ab64fe2663 Mon Sep 17 00:00:00 2001 From: Ludovic Champenois Date: Sat, 24 Oct 2026 00:11:20 -0700 Subject: [PATCH] Bundle the Cloud Tasks client in the runtime jars and add an e2e session test The runtime jars (runtime_impl_jetty12/121) include every class of com.google.appengine.api.taskqueue, so since fd92dbec they also contain CloudTasksClientWrapper and TaskProcessor, but none of the Cloud Tasks client libraries they use. The runtime itself uses the Task Queue API from its private class loader, e.g. DeferredDatastoreSessionStore for async session persistence. With APPENGINE_USE_CLOUDTASK_PUSH_QUEUE=true, storing a session then fails the request with NoClassDefFoundError: com/google/auth/Credentials. Add google-cloud-tasks and its dependencies (gax, gax-grpc, gax-httpjson, grpc, google-auth-library, google-http-client, opencensus, opentelemetry, threetenbp) to both runtime jars, merge META-INF/services files so gRPC finds its providers, and exclude the netty native libraries as the SDK jar does. CloudTasksSessionTest runs the allinone app with async session persistence and the Cloud Tasks flag enabled, and checks that the session request is served and that the runtime logs no class loading errors. --- runtime/runtime_impl_jetty12/pom.xml | 40 ++++++ runtime/runtime_impl_jetty121/pom.xml | 40 ++++++ .../runtime/jetty/CloudTasksSessionTest.java | 122 ++++++++++++++++++ .../runtime/jetty/JavaRuntimeViaHttpBase.java | 11 ++ 4 files changed, 213 insertions(+) create mode 100644 runtime/test/src/test/java/com/google/apphosting/runtime/jetty/CloudTasksSessionTest.java diff --git a/runtime/runtime_impl_jetty12/pom.xml b/runtime/runtime_impl_jetty12/pom.xml index 343e779a..e4482f81 100644 --- a/runtime/runtime_impl_jetty12/pom.xml +++ b/runtime/runtime_impl_jetty12/pom.xml @@ -335,6 +335,12 @@ + + io.grpc:grpc-netty-shaded + + META-INF/native/** + + *:* @@ -488,11 +494,45 @@ org.ow2.asm:asm-commons org.ow2.asm:asm org.ow2.asm:asm-tree + + com.google.cloud:google-cloud-tasks + com.google.api.grpc:proto-google-cloud-tasks-v2 + com.google.api.grpc:grpc-google-cloud-tasks-v2 + com.google.api.grpc:proto-google-cloud-tasks-v2beta3 + com.google.api.grpc:grpc-google-cloud-tasks-v2beta3 + com.google.api.grpc:proto-google-iam-v1 + com.google.api:api-common + com.google.api:gax + com.google.api:gax-grpc + com.google.api:gax-httpjson + com.google.auth:google-auth-library-credentials + com.google.auth:google-auth-library-oauth2-http + com.google.http-client:google-http-client + com.google.http-client:google-http-client-gson + com.google.protobuf:protobuf-java-util + io.grpc:grpc-api + io.grpc:grpc-auth + io.grpc:grpc-context + io.grpc:grpc-core + io.grpc:grpc-netty-shaded + io.grpc:grpc-protobuf + io.grpc:grpc-protobuf-lite + io.grpc:grpc-stub + io.grpc:grpc-util + io.opencensus:opencensus-api + io.opencensus:opencensus-contrib-http-util + io.opentelemetry:opentelemetry-api + io.opentelemetry:opentelemetry-context + org.threeten:threetenbp + + + diff --git a/runtime/runtime_impl_jetty121/pom.xml b/runtime/runtime_impl_jetty121/pom.xml index b167e222..c1fa8034 100644 --- a/runtime/runtime_impl_jetty121/pom.xml +++ b/runtime/runtime_impl_jetty121/pom.xml @@ -358,6 +358,12 @@ + + io.grpc:grpc-netty-shaded + + META-INF/native/** + + *:* @@ -516,11 +522,45 @@ org.ow2.asm:asm-commons org.ow2.asm:asm org.ow2.asm:asm-tree + + com.google.cloud:google-cloud-tasks + com.google.api.grpc:proto-google-cloud-tasks-v2 + com.google.api.grpc:grpc-google-cloud-tasks-v2 + com.google.api.grpc:proto-google-cloud-tasks-v2beta3 + com.google.api.grpc:grpc-google-cloud-tasks-v2beta3 + com.google.api.grpc:proto-google-iam-v1 + com.google.api:api-common + com.google.api:gax + com.google.api:gax-grpc + com.google.api:gax-httpjson + com.google.auth:google-auth-library-credentials + com.google.auth:google-auth-library-oauth2-http + com.google.http-client:google-http-client + com.google.http-client:google-http-client-gson + com.google.protobuf:protobuf-java-util + io.grpc:grpc-api + io.grpc:grpc-auth + io.grpc:grpc-context + io.grpc:grpc-core + io.grpc:grpc-netty-shaded + io.grpc:grpc-protobuf + io.grpc:grpc-protobuf-lite + io.grpc:grpc-stub + io.grpc:grpc-util + io.opencensus:opencensus-api + io.opencensus:opencensus-contrib-http-util + io.opentelemetry:opentelemetry-api + io.opentelemetry:opentelemetry-context + org.threeten:threetenbp + + + diff --git a/runtime/test/src/test/java/com/google/apphosting/runtime/jetty/CloudTasksSessionTest.java b/runtime/test/src/test/java/com/google/apphosting/runtime/jetty/CloudTasksSessionTest.java new file mode 100644 index 00000000..2950cebd --- /dev/null +++ b/runtime/test/src/test/java/com/google/apphosting/runtime/jetty/CloudTasksSessionTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.apphosting.runtime.jetty; + +import static com.google.common.truth.Truth.assertWithMessage; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.appengine.tools.development.HttpApiServer; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.regex.Pattern; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * Tests HTTP sessions with async session persistence when the Task Queue API is routed to Cloud + * Tasks via {@code APPENGINE_USE_CLOUDTASK_PUSH_QUEUE}. + * + *

With async persistence, session writes are enqueued by the runtime itself (see {@code + * DeferredDatastoreSessionStore}), from the runtime's private class loader rather than from the + * application. The runtime jar must therefore contain the Cloud Tasks client and all of its + * dependencies, not only the application's {@code appengine-api-1.0-sdk} jar. + */ +@RunWith(Parameterized.class) +public final class CloudTasksSessionTest extends JavaRuntimeViaHttpBase { + @Rule public TemporaryFolder temp = new TemporaryFolder(); + + private static final Pattern CLASS_LOADING_ERROR = + Pattern.compile("ClassNotFoundException|NoClassDefFoundError"); + + private RuntimeContext runtime; + + @Parameterized.Parameters + public static List version() { + return allVersions(); + } + + public CloudTasksSessionTest(String runtimeVersion, String jettyVersion, String jakartaVersion) { + super(runtimeVersion, jettyVersion, jakartaVersion); + } + + @Before + public void startRuntime() throws Exception { + Path appRoot = temp.getRoot().toPath(); + if (isJakarta()) { + copyAppToDir("com/google/apphosting/loadtesting/allinone/ee10", appRoot); + } else { + copyAppToDir("com/google/apphosting/loadtesting/allinone", appRoot); + } + enableAsyncSessionPersistence(appRoot.resolve("WEB-INF/appengine-web.xml")); + ApiServerFactory apiServerFactory = + (apiPort, runtimePort) -> { + HttpApiServer httpApiServer = new HttpApiServer(apiPort, "localhost", runtimePort); + httpApiServer.start(false); + return httpApiServer; + }; + RuntimeContext.Config config = + RuntimeContext.Config.builder(apiServerFactory) + .setApplicationPath(appRoot.toString()) + .setEnvironmentEntries( + ImmutableMap.of( + "GAE_VERSION", "allinone", + "GOOGLE_CLOUD_PROJECT", "1", + "APPENGINE_USE_CLOUDTASK_PUSH_QUEUE", "true")) + .build(); + runtime = createRuntimeContext(config); + } + + @After + public void close() throws IOException { + runtime.close(); + } + + @Test + public void sessionWithAsyncPersistenceUsingCloudTasks() throws Exception { + // Storing the session makes the runtime enqueue a deferred datastore write through + // QueueImpl, which routes it to the Cloud Tasks client. If any class of that client is missing + // from the runtime jar, the request fails with a NoClassDefFoundError. + runtime.executeHttpGet("/?datastore_entities=3", "Added 3 entities\n", RESPONSE_200); + // There is no Cloud Tasks backend in this test, so the deferred write itself cannot succeed and + // the session content is not checked here; the request must still be served. + runtime.executeHttpGet("/?datastore_count", RESPONSE_200); + assertWithMessage("Runtime output should have no class loading errors") + .that(runtime.outputLinesContaining(CLASS_LOADING_ERROR)) + .isEmpty(); + } + + private static void enableAsyncSessionPersistence(Path appEngineWebXml) throws IOException { + String xml = Files.readString(appEngineWebXml, UTF_8); + String sessionsEnabled = "true"; + assertWithMessage("%s should enable sessions", appEngineWebXml) + .that(xml) + .contains(sessionsEnabled); + Files.writeString( + appEngineWebXml, + xml.replace( + sessionsEnabled, + sessionsEnabled + "\n "), + UTF_8); + } +} diff --git a/runtime/test/src/test/java/com/google/apphosting/runtime/jetty/JavaRuntimeViaHttpBase.java b/runtime/test/src/test/java/com/google/apphosting/runtime/jetty/JavaRuntimeViaHttpBase.java index b6d435b7..68938c91 100644 --- a/runtime/test/src/test/java/com/google/apphosting/runtime/jetty/JavaRuntimeViaHttpBase.java +++ b/runtime/test/src/test/java/com/google/apphosting/runtime/jetty/JavaRuntimeViaHttpBase.java @@ -581,6 +581,17 @@ public void awaitStderrLineMatching(String pattern, long timeoutSeconds) errPump.awaitOutputLineMatching(pattern, timeoutSeconds); } + /** Returns the stdout and stderr lines captured so far that contain the given pattern. */ + public ImmutableList outputLinesContaining(Pattern pattern) { + return ImmutableList.builder() + .addAll(outPump.getLastLines(Integer.MAX_VALUE)) + .addAll(errPump.getLastLines(Integer.MAX_VALUE)) + .build() + .stream() + .filter(line -> pattern.matcher(line).find()) + .collect(toImmutableList()); + } + static Process launchRuntime( ImmutableList args, ImmutableMap environmentEntries) throws IOException {