Skip to content

Commit

Permalink
flathttp: impl http server that can have a config reloaded s.t. it ma…
Browse files Browse the repository at this point in the history
…y listen to one or several net.Listeners or be shut down at any time
  • Loading branch information
lithdew committed May 24, 2020
1 parent a79b2af commit 14143d8
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 0 deletions.
48 changes: 48 additions & 0 deletions flathttp/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package flathttp

import (
"fmt"
"net/url"
"time"
)

type Addr struct {
Addr string
Scheme string
Host string
}

type Config struct {
Addrs []string

MaxHeaderBytes int
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
ReadHeaderTimeout time.Duration
ShutdownTimeout time.Duration

addrs []Addr
}

func (c *Config) Parse() error {
for _, addr := range c.Addrs {
u, err := url.Parse(addr)
if err != nil {
return fmt.Errorf("flathttp: invalid addr provided in config %q", u)
}
switch u.Scheme {
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
case "http", "https":
u.Scheme = "tcp"
default:
return fmt.Errorf("flathttp: invalid scheme %q in addr %q (must be http, https, tcp, tcp4, tcp6, unix, unixpacket)", u.Scheme, addr)
}
c.addrs = append(c.addrs, Addr{Addr: addr, Scheme: u.Scheme, Host: u.Host})
}
return nil
}

func (c *Config) reset() {
c.addrs = c.addrs[:0]
}
208 changes: 208 additions & 0 deletions flathttp/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package flathttp

import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"sync"
"time"
)

var DefaultShutdownTimeout = 5 * time.Second

var ErrAlreadyListening = errors.New("already listening")

type Server struct {
cfg Config
srv *http.Server

lns map[string]net.Listener // all listeners
wg sync.WaitGroup // all served goroutines
lm sync.Mutex // lifecycle mutex\
em sync.Mutex // errors mutex
errs []error // errors
}

func NewServer(cfg *Config) *Server {
if cfg == nil {
cfg = &Config{}
}
srv := &Server{
cfg: *cfg,
lns: make(map[string]net.Listener),
}
return srv
}

func (s *Server) Update(cfg *Config) {
s.lm.Lock()
defer s.lm.Unlock()

if cfg == nil {
cfg = &Config{}
}
s.cfg = *cfg
}

func (s *Server) Start() []error {
s.lm.Lock()
defer s.lm.Unlock()

err := s.start()
if err != nil {
return append([]error{err}, s.stop()...)
}
return nil
}

func (s *Server) start() error {
if err := s.cfg.Parse(); err != nil {
return err
}

s.srv = &http.Server{
MaxHeaderBytes: s.cfg.MaxHeaderBytes,
ReadTimeout: s.cfg.ReadTimeout,
WriteTimeout: s.cfg.WriteTimeout,
IdleTimeout: s.cfg.IdleTimeout,
ReadHeaderTimeout: s.cfg.ReadHeaderTimeout,
}

addrs := s.cfg.addrs
if len(addrs) == 0 {
addrs = append(addrs, Addr{Addr: "tcp://:0", Scheme: "tcp", Host: ":0"})
}

for _, a := range addrs {
err := s.listen(a)
if err == nil {
continue
}
if errors.Is(err, ErrAlreadyListening) {
continue
}
return err
}

return nil
}

func (s *Server) listen(a Addr) error {
srv := s.srv

_, exists := s.lns[a.Addr]
if exists {
return fmt.Errorf("flathttp [%s]: %w", a.Addr, ErrAlreadyListening)
}

ln, err := net.Listen(a.Scheme, a.Host)
if err != nil {
return fmt.Errorf("flathttp: failed to start listener for %q: %w", a, err)
}

fmt.Printf("Listening %q.\n", ln.Addr())

s.lns[a.Addr] = ln

// Start serving the listener.

s.wg.Add(1)
go func() {
defer s.wg.Done()

err := eof(srv.Serve(ln))
if err == nil {
return
}

err = fmt.Errorf("flathttp [%s] (serve error): %w", a.Addr, err)

s.em.Lock()
s.errs = append(s.errs, err)
s.em.Unlock()
}()

return nil
}

func (s *Server) Stop() []error {
s.lm.Lock()
defer s.lm.Unlock()

return s.stop()
}

func (s *Server) stop() []error {
var errs []error

// Gracefully shutdown the server.

timeout := s.cfg.ShutdownTimeout
if timeout <= 0 {
timeout = DefaultShutdownTimeout
}

ctx, cancel := context.WithTimeout(context.Background(), timeout)

err := s.srv.Shutdown(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("flathttp: failed to gracefully shutdown the server: %w", err))
}

<-ctx.Done()

errs = append(errs, s.close()...)

if cancel != nil {
cancel()
}

// Wait until all listeners are closed, and if errors rise up, include them in the result.

s.wg.Wait()

s.cfg.reset()

s.srv = nil

for addr := range s.lns {
delete(s.lns, addr)
}

errs = append(errs, s.errs...)
s.errs = s.errs[:0]

return errs
}

func eof(err error) error {
var op *net.OpError

switch {
case errors.As(err, &op) && op.Err.Error() == "use of closed network connection":
return nil
case errors.Is(err, http.ErrServerClosed):
return nil
case errors.Is(err, io.EOF):
return nil
default:
return err
}
}

func (s *Server) close() []error {
var errs []error

for addr, ln := range s.lns {
err := eof(ln.Close())
if err != nil {
err = fmt.Errorf("flathttp [%s] (listener close): %w", addr, err)
errs = append(errs, err)
}
}

return errs
}
47 changes: 47 additions & 0 deletions flathttp/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package flathttp

import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"net"
"testing"
"time"
)

func getAvailableAddrs(t testing.TB, scheme string, n int) []string {
t.Helper()

addrs := make([]string, 0, n)
for i := 0; i < n; i++ {
ln, err := net.Listen(scheme, ":0")
require.NoError(t, err)
require.NoError(t, ln.Close())

addrs = append(addrs, scheme+"://"+ln.Addr().String())
}
return addrs
}

func TestServer(t *testing.T) {
defer goleak.VerifyNone(t)

srv := NewServer(
&Config{
Addrs: getAvailableAddrs(t, "tcp", 1),
ShutdownTimeout: 100 * time.Millisecond,
},
)

for i := 0; i < 10; i++ {
require.Empty(t, srv.Start())

srv.Update(
&Config{
Addrs: append(srv.cfg.Addrs, getAvailableAddrs(t, "tcp", 1)...),
ShutdownTimeout: 100 * time.Millisecond,
},
)
}

require.Empty(t, srv.Stop())
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
module github.com/lithdew/flatend

go 1.14

require (
github.com/stretchr/testify v1.4.0
github.com/tidwall/gjson v1.6.0 // indirect
go.uber.org/goleak v1.0.0
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375 // indirect
)
50 changes: 50 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/goleak v1.0.0 h1:qsup4IcBdlmsnGfqyLl4Ntn3C2XCCuKAE7DwHpScyUo=
go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375 h1:SjQ2+AKWgZLc1xej6WSzL+Dfs5Uyd5xcZH1mGC411IA=
golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

0 comments on commit 14143d8

Please sign in to comment.