github.com/suedadam/up@v0.1.12/http/relay/relay.go (about) 1 // Package relay provides a reverse proxy which 2 // relays requests to your "vanilla" HTTP server, 3 // and supports crash recovery. 4 package relay 5 6 import ( 7 "fmt" 8 "net" 9 "net/http" 10 "net/http/httputil" 11 "net/url" 12 "os" 13 "os/exec" 14 "sync" 15 "time" 16 17 "github.com/apex/log" 18 "github.com/facebookgo/freeport" 19 "github.com/jpillora/backoff" 20 "github.com/pkg/errors" 21 22 "github.com/apex/up" 23 "github.com/apex/up/internal/logs" 24 "github.com/apex/up/internal/logs/writer" 25 ) 26 27 // TODO: Wait() and handle error 28 // TODO: add timeout 29 // TODO: scope all plugin logs to their plugin name 30 // TODO: utilize BufferPool 31 // TODO: if the first Start() fails then bail 32 33 // log context. 34 var ctx = logs.Plugin("relay") 35 36 // DefaultTransport used by relay. 37 var DefaultTransport http.RoundTripper = &http.Transport{ 38 DialContext: (&net.Dialer{ 39 Timeout: 2 * time.Second, 40 KeepAlive: 2 * time.Second, 41 DualStack: true, 42 }).DialContext, 43 MaxIdleConns: 0, 44 MaxIdleConnsPerHost: 10, 45 IdleConnTimeout: 5 * time.Minute, 46 TLSHandshakeTimeout: 2 * time.Second, 47 ExpectContinueTimeout: 1 * time.Second, 48 } 49 50 // Proxy is a reverse proxy and sub-process monitor 51 // for ensuring your web server is running. 52 type Proxy struct { 53 config *up.Config 54 55 mu sync.Mutex 56 restarts int 57 port int 58 target *url.URL 59 *httputil.ReverseProxy 60 } 61 62 // New proxy. 63 func New(c *up.Config) (http.Handler, error) { 64 p := &Proxy{ 65 config: c, 66 } 67 68 if err := p.Start(); err != nil { 69 return nil, err 70 } 71 72 return p, nil 73 } 74 75 // Start the server. 76 func (p *Proxy) Start() error { 77 if err := p.start(); err != nil { 78 return err 79 } 80 81 p.ReverseProxy = httputil.NewSingleHostReverseProxy(p.target) 82 p.ReverseProxy.Transport = p 83 84 // TODO: configurable timeout 85 ctx.Infof("waiting for %s", p.target.String()) 86 if err := waitForListen(p.target, 5*time.Second); err != nil { 87 return errors.Wrapf(err, "waiting for %s to be in listening state", p.target.String()) 88 } 89 90 return nil 91 } 92 93 // Restart the server. 94 func (p *Proxy) Restart() error { 95 ctx.Warn("restarting") 96 p.restarts++ 97 98 if err := p.Start(); err != nil { 99 return err 100 } 101 102 ctx.WithField("restarts", p.restarts).Warn("restarted") 103 return nil 104 } 105 106 // ServeHTTP implementation. 107 func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 108 p.mu.Lock() 109 defer p.mu.Unlock() 110 p.ReverseProxy.ServeHTTP(w, r) 111 } 112 113 // RoundTrip implementation. 114 func (p *Proxy) RoundTrip(r *http.Request) (*http.Response, error) { 115 // TODO: give up after N attempts 116 117 b := p.config.Proxy.Backoff.Backoff() 118 119 retry: 120 // replace host as it will change on restart 121 r.URL.Host = p.target.Host 122 res, err := DefaultTransport.RoundTrip(r) 123 124 // everything is fine 125 if err == nil { 126 return res, nil 127 } 128 129 // temporary error, try again 130 if e, ok := err.(net.Error); ok && e.Temporary() { 131 ctx.WithError(err).Warn("temporary error") 132 time.Sleep(b.Duration()) 133 goto retry 134 } 135 136 // timeout error, try again 137 if e, ok := err.(net.Error); ok && e.Timeout() { 138 ctx.WithError(err).Warn("timed out") 139 time.Sleep(b.Duration()) 140 goto retry 141 } 142 143 // restart the server, try again 144 ctx.WithError(err).Error("network error") 145 if err := p.Restart(); err != nil { 146 return nil, errors.Wrap(err, "restarting") 147 } 148 149 goto retry 150 } 151 152 // environment returns the server env variables. 153 func (p *Proxy) environment() []string { 154 return []string{ 155 env("PORT", p.port), 156 env("UP_RESTARTS", p.restarts), 157 } 158 } 159 160 // start the server on a free port. 161 func (p *Proxy) start() error { 162 port, err := freeport.Get() 163 if err != nil { 164 return errors.Wrap(err, "getting free port") 165 } 166 p.port = port 167 ctx.Infof("found free port %d", port) 168 169 ctx.Infof("executing %q", p.config.Proxy.Command) 170 cmd := exec.Command("sh", "-c", p.config.Proxy.Command) 171 cmd.Stdout = writer.New(log.InfoLevel) 172 cmd.Stderr = writer.New(log.ErrorLevel) 173 env := append(p.environment(), "PATH=node_modules/.bin:"+os.Getenv("PATH")) 174 cmd.Env = append(os.Environ(), env...) 175 176 if err := cmd.Start(); err != nil { 177 return errors.Wrap(err, "running command") 178 } 179 180 target, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port)) 181 if err != nil { 182 return errors.Wrap(err, "parsing url") 183 } 184 p.target = target 185 186 return nil 187 } 188 189 // env returns an environment variable. 190 func env(name string, val interface{}) string { 191 return fmt.Sprintf("%s=%v", name, val) 192 } 193 194 // waitForListen blocks until `u` is listening with timeout. 195 func waitForListen(u *url.URL, timeout time.Duration) error { 196 timedout := time.After(timeout) 197 198 b := backoff.Backoff{ 199 Min: 100 * time.Millisecond, 200 Max: time.Second, 201 Factor: 1.5, 202 } 203 204 for { 205 select { 206 case <-timedout: 207 return errors.Errorf("timed out after %s", timeout) 208 case <-time.After(b.Duration()): 209 if isListening(u) { 210 return nil 211 } 212 } 213 } 214 } 215 216 // isListening returns true if there's a server listening on `u`. 217 func isListening(u *url.URL) bool { 218 conn, err := net.Dial("tcp", u.Host) 219 if err != nil { 220 return false 221 } 222 223 conn.Close() 224 return true 225 }