Skip to content

Commit 13e4c7d

Browse files
committed
chore(implementation): Use Jetty handlers rather than Servlets
Optimization to avoid the extra dispatch overheads of Servlets by using Eclipse Jetty Handlers directly.
1 parent bef9b6d commit 13e4c7d

File tree

9 files changed

+253
-122
lines changed

9 files changed

+253
-122
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, HttpServletResponse res)
326+
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;
@@ -62,8 +65,9 @@ public static HttpFunctionExecutor forClass(Class<?> functionClass) {
6265
}
6366

6467
/** Executes the user's method, can handle all HTTP type methods. */
65-
@Override
66-
public void service(HttpServletRequest req, HttpServletResponse res) {
68+
public void handle(String s, Request baseRequest, HttpServletRequest req, HttpServletResponse res)
69+
throws IOException, ServletException {
70+
baseRequest.setHandled(true);
6771
HttpRequestImpl reqImpl = new HttpRequestImpl(req);
6872
HttpResponseImpl respImpl = new HttpResponseImpl(res);
6973
ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader();
@@ -75,7 +79,7 @@ public void service(HttpServletRequest req, HttpServletResponse res) {
7579
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
7680
} finally {
7781
Thread.currentThread().setContextClassLoader(oldContextLoader);
78-
respImpl.flush();
82+
respImpl.close();
7983
}
8084
}
8185
}

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, HttpServletResponse res)
102+
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: 82 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,92 @@ 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+
/** Close the response, flushing all content. */
97+
public void close() {
10398
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.
99+
IO.close(getOutputStream());
100+
} catch (IllegalStateException | IOException e) {
108101
try {
109-
getOutputStream().flush();
110-
} catch (IllegalStateException e) {
111-
getWriter().flush();
102+
IO.close(getWriter());
103+
} catch (IOException ioe) {
104+
// Too bad, can't close.
112105
}
113-
} catch (IOException e) {
114-
// Too bad, can't flush.
106+
}
107+
}
108+
109+
/**
110+
* A {@link BufferedWriter} that does not buffer. It is generally more efficient to buffer at the
111+
* lower level, since frequently total content is smaller than a single buffer and the lower level
112+
* buffer can turn a close into a last write that will avoid chunking the response if at all
113+
* possible. However, {@link BufferedWriter} is in the API for {@link HttpResponse}, so we must
114+
* return a writer of that type.
115+
*/
116+
private static class NonBufferedWriter extends BufferedWriter {
117+
private final Writer writer;
118+
119+
public NonBufferedWriter(Writer out) {
120+
super(out, 1);
121+
writer = out;
122+
}
123+
124+
@Override
125+
public void write(int c) throws IOException {
126+
writer.write(c);
127+
}
128+
129+
@Override
130+
public void write(char[] cbuf) throws IOException {
131+
writer.write(cbuf);
132+
}
133+
134+
@Override
135+
public void write(char[] cbuf, int off, int len) throws IOException {
136+
writer.write(cbuf, off, len);
137+
}
138+
139+
@Override
140+
public void write(String str) throws IOException {
141+
writer.write(str);
142+
}
143+
144+
@Override
145+
public void write(String str, int off, int len) throws IOException {
146+
writer.write(str, off, len);
147+
}
148+
149+
@Override
150+
public Writer append(CharSequence csq) throws IOException {
151+
return writer.append(csq);
152+
}
153+
154+
@Override
155+
public Writer append(CharSequence csq, int start, int end) throws IOException {
156+
return writer.append(csq, start, end);
157+
}
158+
159+
@Override
160+
public Writer append(char c) throws IOException {
161+
return writer.append(c);
162+
}
163+
164+
@Override
165+
public void flush() throws IOException {
166+
writer.flush();
167+
}
168+
169+
@Override
170+
public void close() throws IOException {
171+
writer.close();
172+
}
173+
174+
@Override
175+
public void newLine() throws IOException {
176+
writer.write(System.lineSeparator());
115177
}
116178
}
117179
}

0 commit comments

Comments
 (0)