Skip to content

Commit 04bd0cd

Browse files
committed
Allow configuration of watchdog from env-vars
Signed-off-by: Alex Ellis <[email protected]>
1 parent 5901ef3 commit 04bd0cd

File tree

7 files changed

+258
-41
lines changed

7 files changed

+258
-41
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@ This is a re-write of the OpenFaaS watchdog.
1010

1111
![](https://camo.githubusercontent.com/61c169ab5cd01346bc3dc7a11edc1d218f0be3b4/68747470733a2f2f7062732e7477696d672e636f6d2f6d656469612f4447536344626c554941416f34482d2e6a70673a6c61726765)
1212

13+
## Config
14+
15+
Environmental variables:
16+
17+
| Option | Implemented | Usage |
18+
|------------------------|--------------|-------------------------------|
19+
| `function_process` | Yes | The process to invoke for each function call function process (alias - fprocess). This must be a UNIX binary and accept input via STDIN and output via STDOUT. |
20+
| `read_timeout` | Yes | HTTP timeout for reading the payload from the client caller (in seconds) |
21+
| `write_timeout` | Yes | HTTP timeout for writing a response body from your function (in seconds) |
22+
| `hard_timeout` | Yes | Hard timeout for process exec'd for each incoming request (in seconds). Disabled if set to 0. |
23+
| `port` | Yes | Specify an alternative TCP port fo testing |
24+
| `write_debug` | No | Write all output, error messages, and additional information to the logs. Default is false. |
25+
| `content_type` | No | Force a specific Content-Type response for all responses. |
26+
| `suppress_lock` | No | The watchdog will attempt to write a lockfile to /tmp/ for swarm healthchecks - set this to true to disable behaviour. |
27+
28+
> Note: the .lock file is implemented for health-checking, but cannot be disabled yet.
29+
1330
## Watchdog modes:
1431

1532
The original watchdog supported mode 3 Serializing fork and has support for mode 2 Afterburn in an open PR.
@@ -24,6 +41,8 @@ Forks a process per request and can deal with more data than is available memory
2441

2542
HTTP headers cannot be sent after function starts executing due to input/output being hooked-up directly to response for streaming efficiencies. Response code is always 200 unless there is an issue forking the process. An error mid-flight will have to be picked up on the client. Multi-threaded.
2643

44+
* Input is sent back to client as soon as it's printed to stdout by the executing process.
45+
2746
* A static Content-type can be set ahead of time.
2847

2948
* Hard timeout: supported.

config/config.go

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,25 @@ package config
22

33
import (
44
"fmt"
5-
"os"
5+
"strconv"
66
"strings"
77
"time"
88
)
99

10+
// WatchdogConfig configuration for a watchdog.
1011
type WatchdogConfig struct {
1112
TCPPort int
1213
HTTPReadTimeout time.Duration
1314
HTTPWriteTimeout time.Duration
15+
HardTimeout time.Duration
16+
1417
FunctionProcess string
18+
ContentType string
1519
InjectCGIHeaders bool
16-
HardTimeout time.Duration
1720
OperationalMode int
1821
}
1922

23+
// Process returns a string for the process and a slice for the arguments from the FunctionProcess.
2024
func (w WatchdogConfig) Process() (string, []string) {
2125
parts := strings.Split(w.FunctionProcess, " ")
2226

@@ -27,18 +31,36 @@ func (w WatchdogConfig) Process() (string, []string) {
2731
return parts[0], []string{}
2832
}
2933

34+
// New create config based upon environmental variables.
3035
func New(env []string) (WatchdogConfig, error) {
36+
37+
envMap := mapEnv(env)
38+
39+
var functionProcess string
40+
if val, exists := envMap["fprocess"]; exists {
41+
functionProcess = val
42+
}
43+
44+
if val, exists := envMap["function_process"]; exists {
45+
functionProcess = val
46+
}
47+
48+
contentType := "application/octet-stream"
49+
if val, exists := envMap["content_type"]; exists {
50+
contentType = val
51+
}
52+
3153
config := WatchdogConfig{
32-
TCPPort: 8080,
33-
HTTPReadTimeout: time.Second * 10,
34-
HTTPWriteTimeout: time.Second * 10,
35-
FunctionProcess: os.Getenv("fprocess"),
54+
TCPPort: getInt(envMap, "port", 8080),
55+
HTTPReadTimeout: getDuration(envMap, "read_timeout", time.Second*10),
56+
HTTPWriteTimeout: getDuration(envMap, "write_timeout", time.Second*10),
57+
FunctionProcess: functionProcess,
3658
InjectCGIHeaders: true,
37-
HardTimeout: 5 * time.Second,
59+
HardTimeout: getDuration(envMap, "hard_timeout", time.Second*10),
3860
OperationalMode: ModeStreaming,
61+
ContentType: contentType,
3962
}
4063

41-
envMap := mapEnv(env)
4264
if val := envMap["mode"]; len(val) > 0 {
4365
config.OperationalMode = WatchdogModeConst(val)
4466
}
@@ -60,34 +82,24 @@ func mapEnv(env []string) map[string]string {
6082
return mapped
6183
}
6284

63-
const (
64-
ModeStreaming = 1
65-
ModeSerializing = 2
66-
ModeAfterBurn = 3
67-
)
85+
func getDuration(env map[string]string, key string, defaultValue time.Duration) time.Duration {
86+
result := defaultValue
87+
if val, exists := env[key]; exists {
88+
parsed, _ := time.ParseDuration(val)
89+
result = parsed
6890

69-
func WatchdogModeConst(mode string) int {
70-
switch mode {
71-
case "streaming":
72-
return ModeStreaming
73-
case "afterburn":
74-
return ModeAfterBurn
75-
case "serializing":
76-
return ModeSerializing
77-
default:
78-
return 0
7991
}
92+
93+
return result
8094
}
8195

82-
func WatchdogMode(mode int) string {
83-
switch mode {
84-
case ModeStreaming:
85-
return "streaming"
86-
case ModeAfterBurn:
87-
return "afterburn"
88-
case ModeSerializing:
89-
return "serializing"
90-
default:
91-
return "unknown"
96+
func getInt(env map[string]string, key string, defaultValue int) int {
97+
result := defaultValue
98+
if val, exists := env[key]; exists {
99+
parsed, _ := strconv.Atoi(val)
100+
result = parsed
101+
92102
}
103+
104+
return result
93105
}

config/config_modes.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package config
2+
3+
const (
4+
// ModeStreaming streams the values live to the caller as they are printed by the process.
5+
ModeStreaming = 1
6+
7+
// ModeSerializing reads all the response and buffers before returning
8+
ModeSerializing = 2
9+
10+
// ModeAfterBurn for performance tuning
11+
ModeAfterBurn = 3
12+
)
13+
14+
// WatchdogModeConst as a const int
15+
func WatchdogModeConst(mode string) int {
16+
switch mode {
17+
case "streaming":
18+
return ModeStreaming
19+
case "afterburn":
20+
return ModeAfterBurn
21+
case "serializing":
22+
return ModeSerializing
23+
default:
24+
return 0
25+
}
26+
}
27+
28+
// WatchdogMode as a string
29+
func WatchdogMode(mode int) string {
30+
switch mode {
31+
case ModeStreaming:
32+
return "streaming"
33+
case ModeAfterBurn:
34+
return "afterburn"
35+
case ModeSerializing:
36+
return "serializing"
37+
default:
38+
return "unknown"
39+
}
40+
}

config/config_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import "testing"
4+
import "time"
45

56
func TestNew(t *testing.T) {
67
defaults, err := New([]string{})
@@ -35,4 +36,135 @@ func Test_OperationalMode_AfterBurn(t *testing.T) {
3536
if actual.OperationalMode != ModeAfterBurn {
3637
t.Errorf("Want %s. got: %s", WatchdogMode(ModeAfterBurn), WatchdogMode(actual.OperationalMode))
3738
}
39+
40+
}
41+
42+
func Test_ContentType_Default(t *testing.T) {
43+
env := []string{}
44+
45+
actual, err := New(env)
46+
if err != nil {
47+
t.Errorf("Expected no errors")
48+
}
49+
50+
if actual.ContentType != "application/octet-stream" {
51+
t.Errorf("Default (ContentType) Want %s. got: %s", actual.ContentType, "octet-stream")
52+
}
53+
}
54+
55+
func Test_ContentType_Override(t *testing.T) {
56+
env := []string{
57+
"content_type=application/json",
58+
}
59+
60+
actual, err := New(env)
61+
if err != nil {
62+
t.Errorf("Expected no errors")
63+
}
64+
65+
if actual.ContentType != "application/json" {
66+
t.Errorf("(ContentType) Want %s. got: %s", actual.ContentType, "application/json")
67+
}
68+
}
69+
70+
func Test_FunctionProcessLegacyName(t *testing.T) {
71+
env := []string{
72+
"fprocess=env",
73+
}
74+
75+
actual, err := New(env)
76+
if err != nil {
77+
t.Errorf("Expected no errors")
78+
}
79+
80+
if actual.FunctionProcess != "env" {
81+
t.Errorf("Want %s. got: %s", "env", actual.FunctionProcess)
82+
}
83+
}
84+
85+
func Test_FunctionProcessAlternativeName(t *testing.T) {
86+
env := []string{
87+
"function_process=env",
88+
}
89+
90+
actual, err := New(env)
91+
if err != nil {
92+
t.Errorf("Expected no errors")
93+
}
94+
95+
if actual.FunctionProcess != "env" {
96+
t.Errorf("Want %s. got: %s", "env", actual.FunctionProcess)
97+
}
98+
}
99+
100+
func Test_PortOverride(t *testing.T) {
101+
env := []string{
102+
"port=8081",
103+
}
104+
105+
actual, err := New(env)
106+
if err != nil {
107+
t.Errorf("Expected no errors")
108+
}
109+
110+
if actual.TCPPort != 8081 {
111+
t.Errorf("Want %s. got: %s", 8081, actual.TCPPort)
112+
}
113+
}
114+
115+
func Test_Timeouts(t *testing.T) {
116+
cases := []struct {
117+
readTimeout time.Duration
118+
writeTimeout time.Duration
119+
hardTimeout time.Duration
120+
env []string
121+
name string
122+
}{
123+
{
124+
name: "Defaults",
125+
readTimeout: time.Second * 10,
126+
writeTimeout: time.Second * 10,
127+
hardTimeout: time.Second * 10,
128+
env: []string{},
129+
},
130+
{
131+
name: "Custom read-timeout",
132+
readTimeout: time.Second * 5,
133+
writeTimeout: time.Second * 10,
134+
hardTimeout: time.Second * 10,
135+
env: []string{"read_timeout=5s"},
136+
},
137+
{
138+
name: "Custom write-timeout",
139+
readTimeout: time.Second * 10,
140+
writeTimeout: time.Second * 5,
141+
hardTimeout: time.Second * 10,
142+
env: []string{"write_timeout=5s"},
143+
},
144+
{
145+
name: "Custom hard-timeout",
146+
readTimeout: time.Second * 10,
147+
writeTimeout: time.Second * 10,
148+
hardTimeout: time.Second * 5,
149+
env: []string{"hard_timeout=5s"},
150+
},
151+
}
152+
153+
for _, testCase := range cases {
154+
actual, err := New(testCase.env)
155+
if err != nil {
156+
t.Errorf("(%s) Expected no errors", testCase.name)
157+
}
158+
if testCase.readTimeout != actual.HTTPReadTimeout {
159+
t.Errorf("(%s) HTTPReadTimeout want: %s, got: %s", testCase.name, actual.HTTPReadTimeout, testCase.readTimeout)
160+
}
161+
if testCase.writeTimeout != actual.HTTPWriteTimeout {
162+
t.Errorf("(%s) HTTPWriteTimeout want: %s, got: %s", testCase.name, actual.HTTPWriteTimeout, testCase.writeTimeout)
163+
}
164+
if testCase.hardTimeout != actual.HardTimeout {
165+
t.Errorf("(%s) HardTimeout want: %s, got: %s", testCase.name, actual.HardTimeout, testCase.hardTimeout)
166+
}
167+
168+
}
169+
38170
}

functions/serializing_fork_runner.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package functions
22

33
import (
4-
"fmt"
54
"io"
65
"io/ioutil"
76
"log"
@@ -26,6 +25,7 @@ func (f *SerializingForkFunctionRunner) Run(req FunctionRequest, w http.Response
2625
}
2726

2827
w.WriteHeader(200)
28+
2929
if functionBytes != nil {
3030
_, err = w.Write(*functionBytes)
3131
} else {
@@ -44,18 +44,22 @@ func serializeFunction(req FunctionRequest, f *SerializingForkFunctionRunner) (*
4444

4545
var timer *time.Timer
4646
if f.HardTimeout > time.Millisecond*0 {
47+
log.Println("Started a timer.")
48+
4749
timer = time.NewTimer(f.HardTimeout)
4850
go func() {
4951
<-timer.C
5052

51-
fmt.Printf("Function was killed by HardTimeout: %d\n", f.HardTimeout)
53+
log.Printf("Function was killed by HardTimeout: %s\n", f.HardTimeout)
5254
killErr := cmd.Process.Kill()
5355
if killErr != nil {
54-
fmt.Println("Error killing function due to HardTimeout", killErr)
56+
log.Println("Error killing function due to HardTimeout", killErr)
5557
}
5658
}()
5759
}
58-
defer timer.Stop()
60+
if timer != nil {
61+
defer timer.Stop()
62+
}
5963

6064
var data []byte
6165

0 commit comments

Comments
 (0)