Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions runtime/runtime_impl_jetty12/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,12 @@
</relocation>
</relocations>
<filters>
<filter>
<artifact>io.grpc:grpc-netty-shaded</artifact>
<excludes>
<exclude>META-INF/native/**</exclude>
</excludes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
Expand Down Expand Up @@ -488,11 +494,45 @@
<include>org.ow2.asm:asm-commons</include>
<include>org.ow2.asm:asm</include>
<include>org.ow2.asm:asm-tree</include>
<!-- Cloud Tasks client, used by the Task Queue API (e.g. async session persistence)
when APPENGINE_USE_CLOUDTASK_PUSH_QUEUE is enabled. -->
<include>com.google.cloud:google-cloud-tasks</include>
<include>com.google.api.grpc:proto-google-cloud-tasks-v2</include>
<include>com.google.api.grpc:grpc-google-cloud-tasks-v2</include>
<include>com.google.api.grpc:proto-google-cloud-tasks-v2beta3</include>
<include>com.google.api.grpc:grpc-google-cloud-tasks-v2beta3</include>
<include>com.google.api.grpc:proto-google-iam-v1</include>
<include>com.google.api:api-common</include>
<include>com.google.api:gax</include>
<include>com.google.api:gax-grpc</include>
<include>com.google.api:gax-httpjson</include>
<include>com.google.auth:google-auth-library-credentials</include>
<include>com.google.auth:google-auth-library-oauth2-http</include>
<include>com.google.http-client:google-http-client</include>
<include>com.google.http-client:google-http-client-gson</include>
<include>com.google.protobuf:protobuf-java-util</include>
<include>io.grpc:grpc-api</include>
<include>io.grpc:grpc-auth</include>
<include>io.grpc:grpc-context</include>
<include>io.grpc:grpc-core</include>
<include>io.grpc:grpc-netty-shaded</include>
<include>io.grpc:grpc-protobuf</include>
<include>io.grpc:grpc-protobuf-lite</include>
<include>io.grpc:grpc-stub</include>
<include>io.grpc:grpc-util</include>
<include>io.opencensus:opencensus-api</include>
<include>io.opencensus:opencensus-contrib-http-util</include>
<include>io.opentelemetry:opentelemetry-api</include>
<include>io.opentelemetry:opentelemetry-context</include>
<include>org.threeten:threetenbp</include>
</includes>
<!--excludes>
<exclude>jakarta.servlet:jakarta.servlet-api</exclude>
</excludes-->
</artifactSet>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
Expand Down
40 changes: 40 additions & 0 deletions runtime/runtime_impl_jetty121/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@
</relocation>
</relocations>
<filters>
<filter>
<artifact>io.grpc:grpc-netty-shaded</artifact>
<excludes>
<exclude>META-INF/native/**</exclude>
</excludes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
Expand Down Expand Up @@ -516,11 +522,45 @@
<include>org.ow2.asm:asm-commons</include>
<include>org.ow2.asm:asm</include>
<include>org.ow2.asm:asm-tree</include>
<!-- Cloud Tasks client, used by the Task Queue API (e.g. async session persistence)
when APPENGINE_USE_CLOUDTASK_PUSH_QUEUE is enabled. -->
<include>com.google.cloud:google-cloud-tasks</include>
<include>com.google.api.grpc:proto-google-cloud-tasks-v2</include>
<include>com.google.api.grpc:grpc-google-cloud-tasks-v2</include>
<include>com.google.api.grpc:proto-google-cloud-tasks-v2beta3</include>
<include>com.google.api.grpc:grpc-google-cloud-tasks-v2beta3</include>
<include>com.google.api.grpc:proto-google-iam-v1</include>
<include>com.google.api:api-common</include>
<include>com.google.api:gax</include>
<include>com.google.api:gax-grpc</include>
<include>com.google.api:gax-httpjson</include>
<include>com.google.auth:google-auth-library-credentials</include>
<include>com.google.auth:google-auth-library-oauth2-http</include>
<include>com.google.http-client:google-http-client</include>
<include>com.google.http-client:google-http-client-gson</include>
<include>com.google.protobuf:protobuf-java-util</include>
<include>io.grpc:grpc-api</include>
<include>io.grpc:grpc-auth</include>
<include>io.grpc:grpc-context</include>
<include>io.grpc:grpc-core</include>
<include>io.grpc:grpc-netty-shaded</include>
<include>io.grpc:grpc-protobuf</include>
<include>io.grpc:grpc-protobuf-lite</include>
<include>io.grpc:grpc-stub</include>
<include>io.grpc:grpc-util</include>
<include>io.opencensus:opencensus-api</include>
<include>io.opencensus:opencensus-contrib-http-util</include>
<include>io.opentelemetry:opentelemetry-api</include>
<include>io.opentelemetry:opentelemetry-context</include>
<include>org.threeten:threetenbp</include>
</includes>
<!--excludes>
<exclude>jakarta.servlet:jakarta.servlet-api</exclude>
</excludes-->
</artifactSet>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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<Object[]> 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<HttpApiServer> 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 = "<sessions-enabled>true</sessions-enabled>";
assertWithMessage("%s should enable sessions", appEngineWebXml)
.that(xml)
.contains(sessionsEnabled);
Files.writeString(
appEngineWebXml,
xml.replace(
sessionsEnabled,
sessionsEnabled + "\n <async-session-persistence enabled=\"true\" />"),
UTF_8);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> outputLinesContaining(Pattern pattern) {
return ImmutableList.<String>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<String> args, ImmutableMap<String, String> environmentEntries)
throws IOException {
Expand Down
Loading