github.com/haraldrudell/parl@v0.4.176/phttp/http.go (about)

     1  /*
     2  © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package phttp
     7  
     8  import (
     9  	"context"
    10  	"net"
    11  	"net/http"
    12  	"net/netip"
    13  	"sync"
    14  	"sync/atomic"
    15  	"time"
    16  
    17  	"github.com/haraldrudell/parl"
    18  	"github.com/haraldrudell/parl/perrors"
    19  	"github.com/haraldrudell/parl/pnet"
    20  )
    21  
    22  const (
    23  	// default port for http: 80, or address for localhost IPv4 or IPv6 port 80
    24  	HttpAddr = ":http"
    25  )
    26  
    27  // Http is an http server instance
    28  //   - based on [http.Server]
    29  //   - has listener thread
    30  //   - all errors sent on channel
    31  //   - idempotent deferrable panic-free shutdown
    32  //   - awaitable, observable
    33  type Http struct {
    34  	// used for [http.Server.Listen] invocation
    35  	//	- "tcp", "tcp4", "tcp6", "unix" or "unixpacket"
    36  	Network pnet.Network
    37  	// Close() ListenAndServe() ListenAndServeTLS() RegisterOnShutdown()
    38  	// Serve() ServeTLS() SetKeepAlivesEnabled() Shutdown()
    39  	http.Server
    40  	// real-time server error stream, unbound non-blocking
    41  	//	- [Http.SendErr] invocations
    42  	//   - shutdown error
    43  	ErrCh parl.NBChan[error]
    44  	// near socket address, protocol is tcp
    45  	Near netip.AddrPort
    46  	// allows to wait for listen
    47  	//	- when triggered, [Http.Near] is present
    48  	ListenAwaitable parl.Awaitable
    49  	// allows to wait for end of listen
    50  	//	- end of thread launched by [Http.Listen]
    51  	EndListenAwaitable parl.Awaitable
    52  	// the URL router
    53  	serveMux *http.ServeMux
    54  	// whether Listen invocation is allowed
    55  	NoListen atomic.Bool
    56  	// Cancel of listening set-up
    57  	Cancel       atomic.Pointer[context.CancelFunc]
    58  	shutdownOnce sync.Once
    59  }
    60  
    61  // NewHttp creates http server for default “localhost:80”
    62  //   - if nearSocket.Addr is invalid, all interfaces for IPv6 if allowed, IPv4 otherwise is used
    63  //   - if nearSocket.Port is zero:
    64  //   - — if network is NetworkDefault: ephemeral port
    65  //   - — otherwise port 80 “:http” is used
    66  //   - for NetworkDefault, NetworkTCP is used
    67  //   - panic for bad Network
    68  //
    69  // Usage:
    70  //
    71  //	var s = NewHttp(netip.AdddrPort{}, pnet.NetworkTCP)
    72  //	s.HandleFunc("/", myHandler)
    73  //	defer s.Shutdown()
    74  //	for err := range s.Listen() {
    75  func NewHttp(nearSocket netip.AddrPort, network pnet.Network) (hp *Http) {
    76  	var hostPort string
    77  	if a := nearSocket.Addr(); a.IsValid() {
    78  		if nearSocket.Port() != 0 || network == pnet.NetworkDefault {
    79  			hostPort = nearSocket.String()
    80  		} else {
    81  			hostPort = a.String() + HttpAddr
    82  		}
    83  	} else {
    84  		hostPort = HttpAddr // default “:http” meaning IPv4 or IPv6 localhost port 80
    85  	}
    86  	switch network {
    87  	case pnet.NetworkDefault:
    88  		network = pnet.NetworkTCP
    89  	case pnet.NetworkTCP, pnet.NetworkTCP4, pnet.NetworkTCP6,
    90  		pnet.NetworkUnix, pnet.NetworkUnixPacket:
    91  	default:
    92  		panic(perrors.ErrorfPF("Bad network: %s allowed: tcp tcp4 tcp6 unix unixpacket", network))
    93  	}
    94  	var serveMux = http.NewServeMux()
    95  	// there is no new-function for [http.Server]
    96  	return &Http{
    97  		Network:  network,
    98  		serveMux: serveMux,
    99  		Server: http.Server{
   100  			// ServeMux matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.
   101  			//	- http.Handler is interface { ServeHTTP(ResponseWriter, *Request) }
   102  			Handler: serveMux, // struct
   103  			Addr:    hostPort,
   104  		},
   105  	}
   106  }
   107  
   108  const (
   109  	httpShutdownTimeout = 5 * time.Second
   110  )
   111  
   112  // HandlerFunc is the signature for URL handlers
   113  type HandlerFunc func(http.ResponseWriter, *http.Request)
   114  
   115  // HandleFunc registers a URL-handler for the server
   116  func (s *Http) HandleFunc(pattern string, handler HandlerFunc) {
   117  	s.serveMux.HandleFunc(pattern, handler)
   118  }
   119  
   120  // Listen initiates listening and returns the error channel
   121  //   - can only be invoked once or panic
   122  //   - errCh closes on server shutdown
   123  //   - non-blocking, all errors are sent on the error channel
   124  func (s *Http) Listen() (errCh <-chan error) {
   125  	if !s.NoListen.CompareAndSwap(false, true) {
   126  		panic(perrors.NewPF("multiple invocations"))
   127  	}
   128  	errCh = s.ErrCh.Ch()
   129  	// listen is deferred so just launch the thread
   130  	go s.httpListenerThread()
   131  	return
   132  }
   133  
   134  // httpListenerThread is gorouitn starting listen and
   135  // waiting for server to terminate
   136  func (s *Http) httpListenerThread() {
   137  	defer s.EndListenAwaitable.Close()
   138  	var err error
   139  	defer parl.Recover(func() parl.DA { return parl.A() }, &err, s.SendErr)
   140  
   141  	// get near tcp socket listener
   142  	var listener net.Listener
   143  	if listener, err = pnet.Listen(s.Network, s.Server.Addr, &s.Cancel); err != nil {
   144  		return
   145  	}
   146  	defer s.maybeClose(&listener, &err)
   147  
   148  	// set Near socket address
   149  	if s.Near, err = pnet.AddrPortFromAddr(listener.Addr()); err != nil {
   150  		return
   151  	}
   152  	s.ListenAwaitable.Close()
   153  
   154  	// blocks here until Shutdown or Close
   155  	err = s.Server.Serve(listener)
   156  	listener = nil
   157  
   158  	// on regular close, http.ErrServerClosed
   159  	if err == http.ErrServerClosed {
   160  		err = nil // ignore error
   161  		return    // successful return
   162  	}
   163  	err = perrors.Errorf("hp.Server.Serve: ‘%w’", err)
   164  }
   165  
   166  // SendErr sends errors on the server’s error channel
   167  func (s *Http) SendErr(err error) { s.ErrCh.Send(err) }
   168  
   169  // idempotent panic-free shutdown that does not return prior to server shut down
   170  func (s *Http) Shutdown() {
   171  	s.shutdownOnce.Do(s.shutdown)
   172  }
   173  
   174  // closes listener if non-nil
   175  func (s *Http) maybeClose(listenerp *net.Listener, errp *error) {
   176  	var listener = *listenerp
   177  	if listener == nil {
   178  		return
   179  	}
   180  	parl.Close(listener, errp)
   181  }
   182  
   183  // 5-second shutdown
   184  func (s *Http) shutdown() {
   185  	var wasListen = s.NoListen.Load()
   186  	if !wasListen {
   187  		// prevent further listen
   188  		wasListen = s.NoListen.Swap(true)
   189  	} else {
   190  		if cancelFuncp := s.Cancel.Load(); cancelFuncp != nil {
   191  			(*cancelFuncp)()
   192  		}
   193  	}
   194  	var ctx, cancel = context.WithTimeout(context.Background(), httpShutdownTimeout)
   195  	defer cancel()
   196  	if err := s.Server.Shutdown(ctx); perrors.IsPF(&err, "hp.Server.Shutdown: '%w'", err) {
   197  		s.SendErr(err)
   198  	}
   199  	if wasListen {
   200  		// wait for thread to exit
   201  		<-s.EndListenAwaitable.Ch()
   202  	}
   203  	s.ErrCh.Close()
   204  }