github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/admin/http_over_uds_server.go (about)

     1  package admin
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"os"
    10  	"syscall"
    11  	"time"
    12  )
    13  
    14  var (
    15  	// ErrSocketStillResponding refers to when
    16  	// a) an instance of the server is still running normally; or
    17  	// b) server was not closed properly
    18  	ErrSocketStillResponding = errors.New("a server is still running and responding to socket")
    19  	// ErrInvalidSocketPathname refers to when the socket filepath is obviously invalid (eg empty string)
    20  	ErrInvalidSocketPathname = errors.New("the socket filepath is invalid")
    21  	// ErrListenerBind refers to generic errors
    22  	ErrListenerBind = errors.New("could not listen on socket")
    23  
    24  	// Anything works here
    25  	SocketHTTPAddress = "http://pyroscope"
    26  	HealthAddress     = SocketHTTPAddress + "/health"
    27  )
    28  
    29  type UdsHTTPServer struct {
    30  	server     *http.Server
    31  	listener   net.Listener
    32  	socketAddr string
    33  }
    34  
    35  type HTTPClient interface {
    36  	Get(url string) (resp *http.Response, err error)
    37  }
    38  
    39  // NewUdsHTTPServer creates a http server that responds over UDS (unix domain socket)
    40  func NewUdsHTTPServer(socketAddr string, httpClient HTTPClient) (*UdsHTTPServer, error) {
    41  	if err := validateSocketAddress(socketAddr); err != nil {
    42  		return nil, err
    43  	}
    44  
    45  	listener, err := createListener(socketAddr, httpClient)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  
    50  	return &UdsHTTPServer{
    51  		listener:   listener,
    52  		socketAddr: socketAddr,
    53  	}, nil
    54  }
    55  
    56  type myHandler struct {
    57  	originalHandler http.Handler
    58  }
    59  
    60  func (m myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    61  	// enrich with an additional endpoint
    62  	// that we will use to probe when starting a new instance
    63  	if r.URL.Path == "/health" {
    64  		writeMessage(w, 200, "it works!")
    65  		return
    66  	}
    67  
    68  	m.originalHandler.ServeHTTP(w, r)
    69  }
    70  
    71  func (u *UdsHTTPServer) Start(handler http.Handler) error {
    72  	h := myHandler{handler}
    73  	u.server = &http.Server{Handler: h}
    74  	err := u.server.Serve(u.listener)
    75  
    76  	// ListenAndServe always returns a non-nil error. After Shutdown or Close,
    77  	// the returned error is ErrServerClosed.
    78  	if errors.Is(err, http.ErrServerClosed) {
    79  		return nil
    80  	}
    81  
    82  	return err
    83  }
    84  
    85  // createListener creates a listener on socketAddr UDS
    86  // it tries to bind to socketAddr
    87  // if it fails, it also tries to consume that socket
    88  // if it's able to, it fails with ErrSocketStillResponding
    89  // if it not able to, then it assumes it's a dangling socket and takes over it
    90  //
    91  // keep in mind there's a slight chance for a race condition there
    92  // where a socket is verified to be not responding
    93  // but the moment it's taken over, it starts to respond (probably because it was taken over by a different instance)
    94  func createListener(socketAddr string, httpClient HTTPClient) (net.Listener, error) {
    95  	takeOver := func(socketAddr string) (net.Listener, error) {
    96  		err := os.Remove(socketAddr)
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  
   101  		return net.Listen("unix", socketAddr)
   102  	}
   103  
   104  	// we listen on a unix domain socket
   105  	// which will be created by the bind syscall
   106  	// https://man7.org/linux/man-pages/man2/bind.2.html
   107  	listener, err := net.Listen("unix", socketAddr)
   108  
   109  	if err != nil {
   110  		if isErrorAddressAlreadyInUse(err) {
   111  			// that socket is already being used
   112  			// let's check if the server is also responding
   113  			resp, err := httpClient.Get(HealthAddress)
   114  
   115  			// the httpclient failed
   116  			// let's take over
   117  			// TODO identify what kind of error happened
   118  			if err != nil {
   119  				return takeOver(socketAddr)
   120  			}
   121  
   122  			// httpclient responded
   123  			// let's check the status code
   124  			if resp.StatusCode == http.StatusOK {
   125  				return nil, ErrSocketStillResponding
   126  			}
   127  
   128  			// httpclient responded, but with a non 200 status code
   129  			// let's be optimistic and try to take over
   130  			return takeOver(socketAddr)
   131  		}
   132  
   133  		return nil, fmt.Errorf("could not bind to socket due to unrecoverable error: %w", err)
   134  	}
   135  
   136  	// no errors happened
   137  	return listener, err
   138  }
   139  
   140  func (u *UdsHTTPServer) Stop() error {
   141  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   142  	defer cancel()
   143  
   144  	// there's no need to remove the socket
   145  	// since go does it for us
   146  	// https://github.com/golang/go/blob/47db3bb443774c0b0df2cab188aa3d76b361dca2/src/net/unixsock_posix.go#L187
   147  	return u.server.Shutdown(ctx)
   148  }
   149  
   150  // https://stackoverflow.com/a/65865898
   151  func isErrorAddressAlreadyInUse(err error) bool {
   152  	var eOsSyscall *os.SyscallError
   153  	if !errors.As(err, &eOsSyscall) {
   154  		return false
   155  	}
   156  	var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr)
   157  	if !errors.As(eOsSyscall, &errErrno) {
   158  		return false
   159  	}
   160  	if errErrno == syscall.EADDRINUSE {
   161  		return true
   162  	}
   163  
   164  	return false
   165  }
   166  
   167  func validateSocketAddress(socketAddr string) error {
   168  	if socketAddr == "" {
   169  		return ErrInvalidSocketPathname
   170  	}
   171  
   172  	// TODO
   173  	// check for the filepath size?
   174  	// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_un.h.html#tag_13_67_04
   175  
   176  	return nil
   177  }