Skip to content

Commit 21bb6fc

Browse files
committed
fix: server times out when specified by CLOUD_RUN_TIMEOUT_SECONDS
1 parent dc9bc1f commit 21bb6fc

File tree

5 files changed

+168
-12
lines changed

5 files changed

+168
-12
lines changed

invoker/core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@
100100
<artifactId>jetty-servlet</artifactId>
101101
<version>9.4.54.v20240208</version>
102102
</dependency>
103+
<dependency>
104+
<groupId>org.eclipse.jetty</groupId>
105+
<artifactId>jetty-servlets</artifactId>
106+
<version>9.4.54.v20240208</version>
107+
</dependency>
103108
<dependency>
104109
<groupId>org.eclipse.jetty</groupId>
105110
<artifactId>jetty-server</artifactId>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.cloud.functions.invoker.http;
16+
17+
import java.io.IOException;
18+
import java.util.Timer;
19+
import java.util.TimerTask;
20+
import javax.servlet.Filter;
21+
import javax.servlet.FilterChain;
22+
import javax.servlet.ServletException;
23+
import javax.servlet.ServletRequest;
24+
import javax.servlet.ServletResponse;
25+
import javax.servlet.http.HttpServletResponse;
26+
27+
public class TimeoutFilter implements Filter {
28+
private static final int DEFAULT_TIMEOUT_SECONDS = 30; // Default timeout in seconds
29+
private final int timeoutMs;
30+
31+
public TimeoutFilter() {
32+
this(DEFAULT_TIMEOUT_SECONDS);
33+
}
34+
35+
public TimeoutFilter(int timeoutSeconds) {
36+
this.timeoutMs = timeoutSeconds * 1000; // Convert seconds to milliseconds
37+
}
38+
39+
@Override
40+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
41+
throws IOException, ServletException {
42+
Timer timer = new Timer(true);
43+
TimerTask timeoutTask =
44+
new TimerTask() {
45+
@Override
46+
public void run() {
47+
if (response instanceof HttpServletResponse) {
48+
try {
49+
((HttpServletResponse) response)
50+
.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, "Request timed out");
51+
} catch (IOException e) {
52+
e.printStackTrace();
53+
}
54+
} else {
55+
try {
56+
response.getWriter().write("Request timed out");
57+
} catch (IOException e) {
58+
e.printStackTrace();
59+
}
60+
}
61+
}
62+
};
63+
64+
timer.schedule(timeoutTask, timeoutMs);
65+
66+
try {
67+
chain.doFilter(request, response);
68+
timeoutTask.cancel();
69+
} finally {
70+
timer.purge();
71+
}
72+
}
73+
}

invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.cloud.functions.invoker.HttpFunctionExecutor;
2626
import com.google.cloud.functions.invoker.TypedFunctionExecutor;
2727
import com.google.cloud.functions.invoker.gcf.JsonLogHandler;
28+
import com.google.cloud.functions.invoker.http.TimeoutFilter;
2829
import java.io.File;
2930
import java.io.IOException;
3031
import java.io.UncheckedIOException;
@@ -38,6 +39,7 @@
3839
import java.util.ArrayList;
3940
import java.util.Arrays;
4041
import java.util.Collections;
42+
import java.util.EnumSet;
4143
import java.util.HashSet;
4244
import java.util.List;
4345
import java.util.Map;
@@ -48,6 +50,7 @@
4850
import java.util.logging.Level;
4951
import java.util.logging.Logger;
5052
import java.util.stream.Stream;
53+
import javax.servlet.DispatcherType;
5154
import javax.servlet.MultipartConfigElement;
5255
import javax.servlet.ServletException;
5356
import javax.servlet.http.HttpServlet;
@@ -59,6 +62,7 @@
5962
import org.eclipse.jetty.server.Server;
6063
import org.eclipse.jetty.server.ServerConnector;
6164
import org.eclipse.jetty.server.handler.HandlerWrapper;
65+
import org.eclipse.jetty.servlet.FilterHolder;
6266
import org.eclipse.jetty.servlet.ServletContextHandler;
6367
import org.eclipse.jetty.servlet.ServletHolder;
6468
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -324,6 +328,7 @@ private void startServer(boolean join) throws Exception {
324328
ServletHolder servletHolder = new ServletHolder(servlet);
325329
servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement(""));
326330
servletContextHandler.addServlet(servletHolder, "/*");
331+
servletContextHandler = addTimerFilterForRequestTimeout(servletContextHandler);
327332

328333
server.start();
329334
logServerInfo();
@@ -393,6 +398,18 @@ private HttpServlet servletForDeducedSignatureType(Class<?> functionClass) {
393398
throw new RuntimeException(error);
394399
}
395400

401+
private ServletContextHandler addTimerFilterForRequestTimeout(
402+
ServletContextHandler servletContextHandler) {
403+
String timeoutSeconds = System.getenv("CLOUD_RUN_TIMEOUT_SECONDS");
404+
if (timeoutSeconds == null) {
405+
return servletContextHandler;
406+
}
407+
int seconds = Integer.parseInt(timeoutSeconds);
408+
FilterHolder holder = new FilterHolder(new TimeoutFilter(seconds));
409+
servletContextHandler.addFilter(holder, "/*", EnumSet.of(DispatcherType.REQUEST));
410+
return servletContextHandler;
411+
}
412+
396413
static URL[] classpathToUrls(String classpath) {
397414
String[] components = classpath.split(File.pathSeparator);
398415
List<URL> urls = new ArrayList<>();

invoker/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import java.time.OffsetDateTime;
5252
import java.time.ZoneOffset;
5353
import java.util.Arrays;
54+
import java.util.Collections;
5455
import java.util.List;
5556
import java.util.Map;
5657
import java.util.Optional;
@@ -252,6 +253,30 @@ public void helloWorld() throws Exception {
252253
ROBOTS_TXT_TEST_CASE));
253254
}
254255

256+
@Test
257+
public void timeoutHttpSuccess() throws Exception {
258+
testFunction(
259+
SignatureType.HTTP,
260+
fullTarget("TimeoutHttp"),
261+
ImmutableList.of(),
262+
ImmutableList.of(
263+
TestCase.builder()
264+
.setExpectedResponseText("finished\n")
265+
.setExpectedResponseText(Optional.empty())
266+
.build()),
267+
ImmutableMap.of("CLOUD_RUN_TIMEOUT_SECONDS", "3"));
268+
}
269+
270+
@Test
271+
public void timeoutHttpTimesOut() throws Exception {
272+
testFunction(
273+
SignatureType.HTTP,
274+
fullTarget("TimeoutHttp"),
275+
ImmutableList.of(),
276+
ImmutableList.of(TestCase.builder().setExpectedResponseCode(408).build()),
277+
ImmutableMap.of("CLOUD_RUN_TIMEOUT_SECONDS", "1"));
278+
}
279+
255280
@Test
256281
public void exceptionHttp() throws Exception {
257282
String exceptionExpectedOutput =
@@ -290,7 +315,8 @@ public void exceptionBackground() throws Exception {
290315
.setRequestText(gcfRequestText)
291316
.setExpectedResponseCode(500)
292317
.setExpectedOutput(exceptionExpectedOutput)
293-
.build()));
318+
.build()),
319+
Collections.emptyMap());
294320
}
295321

296322
@Test
@@ -400,7 +426,8 @@ public void typedFunction() throws Exception {
400426
TestCase.builder()
401427
.setRequestText(originalJson)
402428
.setExpectedResponseText("{\"fullName\":\"JohnDoe\"}")
403-
.build()));
429+
.build()),
430+
Collections.emptyMap());
404431
}
405432

406433
@Test
@@ -410,7 +437,8 @@ public void typedVoidFunction() throws Exception {
410437
fullTarget("TypedVoid"),
411438
ImmutableList.of(),
412439
ImmutableList.of(
413-
TestCase.builder().setRequestText("{}").setExpectedResponseCode(204).build()));
440+
TestCase.builder().setRequestText("{}").setExpectedResponseCode(204).build()),
441+
Collections.emptyMap());
414442
}
415443

416444
@Test
@@ -424,7 +452,8 @@ public void typedCustomFormat() throws Exception {
424452
.setRequestText("abc\n123\n$#@\n")
425453
.setExpectedResponseText("abc123$#@")
426454
.setExpectedResponseCode(200)
427-
.build()));
455+
.build()),
456+
Collections.emptyMap());
428457
}
429458

430459
private void backgroundTest(String target) throws Exception {
@@ -595,7 +624,8 @@ public void classpathOptionHttp() throws Exception {
595624
SignatureType.HTTP,
596625
"com.example.functionjar.Foreground",
597626
ImmutableList.of("--classpath", functionJarString()),
598-
ImmutableList.of(testCase));
627+
ImmutableList.of(testCase),
628+
Collections.emptyMap());
599629
}
600630

601631
/** Like {@link #classpathOptionHttp} but for background functions. */
@@ -612,7 +642,8 @@ public void classpathOptionBackground() throws Exception {
612642
SignatureType.BACKGROUND,
613643
"com.example.functionjar.Background",
614644
ImmutableList.of("--classpath", functionJarString()),
615-
ImmutableList.of(TestCase.builder().setRequestText(json.toString()).build()));
645+
ImmutableList.of(TestCase.builder().setRequestText(json.toString()).build()),
646+
Collections.emptyMap());
616647
}
617648

618649
/** Like {@link #classpathOptionHttp} but for typed functions. */
@@ -629,7 +660,8 @@ public void classpathOptionTyped() throws Exception {
629660
TestCase.builder()
630661
.setRequestText(originalJson)
631662
.setExpectedResponseText("{\"fullName\":\"JohnDoe\"}")
632-
.build()));
663+
.build()),
664+
Collections.emptyMap());
633665
}
634666

635667
// In these tests, we test a number of different functions that express the same functionality
@@ -643,7 +675,12 @@ private void backgroundTest(
643675
for (TestCase testCase : testCases) {
644676
File snoopFile = testCase.snoopFile().get();
645677
snoopFile.delete();
646-
testFunction(signatureType, functionTarget, ImmutableList.of(), ImmutableList.of(testCase));
678+
testFunction(
679+
signatureType,
680+
functionTarget,
681+
ImmutableList.of(),
682+
ImmutableList.of(testCase),
683+
Collections.emptyMap());
647684
String snooped = new String(Files.readAllBytes(snoopFile.toPath()), StandardCharsets.UTF_8);
648685
Gson gson = new Gson();
649686
JsonObject snoopedJson = gson.fromJson(snooped, JsonObject.class);
@@ -667,16 +704,18 @@ private void checkSnoopFile(TestCase testCase) throws IOException {
667704
}
668705

669706
private void testHttpFunction(String target, List<TestCase> testCases) throws Exception {
670-
testFunction(SignatureType.HTTP, target, ImmutableList.of(), testCases);
707+
testFunction(SignatureType.HTTP, target, ImmutableList.of(), testCases, Collections.emptyMap());
671708
}
672709

673710
private void testFunction(
674711
SignatureType signatureType,
675712
String target,
676713
ImmutableList<String> extraArgs,
677-
List<TestCase> testCases)
714+
List<TestCase> testCases,
715+
Map<String, String> environmentVariables)
678716
throws Exception {
679-
ServerProcess serverProcess = startServer(signatureType, target, extraArgs);
717+
ServerProcess serverProcess =
718+
startServer(signatureType, target, extraArgs, environmentVariables);
680719
try {
681720
HttpClient httpClient = new HttpClient();
682721
httpClient.start();
@@ -772,7 +811,10 @@ public void close() {
772811
}
773812

774813
private ServerProcess startServer(
775-
SignatureType signatureType, String target, ImmutableList<String> extraArgs)
814+
SignatureType signatureType,
815+
String target,
816+
ImmutableList<String> extraArgs,
817+
Map<String, String> environmentVariables)
776818
throws IOException, InterruptedException {
777819
File javaHome = new File(System.getProperty("java.home"));
778820
assertThat(javaHome.exists()).isTrue();
@@ -798,6 +840,7 @@ private ServerProcess startServer(
798840
"FUNCTION_TARGET",
799841
target);
800842
processBuilder.environment().putAll(environment);
843+
processBuilder.environment().putAll(environmentVariables);
801844
Process serverProcess = processBuilder.start();
802845
CountDownLatch ready = new CountDownLatch(1);
803846
StringBuilder output = new StringBuilder();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.google.cloud.functions.invoker.testfunctions;
2+
3+
import com.google.cloud.functions.HttpFunction;
4+
import com.google.cloud.functions.HttpRequest;
5+
import com.google.cloud.functions.HttpResponse;
6+
7+
public class TimeoutHttp implements HttpFunction {
8+
9+
@Override
10+
public void service(HttpRequest request, HttpResponse response) throws Exception {
11+
try {
12+
Thread.sleep(2000);
13+
} catch (InterruptedException e) {
14+
response.getWriter().close();
15+
}
16+
response.getWriter().write("finished\n");
17+
}
18+
}

0 commit comments

Comments
 (0)