Skip to content

Commit ba8716e

Browse files
committed
Provide option to timeout functions earlier than configured
A header of X-Timeout can be passed to reduce the value of exec_timeout. The consumer invoking the function is responsible for tuning this value and making sure it has enough headroom. The change was requested by @kevin-lindsay-1 for functions which process batched data of varying size and could run for 24 hours or 2 minutes, but need to timeout in a timely fashion, according to the input. Signed-off-by: Alex Ellis (OpenFaaS Ltd) <[email protected]>
1 parent 3d089de commit ba8716e

File tree

3 files changed

+75
-2
lines changed

3 files changed

+75
-2
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ In HTTP mode, the watchdog will append the X-Call-Id to its own HTTP log message
128128
2024/04/25 17:29:58 GET / - 301 Moved Permanently - ContentLength: 39B (0.0037s) [079d9ff9-d7b7-4e37-b195-5ad520e6f797]
129129
```
130130

131+
#### 1.5 Reducing timeouts
132+
133+
If a function has a timeout set via `exec_timeout` of a large value like `1h`, but you need an individual request to timeout earlier, i.e. `1m`, then you can pass in a HTTP header of `X-Timeout` with a Go duration to override the behaviour.
134+
135+
The value for `X-Timeout` must be equal to or shorter than the `exec_timeout` environment variable.
136+
137+
`X-Timeout` cannot be set when the `exec_timeout` is set to `0` or hasn't been specified.
138+
131139
### 2. Serializing fork (mode=serializing)
132140

133141
#### 2.1 Status

executor/http_runner.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,13 @@ func (f *HTTPFunctionRunner) Run(req FunctionRequest, contentLength int64, r *ht
118118
request.Host = r.Host
119119
copyHeaders(request.Header, &r.Header)
120120

121+
execTimeout := getTimeout(r, f.ExecTimeout)
122+
121123
var reqCtx context.Context
122124
var cancel context.CancelFunc
123125

124-
if f.ExecTimeout.Nanoseconds() > 0 {
125-
reqCtx, cancel = context.WithTimeout(r.Context(), f.ExecTimeout)
126+
if execTimeout.Nanoseconds() > 0 {
127+
reqCtx, cancel = context.WithTimeout(r.Context(), execTimeout)
126128
} else {
127129
reqCtx = r.Context()
128130
cancel = func() {
@@ -203,6 +205,20 @@ func (f *HTTPFunctionRunner) Run(req FunctionRequest, contentLength int64, r *ht
203205
return nil
204206
}
205207

208+
func getTimeout(r *http.Request, defaultTimeout time.Duration) time.Duration {
209+
execTimeout := defaultTimeout
210+
if v := r.Header.Get("X-Timeout"); len(v) > 0 {
211+
dur, err := time.ParseDuration(v)
212+
if err == nil {
213+
if dur <= defaultTimeout {
214+
execTimeout = dur
215+
}
216+
}
217+
}
218+
219+
return execTimeout
220+
}
221+
206222
func copyHeaders(destination http.Header, source *http.Header) {
207223
for k, v := range *source {
208224
vClone := make([]string, len(v))

executor/http_runner_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package executor
2+
3+
import (
4+
"net/http/httptest"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestGetTimeout_PicksLowerOverride(t *testing.T) {
10+
override := time.Second * 10
11+
r := httptest.NewRequest("GET", "http://localhost:8080", nil)
12+
r.Header.Add("X-Timeout", override.Round(time.Second).String())
13+
14+
defaultTimeout := time.Second * 20
15+
got := getTimeout(r, defaultTimeout)
16+
want := time.Second * 10
17+
18+
if got != want {
19+
t.Errorf("getTimeout() got: %v, want %v", got, want)
20+
}
21+
}
22+
23+
func TestGetTimeout_CapsOverrideToDefaultValue(t *testing.T) {
24+
override := time.Second * 21
25+
r := httptest.NewRequest("GET", "http://localhost:8080", nil)
26+
r.Header.Add("X-Timeout", override.Round(time.Second).String())
27+
28+
defaultTimeout := time.Second * 20
29+
got := getTimeout(r, defaultTimeout)
30+
want := time.Second * 20
31+
32+
if got != want {
33+
t.Errorf("getTimeout() got: %v, want %v", got, want)
34+
}
35+
}
36+
37+
func TestGetTimeout_NoDefaultMeansNoOverride(t *testing.T) {
38+
override := time.Second * 10
39+
r := httptest.NewRequest("GET", "http://localhost:8080", nil)
40+
r.Header.Add("X-Timeout", override.Round(time.Second).String())
41+
42+
defaultTimeout := time.Nanosecond * 0
43+
got := getTimeout(r, defaultTimeout)
44+
want := time.Nanosecond * 0
45+
46+
if got != want {
47+
t.Errorf("getTimeout() got: %v, want %v", got, want)
48+
}
49+
}

0 commit comments

Comments
 (0)