Skip to content

Commit 6910e94

Browse files
committed
Add HTTP as a watchdog mode
Signed-off-by: Alex Ellis <[email protected]>
1 parent 4b0ee8f commit 6910e94

File tree

4 files changed

+251
-33
lines changed

4 files changed

+251
-33
lines changed

README.md

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,13 @@ 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-
3013
## Watchdog modes:
3114

32-
The original watchdog supported mode 3 Serializing fork and has support for mode 2 Afterburn in an open PR.
15+
History/context: the original watchdog supported mode the Serializing fork mode only and Afterburn was available for testing via a pull request.
3316

34-
When complete this work will support all three modes and additional stretch goal of:
17+
When the of-watchdog is complete this version will support four modes as listed below. We may consolidate or remove some of these modes before going to 1.0 so please consider modes 2-4 experimental.
3518

36-
* Handling of multi-part forms
37-
38-
### 1. Streaming fork (implemented) - default.
19+
### 1. Streaming fork (mode=streaming) - default.
3920

4021
Forks a process per request and can deal with more data than is available memory capacity - i.e. 512mb VM can process multiple GB of video.
4122

@@ -47,7 +28,7 @@ HTTP headers cannot be sent after function starts executing due to input/output
4728

4829
* Hard timeout: supported.
4930

50-
### 2. Afterburn (implemented)
31+
### 2. Afterburn (mode=afterburn)
5132

5233
Uses a single process for all requests, if that request dies the container dies.
5334

@@ -69,7 +50,41 @@ https://github.com/alexellis/python-afterburn
6950

7051
https://github.com/alexellis/java-afterburn
7152

72-
### 3. Serializing fork (implemented in dev-branch)
53+
### 3. HTTP (mode=http)
54+
55+
The HTTP mode is similar to AfterBurn.
56+
57+
A process is forked when the watchdog starts, we then forward any request incoming to the watchdog to a HTTP port within the container.
58+
59+
Pros:
60+
61+
* Fastest option - high concurrency and throughput
62+
63+
* Does not require new/custom client libraries like afterburn but makes use of a long-running daemon such as Express.js for Node or Flask for Python
64+
65+
Example usage for testing:
66+
67+
* Forward to an NGinx container:
68+
69+
```
70+
$ go build ; mode=http port=8081 fprocess="docker run -p 80:80 --name nginx -t nginx" upstream_url=http://127.0.0.1:80 ./of-watchdog
71+
```
72+
73+
* Forward to a Node.js / Express.js hello-world app:
74+
75+
```
76+
$ go build ; mode=http port=8081 fprocess="node expressjs-hello-world.js" upstream_url=http://127.0.0.1:3000 ./of-watchdog
77+
```
78+
79+
Cons:
80+
81+
* Questionable as to whether this is actually "serverless"
82+
83+
* Daemons such as express/flask/sinatra could be hard to configure or potentially unpredictable when used in this way
84+
85+
* One more HTTP hop in the chain between the client and the function
86+
87+
### 4. Serializing fork (mode=serializing)
7388

7489
Forks one process per request. Multi-threaded. Ideal for retro-fitting a CGI application handler i.e. for Flask.
7590

@@ -85,3 +100,20 @@ Reads entire request into memory from the HTTP request. At this point we seriali
85100

86101
* Hard timeout: supported.
87102

103+
## Configuration
104+
105+
Environmental variables:
106+
107+
| Option | Implemented | Usage |
108+
|------------------------|--------------|-------------------------------|
109+
| `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. |
110+
| `read_timeout` | Yes | HTTP timeout for reading the payload from the client caller (in seconds) |
111+
| `write_timeout` | Yes | HTTP timeout for writing a response body from your function (in seconds) |
112+
| `hard_timeout` | Yes | Hard timeout for process exec'd for each incoming request (in seconds). Disabled if set to 0. |
113+
| `port` | Yes | Specify an alternative TCP port fo testing |
114+
| `write_debug` | No | Write all output, error messages, and additional information to the logs. Default is false. |
115+
| `content_type` | Yes | Force a specific Content-Type response for all responses - only in forking/serializing modes. |
116+
| `suppress_lock` | No | The watchdog will attempt to write a lockfile to /tmp/ for swarm healthchecks - set this to true to disable behaviour. |
117+
| `upstream_url` | Yes | `http` mode only - where to forward requests i.e. 127.0.0.1:5000 |
118+
119+
> Note: the .lock file is implemented for health-checking, but cannot be disabled yet. You must create this file in /tmp/.

config/config_modes.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const (
99

1010
// ModeAfterBurn for performance tuning
1111
ModeAfterBurn = 3
12+
13+
//ModeHTTP for routing requests over HTTP
14+
ModeHTTP = 4
1215
)
1316

1417
// WatchdogModeConst as a const int
@@ -20,6 +23,8 @@ func WatchdogModeConst(mode string) int {
2023
return ModeAfterBurn
2124
case "serializing":
2225
return ModeSerializing
26+
case "http":
27+
return ModeHTTP
2328
default:
2429
return 0
2530
}
@@ -34,6 +39,8 @@ func WatchdogMode(mode int) string {
3439
return "afterburn"
3540
case ModeSerializing:
3641
return "serializing"
42+
case ModeHTTP:
43+
return "http"
3744
default:
3845
return "unknown"
3946
}

functions/http_runner.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package functions
2+
3+
import (
4+
"io"
5+
"io/ioutil"
6+
"log"
7+
"net"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"os/exec"
12+
"sync"
13+
"time"
14+
)
15+
16+
// HTTPFunctionRunner creates and maintains one process responsible for handling all calls
17+
type HTTPFunctionRunner struct {
18+
Process string
19+
ProcessArgs []string
20+
Command *exec.Cmd
21+
StdinPipe io.WriteCloser
22+
StdoutPipe io.ReadCloser
23+
Stderr io.Writer
24+
Mutex sync.Mutex
25+
Client *http.Client
26+
UpstreamURL *url.URL
27+
}
28+
29+
// Start forks the process used for processing incoming requests
30+
func (f *HTTPFunctionRunner) Start() error {
31+
cmd := exec.Command(f.Process, f.ProcessArgs...)
32+
33+
var stdinErr error
34+
var stdoutErr error
35+
36+
f.Command = cmd
37+
f.StdinPipe, stdinErr = cmd.StdinPipe()
38+
if stdinErr != nil {
39+
return stdinErr
40+
}
41+
42+
f.StdoutPipe, stdoutErr = cmd.StdoutPipe()
43+
if stdoutErr != nil {
44+
return stdoutErr
45+
}
46+
47+
errPipe, _ := cmd.StderrPipe()
48+
49+
// Prints stderr to console and is picked up by container logging driver.
50+
go func() {
51+
log.Println("Started logging stderr from function.")
52+
for {
53+
errBuff := make([]byte, 256)
54+
55+
_, err := errPipe.Read(errBuff)
56+
if err != nil {
57+
log.Fatalf("Error reading stderr: %s", err)
58+
59+
} else {
60+
log.Printf("stderr: %s", errBuff)
61+
}
62+
}
63+
}()
64+
65+
go func() {
66+
log.Println("Started logging stdout from function.")
67+
for {
68+
errBuff := make([]byte, 256)
69+
70+
_, err := f.StdoutPipe.Read(errBuff)
71+
if err != nil {
72+
log.Fatalf("Error reading stdout: %s", err)
73+
74+
} else {
75+
log.Printf("stdout: %s", errBuff)
76+
}
77+
}
78+
}()
79+
80+
dialTimeout := 3 * time.Second
81+
f.Client = makeProxyClient(dialTimeout)
82+
83+
urlValue, upstreamURLErr := url.Parse(os.Getenv("upstream_url"))
84+
if upstreamURLErr != nil {
85+
log.Fatal(upstreamURLErr)
86+
}
87+
88+
f.UpstreamURL = urlValue
89+
90+
return cmd.Start()
91+
}
92+
93+
// Run a function with a long-running process with a HTTP protocol for communication
94+
func (f *HTTPFunctionRunner) Run(req FunctionRequest, contentLength int64, r *http.Request, w http.ResponseWriter) error {
95+
96+
request, _ := http.NewRequest(r.Method, f.UpstreamURL.String(), r.Body)
97+
98+
res, err := f.Client.Do(request)
99+
100+
if err != nil {
101+
log.Println(err)
102+
}
103+
104+
for h := range res.Header {
105+
w.Header().Set(h, res.Header.Get(h))
106+
}
107+
108+
w.WriteHeader(res.StatusCode)
109+
if res.Body != nil {
110+
defer res.Body.Close()
111+
bodyBytes, bodyErr := ioutil.ReadAll(res.Body)
112+
if bodyErr != nil {
113+
log.Println("read body err", bodyErr)
114+
}
115+
w.Write(bodyBytes)
116+
}
117+
118+
log.Printf("%s %s - %s - ContentLength: %d", r.Method, r.RequestURI, res.Status, res.ContentLength)
119+
120+
return nil
121+
}
122+
123+
func makeProxyClient(dialTimeout time.Duration) *http.Client {
124+
proxyClient := http.Client{
125+
Transport: &http.Transport{
126+
Proxy: http.ProxyFromEnvironment,
127+
DialContext: (&net.Dialer{
128+
Timeout: dialTimeout,
129+
KeepAlive: 1,
130+
}).DialContext,
131+
MaxIdleConns: 1,
132+
DisableKeepAlives: true,
133+
IdleConnTimeout: 120 * time.Millisecond,
134+
ExpectContinueTimeout: 1500 * time.Millisecond,
135+
},
136+
}
137+
138+
return &proxyClient
139+
}

main.go

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,29 +32,40 @@ func main() {
3232
MaxHeaderBytes: 1 << 20, // Max header of 1MB
3333
}
3434

35+
requestHandler := buildRequestHandler(watchdogConfig)
36+
37+
log.Printf("OperationalMode: %s\n", config.WatchdogMode(watchdogConfig.OperationalMode))
38+
39+
if err := lock(); err != nil {
40+
log.Panic(err.Error())
41+
}
42+
43+
http.HandleFunc("/", requestHandler)
44+
log.Fatal(s.ListenAndServe())
45+
}
46+
47+
func buildRequestHandler(watchdogConfig config.WatchdogConfig) http.HandlerFunc {
3548
var requestHandler http.HandlerFunc
3649

3750
switch watchdogConfig.OperationalMode {
3851
case config.ModeStreaming:
39-
log.Println("OperationalMode: Streaming")
4052
requestHandler = makeForkRequestHandler(watchdogConfig)
4153
break
4254
case config.ModeSerializing:
43-
log.Println("OperationalMode: Serializing")
4455
requestHandler = makeSerializingForkRequestHandler(watchdogConfig)
4556
break
4657
case config.ModeAfterBurn:
47-
log.Println("OperationalMode: AfterBurn")
4858
requestHandler = makeAfterBurnRequestHandler(watchdogConfig)
4959
break
60+
case config.ModeHTTP:
61+
requestHandler = makeHTTPRequestHandler(watchdogConfig)
62+
break
63+
default:
64+
log.Panicf("unknown watchdog mode: %d", watchdogConfig.OperationalMode)
65+
break
5066
}
5167

52-
if err := lock(); err != nil {
53-
log.Panic(err.Error())
54-
}
55-
56-
http.HandleFunc("/", requestHandler)
57-
log.Fatal(s.ListenAndServe())
68+
return requestHandler
5869
}
5970

6071
func lock() error {
@@ -182,3 +193,32 @@ func getEnvironment(r *http.Request) []string {
182193

183194
return envs
184195
}
196+
197+
func makeHTTPRequestHandler(watchdogConfig config.WatchdogConfig) func(http.ResponseWriter, *http.Request) {
198+
commandName, arguments := watchdogConfig.Process()
199+
functionInvoker := functions.HTTPFunctionRunner{
200+
Process: commandName,
201+
ProcessArgs: arguments,
202+
}
203+
204+
fmt.Printf("Forking - %s %s\n", commandName, arguments)
205+
functionInvoker.Start()
206+
207+
return func(w http.ResponseWriter, r *http.Request) {
208+
209+
req := functions.FunctionRequest{
210+
Process: commandName,
211+
ProcessArgs: arguments,
212+
InputReader: r.Body,
213+
OutputWriter: w,
214+
}
215+
216+
err := functionInvoker.Run(req, r.ContentLength, r, w)
217+
218+
if err != nil {
219+
w.WriteHeader(500)
220+
w.Write([]byte(err.Error()))
221+
}
222+
223+
}
224+
}

0 commit comments

Comments
 (0)