github.com/orteth01/up@v0.2.0/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  	timeout := time.Duration(p.config.Proxy.Timeout) * time.Second
    85  	ctx.Infof("waiting for %s (timeout %s)", p.target.String())
    86  
    87  	if err := waitForListen(p.target, timeout); err != nil {
    88  		return errors.Wrapf(err, "waiting for %s to be in listening state", p.target.String())
    89  	}
    90  
    91  	return nil
    92  }
    93  
    94  // Restart the server.
    95  func (p *Proxy) Restart() error {
    96  	ctx.Warn("restarting")
    97  	p.restarts++
    98  
    99  	if err := p.Start(); err != nil {
   100  		return err
   101  	}
   102  
   103  	ctx.WithField("restarts", p.restarts).Warn("restarted")
   104  	return nil
   105  }
   106  
   107  // ServeHTTP implementation.
   108  func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   109  	p.mu.Lock()
   110  	defer p.mu.Unlock()
   111  	p.ReverseProxy.ServeHTTP(w, r)
   112  }
   113  
   114  // RoundTrip implementation.
   115  func (p *Proxy) RoundTrip(r *http.Request) (*http.Response, error) {
   116  	// TODO: give up after N attempts
   117  
   118  	b := p.config.Proxy.Backoff.Backoff()
   119  
   120  retry:
   121  	// replace host as it will change on restart
   122  	r.URL.Host = p.target.Host
   123  	res, err := DefaultTransport.RoundTrip(r)
   124  
   125  	// everything is fine
   126  	if err == nil {
   127  		return res, nil
   128  	}
   129  
   130  	// temporary error, try again
   131  	if e, ok := err.(net.Error); ok && e.Temporary() {
   132  		ctx.WithError(err).Warn("temporary error")
   133  		time.Sleep(b.Duration())
   134  		goto retry
   135  	}
   136  
   137  	// timeout error, try again
   138  	if e, ok := err.(net.Error); ok && e.Timeout() {
   139  		ctx.WithError(err).Warn("timed out")
   140  		time.Sleep(b.Duration())
   141  		goto retry
   142  	}
   143  
   144  	// restart the server, try again
   145  	ctx.WithError(err).Error("network error")
   146  	if err := p.Restart(); err != nil {
   147  		return nil, errors.Wrap(err, "restarting")
   148  	}
   149  
   150  	goto retry
   151  }
   152  
   153  // environment returns the server env variables.
   154  func (p *Proxy) environment() []string {
   155  	return []string{
   156  		env("PORT", p.port),
   157  		env("UP_RESTARTS", p.restarts),
   158  	}
   159  }
   160  
   161  // start the server on a free port.
   162  func (p *Proxy) start() error {
   163  	port, err := freeport.Get()
   164  	if err != nil {
   165  		return errors.Wrap(err, "getting free port")
   166  	}
   167  	p.port = port
   168  	ctx.Infof("found free port %d", port)
   169  
   170  	ctx.Infof("executing %q", p.config.Proxy.Command)
   171  	cmd := exec.Command("sh", "-c", p.config.Proxy.Command)
   172  	cmd.Stdout = writer.New(log.InfoLevel)
   173  	cmd.Stderr = writer.New(log.ErrorLevel)
   174  	env := append(p.environment(), "PATH=node_modules/.bin:"+os.Getenv("PATH"))
   175  	cmd.Env = append(os.Environ(), env...)
   176  
   177  	if err := cmd.Start(); err != nil {
   178  		return errors.Wrap(err, "running command")
   179  	}
   180  
   181  	target, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port))
   182  	if err != nil {
   183  		return errors.Wrap(err, "parsing url")
   184  	}
   185  	p.target = target
   186  
   187  	return nil
   188  }
   189  
   190  // env returns an environment variable.
   191  func env(name string, val interface{}) string {
   192  	return fmt.Sprintf("%s=%v", name, val)
   193  }
   194  
   195  // waitForListen blocks until `u` is listening with timeout.
   196  func waitForListen(u *url.URL, timeout time.Duration) error {
   197  	timedout := time.After(timeout)
   198  
   199  	b := backoff.Backoff{
   200  		Min:    100 * time.Millisecond,
   201  		Max:    time.Second,
   202  		Factor: 1.5,
   203  	}
   204  
   205  	for {
   206  		select {
   207  		case <-timedout:
   208  			return errors.Errorf("timed out after %s", timeout)
   209  		case <-time.After(b.Duration()):
   210  			if isListening(u) {
   211  				return nil
   212  			}
   213  		}
   214  	}
   215  }
   216  
   217  // isListening returns true if there's a server listening on `u`.
   218  func isListening(u *url.URL) bool {
   219  	conn, err := net.Dial("tcp", u.Host)
   220  	if err != nil {
   221  		return false
   222  	}
   223  
   224  	conn.Close()
   225  	return true
   226  }