Skip to content

Commit 9e91f57

Browse files
authored
fix: server times out when specified by CLOUD_RUN_TIMEOUT_SECONDS (#275)
* fix: server times out when specified by CLOUD_RUN_TIMEOUT_SECONDS
1 parent dc9bc1f commit 9e91f57

File tree

4 files changed

+165
-12
lines changed

4 files changed

+165
-12
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 java.util.logging.Logger;
21+
import javax.servlet.Filter;
22+
import javax.servlet.FilterChain;
23+
import javax.servlet.ServletException;
24+
import javax.servlet.ServletRequest;
25+
import javax.servlet.ServletResponse;
26+
import javax.servlet.http.HttpServletResponse;
27+
28+
public class TimeoutFilter implements Filter {
29+
30+
private static final Logger logger = Logger.getLogger(TimeoutFilter.class.getName());
31+
private final int timeoutMs;
32+
33+
public TimeoutFilter(int timeoutSeconds) {
34+
this.timeoutMs = timeoutSeconds * 1000; // Convert seconds to milliseconds
35+
}
36+
37+
@Override
38+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
39+
throws IOException, ServletException {
40+
Timer timer = new Timer(true);
41+
TimerTask timeoutTask =
42+
new TimerTask() {
43+
@Override
44+
public void run() {
45+
if (response instanceof HttpServletResponse) {
46+
try {
47+
((HttpServletResponse) response)
48+
.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, "Request timed out");
49+
} catch (IOException e) {
50+
logger.warning("Error while sending HTTP response: " + e.toString());
51+
}
52+
} else {
53+
try {
54+
response.getWriter().write("Request timed out");
55+
} catch (IOException e) {
56+
logger.warning("Error while writing response: " + e.toString());
57+
}
58+
}
59+
}
60+
};
61+
62+
timer.schedule(timeoutTask, timeoutMs);
63+
64+
try {
65+
chain.doFilter(request, response);
66+
timeoutTask.cancel();
67+
} finally {
68+
timer.purge();
69+
}
70+
}
71+
}

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: 59 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,34 @@ 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(
277+
TestCase.builder()
278+
.setExpectedResponseCode(408)
279+
.setExpectedResponseText(Optional.empty())
280+
.build()),
281+
ImmutableMap.of("CLOUD_RUN_TIMEOUT_SECONDS", "1"));
282+
}
283+
255284
@Test
256285
public void exceptionHttp() throws Exception {
257286
String exceptionExpectedOutput =
@@ -290,7 +319,8 @@ public void exceptionBackground() throws Exception {
290319
.setRequestText(gcfRequestText)
291320
.setExpectedResponseCode(500)
292321
.setExpectedOutput(exceptionExpectedOutput)
293-
.build()));
322+
.build()),
323+
Collections.emptyMap());
294324
}
295325

296326
@Test
@@ -400,7 +430,8 @@ public void typedFunction() throws Exception {
400430
TestCase.builder()
401431
.setRequestText(originalJson)
402432
.setExpectedResponseText("{\"fullName\":\"JohnDoe\"}")
403-
.build()));
433+
.build()),
434+
Collections.emptyMap());
404435
}
405436

406437
@Test
@@ -410,7 +441,8 @@ public void typedVoidFunction() throws Exception {
410441
fullTarget("TypedVoid"),
411442
ImmutableList.of(),
412443
ImmutableList.of(
413-
TestCase.builder().setRequestText("{}").setExpectedResponseCode(204).build()));
444+
TestCase.builder().setRequestText("{}").setExpectedResponseCode(204).build()),
445+
Collections.emptyMap());
414446
}
415447

416448
@Test
@@ -424,7 +456,8 @@ public void typedCustomFormat() throws Exception {
424456
.setRequestText("abc\n123\n$#@\n")
425457
.setExpectedResponseText("abc123$#@")
426458
.setExpectedResponseCode(200)
427-
.build()));
459+
.build()),
460+
Collections.emptyMap());
428461
}
429462

430463
private void backgroundTest(String target) throws Exception {
@@ -595,7 +628,8 @@ public void classpathOptionHttp() throws Exception {
595628
SignatureType.HTTP,
596629
"com.example.functionjar.Foreground",
597630
ImmutableList.of("--classpath", functionJarString()),
598-
ImmutableList.of(testCase));
631+
ImmutableList.of(testCase),
632+
Collections.emptyMap());
599633
}
600634

601635
/** Like {@link #classpathOptionHttp} but for background functions. */
@@ -612,7 +646,8 @@ public void classpathOptionBackground() throws Exception {
612646
SignatureType.BACKGROUND,
613647
"com.example.functionjar.Background",
614648
ImmutableList.of("--classpath", functionJarString()),
615-
ImmutableList.of(TestCase.builder().setRequestText(json.toString()).build()));
649+
ImmutableList.of(TestCase.builder().setRequestText(json.toString()).build()),
650+
Collections.emptyMap());
616651
}
617652

618653
/** Like {@link #classpathOptionHttp} but for typed functions. */
@@ -629,7 +664,8 @@ public void classpathOptionTyped() throws Exception {
629664
TestCase.builder()
630665
.setRequestText(originalJson)
631666
.setExpectedResponseText("{\"fullName\":\"JohnDoe\"}")
632-
.build()));
667+
.build()),
668+
Collections.emptyMap());
633669
}
634670

635671
// In these tests, we test a number of different functions that express the same functionality
@@ -643,7 +679,12 @@ private void backgroundTest(
643679
for (TestCase testCase : testCases) {
644680
File snoopFile = testCase.snoopFile().get();
645681
snoopFile.delete();
646-
testFunction(signatureType, functionTarget, ImmutableList.of(), ImmutableList.of(testCase));
682+
testFunction(
683+
signatureType,
684+
functionTarget,
685+
ImmutableList.of(),
686+
ImmutableList.of(testCase),
687+
Collections.emptyMap());
647688
String snooped = new String(Files.readAllBytes(snoopFile.toPath()), StandardCharsets.UTF_8);
648689
Gson gson = new Gson();
649690
JsonObject snoopedJson = gson.fromJson(snooped, JsonObject.class);
@@ -667,16 +708,18 @@ private void checkSnoopFile(TestCase testCase) throws IOException {
667708
}
668709

669710
private void testHttpFunction(String target, List<TestCase> testCases) throws Exception {
670-
testFunction(SignatureType.HTTP, target, ImmutableList.of(), testCases);
711+
testFunction(SignatureType.HTTP, target, ImmutableList.of(), testCases, Collections.emptyMap());
671712
}
672713

673714
private void testFunction(
674715
SignatureType signatureType,
675716
String target,
676717
ImmutableList<String> extraArgs,
677-
List<TestCase> testCases)
718+
List<TestCase> testCases,
719+
Map<String, String> environmentVariables)
678720
throws Exception {
679-
ServerProcess serverProcess = startServer(signatureType, target, extraArgs);
721+
ServerProcess serverProcess =
722+
startServer(signatureType, target, extraArgs, environmentVariables);
680723
try {
681724
HttpClient httpClient = new HttpClient();
682725
httpClient.start();
@@ -772,7 +815,10 @@ public void close() {
772815
}
773816

774817
private ServerProcess startServer(
775-
SignatureType signatureType, String target, ImmutableList<String> extraArgs)
818+
SignatureType signatureType,
819+
String target,
820+
ImmutableList<String> extraArgs,
821+
Map<String, String> environmentVariables)
776822
throws IOException, InterruptedException {
777823
File javaHome = new File(System.getProperty("java.home"));
778824
assertThat(javaHome.exists()).isTrue();
@@ -798,6 +844,7 @@ private ServerProcess startServer(
798844
"FUNCTION_TARGET",
799845
target);
800846
processBuilder.environment().putAll(environment);
847+
processBuilder.environment().putAll(environmentVariables);
801848
Process serverProcess = processBuilder.start();
802849
CountDownLatch ready = new CountDownLatch(1);
803850
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)