Skip to content

Port to use Jetty-12 core without servlets #235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
22 changes: 9 additions & 13 deletions invoker/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.jupiter.version>5.3.2</junit.jupiter.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<cloudevents.sdk.version>2.5.0</cloudevents.sdk.version>
<jetty.version>12.0.2-SNAPSHOT</jetty.version>
</properties>

<licenses>
Expand All @@ -46,11 +47,6 @@
<artifactId>functions-framework-api</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-core</artifactId>
Expand Down Expand Up @@ -97,13 +93,13 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.4.52.v20230823</version>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.52.v20230823</version>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>com.beust</groupId>
Expand Down Expand Up @@ -151,7 +147,7 @@
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>9.4.52.v20230823</version>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,32 @@
import io.cloudevents.http.HttpMessageFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;

/** Executes the user's background function. */
public final class BackgroundFunctionExecutor extends HttpServlet {
public final class BackgroundFunctionExecutor extends Handler.Abstract {
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");

private final FunctionExecutor<?> functionExecutor;
Expand Down Expand Up @@ -175,8 +182,10 @@ static Optional<Type> backgroundFunctionTypeArgument(
.findFirst();
}

private static Event parseLegacyEvent(HttpServletRequest req) throws IOException {
try (BufferedReader bodyReader = req.getReader()) {
private static Event parseLegacyEvent(Request req) throws IOException {
try (BufferedReader bodyReader = new BufferedReader(
new InputStreamReader(Content.Source.asInputStream(req),
Objects.requireNonNullElse(Request.getCharset(req), StandardCharsets.ISO_8859_1)))) {
return parseLegacyEvent(bodyReader);
}
}
Expand Down Expand Up @@ -223,7 +232,7 @@ private static Context contextFromCloudEvent(CloudEvent cloudEvent) {
* for the various triggers. CloudEvents are ones that follow the standards defined by <a
* href="https://cloudevents.io">cloudevents.io</a>.
*
* @param <CloudEventDataT> the type to be used in the {@link Unmarshallers} call when
* @param <CloudEventDataT> the type to be used in the {code Unmarshallers} call when
* unmarshalling this event, if it is a CloudEvent.
*/
private abstract static class FunctionExecutor<CloudEventDataT> {
Expand Down Expand Up @@ -320,20 +329,23 @@ void serviceCloudEvent(CloudEvent cloudEvent) throws Exception {

/** Executes the user's background function. This can handle all HTTP methods. */
@Override
public void service(HttpServletRequest req, HttpServletResponse res) throws IOException {
String contentType = req.getContentType();
public boolean handle(Request req, Response res, Callback callback) throws Exception {
String contentType = req.getHeaders().get(HttpHeader.CONTENT_TYPE);
try {
if ((contentType != null && contentType.startsWith("application/cloudevents+json"))
|| req.getHeader("ce-specversion") != null) {
|| req.getHeaders().get("ce-specversion") != null) {
serviceCloudEvent(req);
} else {
serviceLegacyEvent(req);
}
res.setStatus(HttpServletResponse.SC_OK);
res.setStatus(HttpStatus.OK_200);
callback.succeeded();
} catch (Throwable t) {
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
logger.log(Level.SEVERE, "Failed to execute " + functionExecutor.functionName(), t);
res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
callback.succeeded();
}
return true;
}

private enum CloudEventKind {
Expand All @@ -347,10 +359,11 @@ private enum CloudEventKind {
* @param <CloudEventT> a fake type parameter, which corresponds to the type parameter of {@link
* FunctionExecutor}.
*/
private <CloudEventT> void serviceCloudEvent(HttpServletRequest req) throws Exception {
private <CloudEventT> void serviceCloudEvent(Request req) throws Exception {
@SuppressWarnings("unchecked")
FunctionExecutor<CloudEventT> executor = (FunctionExecutor<CloudEventT>) functionExecutor;
byte[] body = req.getInputStream().readAllBytes();

byte[] body = Content.Source.asByteArrayAsync(req, -1).get();
MessageReader reader = HttpMessageFactory.createReaderFromMultimap(headerMap(req), body);
// It's important not to set the context ClassLoader earlier, because MessageUtils will use
// ServiceLoader.load(EventFormat.class) to find a handler to deserialize a binary CloudEvent
Expand All @@ -364,17 +377,16 @@ private <CloudEventT> void serviceCloudEvent(HttpServletRequest req) throws Exce
// https://github.com/cloudevents/sdk-java/pull/259.
}

private static Map<String, List<String>> headerMap(HttpServletRequest req) {
private static Map<String, List<String>> headerMap(Request req) {
Map<String, List<String>> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (String header : Collections.list(req.getHeaderNames())) {
for (String value : Collections.list(req.getHeaders(header))) {
headerMap.computeIfAbsent(header, unused -> new ArrayList<>()).add(value);
}
for (HttpField field : req.getHeaders()) {
headerMap.computeIfAbsent(field.getName(), unused -> new ArrayList<>())
.addAll(field.getValueList());
}
return headerMap;
}

private void serviceLegacyEvent(HttpServletRequest req) throws Exception {
private void serviceLegacyEvent(Request req) throws Exception {
Event event = parseLegacyEvent(req);
runWithContextClassLoader(() -> functionExecutor.serviceLegacyEvent(event));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
import com.google.cloud.functions.invoker.http.HttpResponseImpl;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;

/** Executes the user's method. */
public class HttpFunctionExecutor extends HttpServlet {
public class HttpFunctionExecutor extends Handler.Abstract {
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");

private final HttpFunction function;
Expand Down Expand Up @@ -59,19 +61,27 @@ public static HttpFunctionExecutor forClass(Class<?> functionClass) {

/** Executes the user's method, can handle all HTTP type methods. */
@Override
public void service(HttpServletRequest req, HttpServletResponse res) {
HttpRequestImpl reqImpl = new HttpRequestImpl(req);
HttpResponseImpl respImpl = new HttpResponseImpl(res);
public boolean handle(Request request, Response response, Callback callback) throws Exception {

HttpRequestImpl reqImpl = new HttpRequestImpl(request);
HttpResponseImpl respImpl = new HttpResponseImpl(response);
ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader());
function.service(reqImpl, respImpl);
respImpl.close(callback);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t);
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
if (response.isCommitted()) {
callback.failed(t);
} else {
response.reset();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
callback.succeeded();
}
} finally {
Thread.currentThread().setContextClassLoader(oldContextLoader);
respImpl.flush();
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;

public class TypedFunctionExecutor extends HttpServlet {
public class TypedFunctionExecutor extends Handler.Abstract {
private static final String APPLY_METHOD = "apply";
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");

Expand Down Expand Up @@ -94,18 +96,27 @@ static Optional<Type> handlerTypeArgument(Class<? extends TypedFunction<?, ?>> f

/** Executes the user's method, can handle all HTTP type methods. */
@Override
public void service(HttpServletRequest req, HttpServletResponse res) {
public boolean handle(Request req, Response res, Callback callback) throws Exception {
HttpRequestImpl reqImpl = new HttpRequestImpl(req);
HttpResponseImpl resImpl = new HttpResponseImpl(res);
ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();

try {
Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader());
handleRequest(reqImpl, resImpl);
resImpl.close(callback);
} catch (Throwable t) {
if (res.isCommitted()) {
callback.failed(t);
} else {
res.reset();
res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
callback.succeeded();
}
} finally {
Thread.currentThread().setContextClassLoader(oldContextClassLoader);
resImpl.flush();
}
return true;
}

private void handleRequest(HttpRequest req, HttpResponse res) {
Expand All @@ -114,7 +125,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
reqObj = format.deserialize(req, argType);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to parse request for " + function.getClass().getName(), t);
res.setStatusCode(HttpServletResponse.SC_BAD_REQUEST);
res.setStatusCode(HttpStatus.BAD_REQUEST_400);
return;
}

Expand All @@ -123,7 +134,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
resObj = function.apply(reqObj);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t);
res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
res.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
return;
}

Expand All @@ -132,7 +143,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
} catch (Throwable t) {
logger.log(
Level.SEVERE, "Failed to serialize response for " + function.getClass().getName(), t);
res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
res.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
return;
}
}
Expand All @@ -147,7 +158,7 @@ private static class GsonWireFormat implements TypedFunction.WireFormat {
@Override
public void serialize(Object object, HttpResponse response) throws Exception {
if (object == null) {
response.setStatusCode(HttpServletResponse.SC_NO_CONTENT);
response.setStatusCode(HttpStatus.NO_CONTENT_204);
return;
}
try (BufferedWriter bodyWriter = response.getWriter()) {
Expand Down
Loading