-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
flathttp: impl http server that can have a config reloaded s.t. it ma…
…y listen to one or several net.Listeners or be shut down at any time
- Loading branch information
Showing
5 changed files
with
361 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |