-
Notifications
You must be signed in to change notification settings - Fork 0
/
proxy.go
167 lines (143 loc) · 4.76 KB
/
proxy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package main
import (
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os/exec"
"sync"
"time"
"github.com/bep/debounce"
)
// The Proxy manages passing requests to the target application. Duties include
// building and running the target application, when necessary.
type Proxy struct {
// Context holds a function that returns a base context to the caller.
// The context can be used to shut down any running operations.
Context context.Context
// The BuildCommand is the command that is executed when the target
// is ready to be built.
BuildCommand string
// The RunCommand is the command that is executed when the target has finished
// building and is ready to go.
RunCommand string
// TargetURL is the URL of the target application.
TargetURL *url.URL
// Change listens for changes on the filesystem to signal when a build
// should be triggered.
Change <-chan string
initOnce sync.Once // initOnce guards initialization.
reverseProxy *httputil.ReverseProxy // The reverse proxy handler.
change chan string // Forwarding change channel.
mu sync.Mutex // mu protects the following attributes.
cmd *exec.Cmd // The currently running command.
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.init()
if err := p.buildAndRun(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// The target may not become immediately available after it is started.
// The request will be attempted a few times to see if we can get a successful
// response.
bw := badGatewayWriter{
ResponseWriter: w,
MaxRetries: 3,
}
for !bw.OK() {
p.reverseProxy.ServeHTTP(&bw, r)
time.Sleep(100 * time.Millisecond)
}
}
// init is called upon the first access to ServeHTTP to setup the environment
// required for the proxy to operate.
func (p *Proxy) init() {
p.initOnce.Do(func() {
p.reverseProxy = httputil.NewSingleHostReverseProxy(p.TargetURL)
// The change loop receives changes from the file watcher change channel and
// copies the value to the internal change channel. This is done to allow injecting
// changes that do not come from the externally defined channel.
//
// Reloads are debounced to prevent successive saves from triggering many builds to
// kick off unnecessarily. This is especially useful when using formatting tools
// (gofmt, goimports, etc.) after saving a file. They will save the file again after
// formatting and it is not useful to run a build multiple times.
debouncer := debounce.New(100 * time.Millisecond)
p.change = make(chan string, 1)
p.change <- ""
go func() {
for {
select {
case change := <-p.Change:
debouncer(func() {
p.change <- change
})
case <-p.Context.Done():
return
}
}
}()
})
}
// buildAndRun builds and runs the target application. If the target application is running
// when a new compile is triggered it will be killed.
func (p *Proxy) buildAndRun() error {
select {
case <-p.change:
default:
return nil
}
if err := build(p.Context, p.BuildCommand); err != nil {
return fmt.Errorf("build: %w", err)
}
p.mu.Lock()
defer p.mu.Unlock()
if p.cmd != nil {
if err := p.cmd.Process.Kill(); err != nil {
return fmt.Errorf("kill: %w", err)
}
}
c, err := run(p.Context, p.RunCommand)
if err != nil {
return fmt.Errorf("run: %w", err)
}
p.cmd = c
return nil
}
// badGatewayWriter wraps a ResponseWriter to allow handling of bad gateway errors from the
// upstream proxy HTTP handler.
type badGatewayWriter struct {
http.ResponseWriter
// MaxRetries holds the number of times the bad gateway writer should hold back the response.
// After MaxRetries number of failures the bad gateway writer will revert to sending the content
// back to the client unmodified. This allows the client to see a response when we have given up.
MaxRetries int
failed int
written bool
}
// WriteHeader watches for a bad gateway error and sets the appropriate internal state and eats
// the header write. Any other status will be sent as normal.
func (b *badGatewayWriter) WriteHeader(status int) {
if b.failed < b.MaxRetries && status == http.StatusBadGateway {
b.failed++
return
}
b.ResponseWriter.WriteHeader(status)
b.written = true
}
// Write takes the content to be written and eats it if a bad gateway error has been detected.
// This pevents sending unwanted data to the client when we want to retry. Under normal conditions
// the data is passed up to the ResponseWriter.
func (b *badGatewayWriter) Write(p []byte) (int, error) {
if !b.OK() {
return 0, nil
}
return b.ResponseWriter.Write(p)
}
// OK returns true if the reponse was anything other than a bad gateway error or the number of retries
// exceeded the limit.
func (b *badGatewayWriter) OK() bool {
return b.written
}