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  }