Skip to content

Commit 298bc02

Browse files
authored
fix: cancel request context when timeout exceeded (#244)
* feat: requests timeout respecting CLOUD_RUN_TIMEOUT_SECONDS * add test coverage * fix windows test
1 parent ac7db72 commit 298bc02

File tree

3 files changed

+150
-1
lines changed

3 files changed

+150
-1
lines changed

funcframework/events.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ func convertBackgroundToCloudEvent(ceHandler http.Handler) http.Handler {
192192
return
193193
}
194194
}
195+
r, cancel := setContextTimeoutIfRequested(r)
196+
if cancel != nil {
197+
defer cancel()
198+
}
195199
ceHandler.ServeHTTP(w, r)
196200
})
197201
}

funcframework/framework.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525
"os"
2626
"reflect"
2727
"runtime/debug"
28+
"strconv"
2829
"strings"
30+
"time"
2931

3032
"github.com/GoogleCloudPlatform/functions-framework-go/internal/registry"
3133
cloudevents "github.com/cloudevents/sdk-go/v2"
@@ -196,6 +198,10 @@ func wrapHTTPFunction(fn func(http.ResponseWriter, *http.Request)) (http.Handler
196198
defer fmt.Println()
197199
defer fmt.Fprintln(os.Stderr)
198200
}
201+
r, cancel := setContextTimeoutIfRequested(r)
202+
if cancel != nil {
203+
defer cancel()
204+
}
199205
defer recoverPanic(w, "user function execution", false)
200206
fn(w, r)
201207
}), nil
@@ -212,7 +218,10 @@ func wrapEventFunction(fn interface{}) (http.Handler, error) {
212218
defer fmt.Println()
213219
defer fmt.Fprintln(os.Stderr)
214220
}
215-
221+
r, cancel := setContextTimeoutIfRequested(r)
222+
if cancel != nil {
223+
defer cancel()
224+
}
216225
if shouldConvertCloudEventToBackgroundRequest(r) {
217226
if err := convertCloudEventToBackgroundRequest(r); err != nil {
218227
writeHTTPErrorResponse(w, http.StatusBadRequest, crashStatus, fmt.Sprintf("error converting CloudEvent to Background Event: %v", err))
@@ -388,3 +397,18 @@ func writeHTTPErrorResponse(w http.ResponseWriter, statusCode int, status, msg s
388397
w.WriteHeader(statusCode)
389398
fmt.Fprint(w, msg)
390399
}
400+
401+
// setContextTimeoutIfRequested replaces the request's context with a cancellation if requested
402+
func setContextTimeoutIfRequested(r *http.Request) (*http.Request, func()) {
403+
timeoutStr := os.Getenv("CLOUD_RUN_TIMEOUT_SECONDS")
404+
if timeoutStr == "" {
405+
return r, nil
406+
}
407+
timeoutSecs, err := strconv.Atoi(timeoutStr)
408+
if err != nil {
409+
fmt.Fprintf(os.Stderr, "Could not parse CLOUD_RUN_TIMEOUT_SECONDS as an integer value in seconds: %v\n", err)
410+
return r, nil
411+
}
412+
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSecs)*time.Second)
413+
return r.WithContext(ctx), cancel
414+
}

funcframework/framework_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ import (
2525
"os"
2626
"strings"
2727
"testing"
28+
"time"
2829

2930
"github.com/GoogleCloudPlatform/functions-framework-go/functions"
3031
"github.com/GoogleCloudPlatform/functions-framework-go/internal/registry"
3132
cloudevents "github.com/cloudevents/sdk-go/v2"
33+
"github.com/cloudevents/sdk-go/v2/event"
3234
"github.com/google/go-cmp/cmp"
3335
)
3436

@@ -995,6 +997,125 @@ func TestServeMultipleFunctions(t *testing.T) {
995997
}
996998
}
997999

1000+
func TestHTTPRequestTimeout(t *testing.T) {
1001+
timeoutEnvVar := "CLOUD_RUN_TIMEOUT_SECONDS"
1002+
prev := os.Getenv(timeoutEnvVar)
1003+
defer os.Setenv(timeoutEnvVar, prev)
1004+
1005+
cloudeventsJSON := []byte(`{
1006+
"specversion" : "1.0",
1007+
"type" : "com.github.pull.create",
1008+
"source" : "https://github.com/cloudevents/spec/pull",
1009+
"subject" : "123",
1010+
"id" : "A234-1234-1234",
1011+
"time" : "2018-04-05T17:31:00Z",
1012+
"comexampleextension1" : "value",
1013+
"datacontenttype" : "application/xml",
1014+
"data" : "<much wow=\"xml\"/>"
1015+
}`)
1016+
1017+
tcs := []struct {
1018+
name string
1019+
wantDeadline bool
1020+
waitForExpiration bool
1021+
timeout string
1022+
}{
1023+
{
1024+
name: "deadline not requested",
1025+
wantDeadline: false,
1026+
waitForExpiration: false,
1027+
timeout: "",
1028+
},
1029+
{
1030+
name: "NaN deadline",
1031+
wantDeadline: false,
1032+
waitForExpiration: false,
1033+
timeout: "aaa",
1034+
},
1035+
{
1036+
name: "very long deadline",
1037+
wantDeadline: true,
1038+
waitForExpiration: false,
1039+
timeout: "3600",
1040+
},
1041+
{
1042+
name: "short deadline should terminate",
1043+
wantDeadline: true,
1044+
waitForExpiration: true,
1045+
timeout: "1",
1046+
},
1047+
}
1048+
1049+
for _, tc := range tcs {
1050+
t.Run(tc.name, func(t *testing.T) {
1051+
defer cleanup()
1052+
os.Setenv(timeoutEnvVar, tc.timeout)
1053+
1054+
var httpReqCtx context.Context
1055+
functions.HTTP("http", func(w http.ResponseWriter, r *http.Request) {
1056+
if tc.waitForExpiration {
1057+
<-r.Context().Done()
1058+
}
1059+
httpReqCtx = r.Context()
1060+
})
1061+
var ceReqCtx context.Context
1062+
functions.CloudEvent("cloudevent", func(ctx context.Context, event event.Event) error {
1063+
if tc.waitForExpiration {
1064+
<-ctx.Done()
1065+
}
1066+
ceReqCtx = ctx
1067+
return nil
1068+
})
1069+
server, err := initServer()
1070+
if err != nil {
1071+
t.Fatalf("initServer(): %v", err)
1072+
}
1073+
srv := httptest.NewServer(server)
1074+
defer srv.Close()
1075+
1076+
t.Run("http", func(t *testing.T) {
1077+
_, err = http.Get(srv.URL + "/http")
1078+
if err != nil {
1079+
t.Fatalf("expected success")
1080+
}
1081+
if httpReqCtx == nil {
1082+
t.Fatalf("expected non-nil request context")
1083+
}
1084+
deadline, ok := httpReqCtx.Deadline()
1085+
if ok != tc.wantDeadline {
1086+
t.Errorf("expected deadline %v but got %v", tc.wantDeadline, ok)
1087+
}
1088+
if expired := deadline.Before(time.Now()); ok && expired != tc.waitForExpiration {
1089+
t.Errorf("expected expired %v but got %v", tc.waitForExpiration, expired)
1090+
}
1091+
})
1092+
1093+
t.Run("cloudevent", func(t *testing.T) {
1094+
req, err := http.NewRequest("POST", srv.URL+"/cloudevent", bytes.NewBuffer(cloudeventsJSON))
1095+
if err != nil {
1096+
t.Fatalf("failed to create request")
1097+
}
1098+
req.Header.Add("Content-Type", "application/cloudevents+json")
1099+
client := &http.Client{}
1100+
_, err = client.Do(req)
1101+
if err != nil {
1102+
t.Fatalf("request failed")
1103+
}
1104+
if ceReqCtx == nil {
1105+
t.Fatalf("expected non-nil request context")
1106+
}
1107+
deadline, ok := ceReqCtx.Deadline()
1108+
if ok != tc.wantDeadline {
1109+
t.Errorf("expected deadline %v but got %v", tc.wantDeadline, ok)
1110+
}
1111+
if expired := deadline.Before(time.Now()); ok && expired != tc.waitForExpiration {
1112+
t.Errorf("expected expired %v but got %v", tc.waitForExpiration, expired)
1113+
}
1114+
})
1115+
})
1116+
}
1117+
}
1118+
9981119
func cleanup() {
9991120
os.Unsetenv("FUNCTION_TARGET")
10001121
registry.Default().Reset()

0 commit comments

Comments
 (0)