Skip to content

Commit 59c1cf4

Browse files
committed
chore(implementation): Remove servlets and use jetty-9.4 handlers instead
Reduce overheads by avoid a servlet dispatch cycle and using Jetty handlers directly. BREAKING CHANGE: use Java 17 or above, as required by Eclipse Jetty-12.
1 parent 7df8be8 commit 59c1cf4

File tree

10 files changed

+266
-121
lines changed

10 files changed

+266
-121
lines changed

invoker/core/pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,6 @@
4646
<artifactId>functions-framework-api</artifactId>
4747
<version>1.1.0</version>
4848
</dependency>
49-
<dependency>
50-
<groupId>javax.servlet</groupId>
51-
<artifactId>javax.servlet-api</artifactId>
52-
<version>4.0.1</version>
53-
</dependency>
5449
<dependency>
5550
<groupId>io.cloudevents</groupId>
5651
<artifactId>cloudevents-core</artifactId>

invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@
4242
import java.util.TreeMap;
4343
import java.util.logging.Level;
4444
import java.util.logging.Logger;
45-
import javax.servlet.http.HttpServlet;
45+
import javax.servlet.ServletException;
4646
import javax.servlet.http.HttpServletRequest;
4747
import javax.servlet.http.HttpServletResponse;
48+
import org.eclipse.jetty.server.Request;
49+
import org.eclipse.jetty.server.handler.AbstractHandler;
4850

4951
/** Executes the user's background function. */
50-
public final class BackgroundFunctionExecutor extends HttpServlet {
52+
public final class BackgroundFunctionExecutor extends AbstractHandler {
5153
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
5254

5355
private final FunctionExecutor<?> functionExecutor;
@@ -223,7 +225,7 @@ private static Context contextFromCloudEvent(CloudEvent cloudEvent) {
223225
* for the various triggers. CloudEvents are ones that follow the standards defined by <a
224226
* href="https://cloudevents.io">cloudevents.io</a>.
225227
*
226-
* @param <CloudEventDataT> the type to be used in the {@link Unmarshallers} call when
228+
* @param <CloudEventDataT> the type to be used in the {code Unmarshallers} call when
227229
* unmarshalling this event, if it is a CloudEvent.
228230
*/
229231
private abstract static class FunctionExecutor<CloudEventDataT> {
@@ -320,7 +322,9 @@ void serviceCloudEvent(CloudEvent cloudEvent) throws Exception {
320322

321323
/** Executes the user's background function. This can handle all HTTP methods. */
322324
@Override
323-
public void service(HttpServletRequest req, HttpServletResponse res) throws IOException {
325+
public void handle(String s, Request baseRequest, HttpServletRequest req,
326+
HttpServletResponse res) throws IOException, ServletException {
327+
baseRequest.setHandled(true);
324328
String contentType = req.getContentType();
325329
try {
326330
if ((contentType != null && contentType.startsWith("application/cloudevents+json"))

invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
import com.google.cloud.functions.HttpFunction;
1818
import com.google.cloud.functions.invoker.http.HttpRequestImpl;
1919
import com.google.cloud.functions.invoker.http.HttpResponseImpl;
20+
import java.io.IOException;
2021
import java.util.logging.Level;
2122
import java.util.logging.Logger;
22-
import javax.servlet.http.HttpServlet;
23+
import javax.servlet.ServletException;
2324
import javax.servlet.http.HttpServletRequest;
2425
import javax.servlet.http.HttpServletResponse;
26+
import org.eclipse.jetty.server.Request;
27+
import org.eclipse.jetty.server.handler.AbstractHandler;
2528

2629
/** Executes the user's method. */
27-
public class HttpFunctionExecutor extends HttpServlet {
30+
public class HttpFunctionExecutor extends AbstractHandler {
2831
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
2932

3033
private final HttpFunction function;
@@ -58,8 +61,9 @@ public static HttpFunctionExecutor forClass(Class<?> functionClass) {
5861
}
5962

6063
/** Executes the user's method, can handle all HTTP type methods. */
61-
@Override
62-
public void service(HttpServletRequest req, HttpServletResponse res) {
64+
public void handle(String s, Request baseRequest, HttpServletRequest req,
65+
HttpServletResponse res) throws IOException, ServletException {
66+
baseRequest.setHandled(true);
6367
HttpRequestImpl reqImpl = new HttpRequestImpl(req);
6468
HttpResponseImpl respImpl = new HttpResponseImpl(res);
6569
ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader();
@@ -71,7 +75,7 @@ public void service(HttpServletRequest req, HttpServletResponse res) {
7175
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
7276
} finally {
7377
Thread.currentThread().setContextClassLoader(oldContextLoader);
74-
respImpl.flush();
78+
respImpl.close();
7579
}
7680
}
7781
}

invoker/core/src/main/java/com/google/cloud/functions/invoker/TypedFunctionExecutor.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@
1010
import com.google.gson.GsonBuilder;
1111
import java.io.BufferedReader;
1212
import java.io.BufferedWriter;
13+
import java.io.IOException;
1314
import java.lang.reflect.Type;
1415
import java.util.Arrays;
1516
import java.util.Optional;
1617
import java.util.logging.Level;
1718
import java.util.logging.Logger;
18-
import javax.servlet.http.HttpServlet;
19+
import javax.servlet.ServletException;
1920
import javax.servlet.http.HttpServletRequest;
2021
import javax.servlet.http.HttpServletResponse;
22+
import org.eclipse.jetty.http.HttpStatus;
23+
import org.eclipse.jetty.server.Request;
24+
import org.eclipse.jetty.server.handler.AbstractHandler;
2125

22-
public class TypedFunctionExecutor extends HttpServlet {
26+
public class TypedFunctionExecutor extends AbstractHandler {
2327
private static final String APPLY_METHOD = "apply";
2428
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
2529

@@ -94,7 +98,9 @@ static Optional<Type> handlerTypeArgument(Class<? extends TypedFunction<?, ?>> f
9498

9599
/** Executes the user's method, can handle all HTTP type methods. */
96100
@Override
97-
public void service(HttpServletRequest req, HttpServletResponse res) {
101+
public void handle(String s, Request baseRequest, HttpServletRequest req,
102+
HttpServletResponse res) throws IOException, ServletException {
103+
baseRequest.setHandled(true);
98104
HttpRequestImpl reqImpl = new HttpRequestImpl(req);
99105
HttpResponseImpl resImpl = new HttpResponseImpl(res);
100106
ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();
@@ -104,7 +110,7 @@ public void service(HttpServletRequest req, HttpServletResponse res) {
104110
handleRequest(reqImpl, resImpl);
105111
} finally {
106112
Thread.currentThread().setContextClassLoader(oldContextClassLoader);
107-
resImpl.flush();
113+
resImpl.close();
108114
}
109115
}
110116

@@ -114,7 +120,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
114120
reqObj = format.deserialize(req, argType);
115121
} catch (Throwable t) {
116122
logger.log(Level.SEVERE, "Failed to parse request for " + function.getClass().getName(), t);
117-
res.setStatusCode(HttpServletResponse.SC_BAD_REQUEST);
123+
res.setStatusCode(HttpStatus.BAD_REQUEST_400);
118124
return;
119125
}
120126

@@ -123,7 +129,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
123129
resObj = function.apply(reqObj);
124130
} catch (Throwable t) {
125131
logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t);
126-
res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
132+
res.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
127133
return;
128134
}
129135

@@ -132,7 +138,7 @@ private void handleRequest(HttpRequest req, HttpResponse res) {
132138
} catch (Throwable t) {
133139
logger.log(
134140
Level.SEVERE, "Failed to serialize response for " + function.getClass().getName(), t);
135-
res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
141+
res.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR_500);
136142
return;
137143
}
138144
}
@@ -147,7 +153,7 @@ private static class GsonWireFormat implements TypedFunction.WireFormat {
147153
@Override
148154
public void serialize(Object object, HttpResponse response) throws Exception {
149155
if (object == null) {
150-
response.setStatusCode(HttpServletResponse.SC_NO_CONTENT);
156+
response.setStatusCode(HttpStatus.NO_CONTENT_204);
151157
return;
152158
}
153159
try (BufferedWriter bodyWriter = response.getWriter()) {

invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@
3333
import java.util.TreeMap;
3434
import java.util.regex.Matcher;
3535
import java.util.regex.Pattern;
36+
import javax.servlet.MultipartConfigElement;
3637
import javax.servlet.ServletException;
3738
import javax.servlet.http.HttpServletRequest;
3839
import javax.servlet.http.Part;
3940

4041
public class HttpRequestImpl implements HttpRequest {
4142
private final HttpServletRequest request;
43+
private final MultipartConfigElement multipartConfigElement = new MultipartConfigElement("");
4244

4345
public HttpRequestImpl(HttpServletRequest request) {
4446
this.request = request;
@@ -81,6 +83,7 @@ public Map<String, HttpPart> getParts() {
8183
throw new IllegalStateException("Content-Type must be multipart/form-data: " + contentType);
8284
}
8385
try {
86+
request.setAttribute("org.eclipse.jetty.multipartConfig", multipartConfigElement);
8487
return request.getParts().stream().collect(toMap(Part::getName, HttpPartImpl::new));
8588
} catch (IOException e) {
8689
throw new UncheckedIOException(e);

invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
import java.io.BufferedWriter;
2121
import java.io.IOException;
2222
import java.io.OutputStream;
23+
import java.io.Writer;
2324
import java.util.ArrayList;
2425
import java.util.Collection;
2526
import java.util.List;
2627
import java.util.Map;
2728
import java.util.Optional;
2829
import java.util.TreeMap;
2930
import javax.servlet.http.HttpServletResponse;
31+
import org.eclipse.jetty.util.IO;
3032

3133
public class HttpResponseImpl implements HttpResponse {
3234
private final HttpServletResponse response;
@@ -43,7 +45,7 @@ public void setStatusCode(int code) {
4345
@Override
4446
@SuppressWarnings("deprecation")
4547
public void setStatusCode(int code, String message) {
46-
response.setStatus(code, message);
48+
response.setStatus(code);
4749
}
4850

4951
@Override
@@ -86,32 +88,96 @@ public OutputStream getOutputStream() throws IOException {
8688
@Override
8789
public synchronized BufferedWriter getWriter() throws IOException {
8890
if (writer == null) {
89-
// Unfortunately this means that we get two intermediate objects between the object we return
90-
// and the underlying Writer that response.getWriter() wraps. We could try accessing the
91-
// PrintWriter.out field via reflection, but that sort of access to non-public fields of
92-
// platform classes is now frowned on and may draw warnings or even fail in subsequent
93-
// versions. We could instead wrap the OutputStream, but that would require us to deduce the
94-
// appropriate Charset, using logic like this:
95-
// https://github.com/eclipse/jetty.project/blob/923ec38adf/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java#L731
96-
// We may end up doing that if performance is an issue.
97-
writer = new BufferedWriter(response.getWriter());
91+
writer = new NonBufferedWriter(response.getWriter());
9892
}
9993
return writer;
10094
}
10195

102-
public void flush() {
96+
/**
97+
* Close the response, flushing all content.
98+
*/
99+
public void close() {
103100
try {
104-
// We can't use HttpServletResponse.flushBuffer() because we wrap the
105-
// PrintWriter returned by HttpServletResponse in our own BufferedWriter
106-
// to match our API. So we have to flush whichever of getWriter() or
107-
// getOutputStream() works.
101+
IO.close(getOutputStream());
102+
} catch (IllegalStateException | IOException e) {
108103
try {
109-
getOutputStream().flush();
110-
} catch (IllegalStateException e) {
111-
getWriter().flush();
104+
IO.close(getWriter());
105+
} catch (IOException ioe) {
106+
// Too bad, can't close.
112107
}
113-
} catch (IOException e) {
114-
// Too bad, can't flush.
108+
}
109+
}
110+
111+
/**
112+
* A {@link BufferedWriter} that does not buffer.
113+
* It is generally more efficient to buffer at the lower level,
114+
* since frequently total content is smaller than a single buffer and
115+
* the lower level buffer can turn a close into a last write that will avoid
116+
* chunking the response if at all possible. However, {@link BufferedWriter}
117+
* is in the API for {@link HttpResponse}, so we must return a writer of
118+
* that type.
119+
*/
120+
private static class NonBufferedWriter extends BufferedWriter {
121+
private final Writer writer;
122+
123+
public NonBufferedWriter(Writer out) {
124+
super(out, 1);
125+
writer = out;
126+
}
127+
128+
@Override
129+
public void write(int c) throws IOException {
130+
writer.write(c);
131+
}
132+
133+
@Override
134+
public void write(char[] cbuf) throws IOException {
135+
writer.write(cbuf);
136+
}
137+
138+
@Override
139+
public void write(char[] cbuf, int off, int len) throws IOException {
140+
writer.write(cbuf, off, len);
141+
}
142+
143+
@Override
144+
public void write(String str) throws IOException {
145+
writer.write(str);
146+
}
147+
148+
@Override
149+
public void write(String str, int off, int len) throws IOException {
150+
writer.write(str, off, len);
151+
}
152+
153+
@Override
154+
public Writer append(CharSequence csq) throws IOException {
155+
return writer.append(csq);
156+
}
157+
158+
@Override
159+
public Writer append(CharSequence csq, int start, int end) throws IOException {
160+
return writer.append(csq, start, end);
161+
}
162+
163+
@Override
164+
public Writer append(char c) throws IOException {
165+
return writer.append(c);
166+
}
167+
168+
@Override
169+
public void flush() throws IOException {
170+
writer.flush();
171+
}
172+
173+
@Override
174+
public void close() throws IOException {
175+
writer.close();
176+
}
177+
178+
@Override
179+
public void newLine() throws IOException {
180+
writer.write(System.lineSeparator());
115181
}
116182
}
117183
}

0 commit comments

Comments
 (0)