-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d7f88d7
Showing
6 changed files
with
392 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,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2021 Andrew Bursavich | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
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,42 @@ | ||
# Graceful | ||
[](https://raw.githubusercontent.com/abursavich/graceful/main/LICENSE) | ||
[](https://pkg.go.dev/bursavich.dev/graceful) | ||
[](https://goreportcard.com/report/bursavich.dev/graceful) | ||
|
||
Package graceful provides graceful shutdown for servers. | ||
|
||
|
||
## Example | ||
|
||
### Dual HTTP Server | ||
|
||
```go | ||
// DualServerConfig specifies a server with functions split between | ||
// internal and external use cases and graceful shutdown parameters. | ||
srv := graceful.DualServerConfig{ | ||
// ExternalServer is the server for the primary clients. | ||
ExternalServer: graceful.HTTPServer(&http.Server{ | ||
Handler: extMux, | ||
}), | ||
// InternalServer is the server for health checks, metrics, debugging, | ||
// profiling, etc. It shuts down after the ExternalServer exits. | ||
InternalServer: graceful.HTTPServer(&http.Server{ | ||
Handler: intMux, | ||
}), | ||
// ShutdownDelay gives time for load balancers to remove the server from | ||
// their backend pools before it stops listening. It's inserted after a | ||
// shutdown signal is received and before GracefulShutdown is called on | ||
// the servers. | ||
ShutdownDelay: 10 * time.Second, | ||
// ShutdownGrace gives time for pending requests complete before the server | ||
// must forcibly shut down. It's the timeout on the context passed to | ||
// GracefulShutdown. | ||
ShutdownGrace: 30 * time.Second, | ||
// Logger optionally specifies a logger which will be used to output info | ||
// and errors. | ||
Logger: log, | ||
} | ||
if err := srv.ListenAndServe(ctx, *intAddr, *extAddr); err != nil { | ||
log.Error(err, "Serving failed") | ||
} | ||
``` |
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,73 @@ | ||
// SPDX-License-Identifier: MIT | ||
// | ||
// Copyright 2021 Andrew Bursavich. All rights reserved. | ||
// Use of this source code is governed by The MIT License | ||
// which can be found in the LICENSE file. | ||
|
||
package graceful | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"os/signal" | ||
"syscall" | ||
"time" | ||
|
||
"github.com/go-logr/logr" | ||
) | ||
|
||
// Contexts returns two contexts which respectively serve as soft and hard | ||
// shutdown signals. They are cancelled after TERM or INT signals are received. | ||
// | ||
// If delay is positive, it will wait that duration after receiving a signal | ||
// before cancelling the first context. This is useful to allow loadbalancer | ||
// updates before the server stops accepting new requests. | ||
// | ||
// If grace is positive, it will wait that duration after cancelling the first | ||
// context before cancelling the second context. This is useful to set a maximum | ||
// time to allow pending requests to complete. | ||
// | ||
// Repeated TERM or INT signals will bypass any delay or grace time. | ||
func Contexts(ctx context.Context, log logr.Logger, delay, grace time.Duration) (context.Context, context.Context) { | ||
if log == nil { | ||
log = logr.Discard() | ||
} | ||
sigCh := make(chan os.Signal, 3) | ||
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) | ||
hardCtx, hardCancel := context.WithCancel(ctx) | ||
softCtx, softCancel := context.WithCancel(hardCtx) | ||
go func() { | ||
defer signal.Stop(sigCh) | ||
defer hardCancel() | ||
select { | ||
case sig := <-sigCh: | ||
log.Info("Shutdown triggered", "signal", sig.String()) | ||
case <-ctx.Done(): | ||
return | ||
} | ||
if delay > 0 { | ||
log.Info("Shutdown starting after delay period", "duration", delay) | ||
select { | ||
case <-time.After(delay): | ||
log.Info("Shutdown delay period ended") | ||
case sig := <-sigCh: | ||
log.Info("Skipping shutdown delay period", "signal", sig.String()) | ||
case <-ctx.Done(): | ||
return | ||
} | ||
} | ||
if grace > 0 { | ||
log.Info("Shutdown starting with grace period", "duration", grace) | ||
softCancel() | ||
select { | ||
case <-time.After(grace): | ||
log.Info("Shutdown grace period ended") | ||
case sig := <-sigCh: | ||
log.Info("Skipping shutdown grace period", "signal", sig.String()) | ||
case <-ctx.Done(): | ||
return | ||
} | ||
} | ||
}() | ||
return softCtx, hardCtx | ||
} |
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,8 @@ | ||
module bursavich.dev/graceful | ||
|
||
go 1.16 | ||
|
||
require ( | ||
github.com/go-logr/logr v0.3.0 | ||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a | ||
) |
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,4 @@ | ||
github.com/go-logr/logr v0.3.0 h1:q4c+kbcR0d5rSurhBR8dIgieOaYpXtsdTYfx22Cu6rs= | ||
github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= | ||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= | ||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
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,244 @@ | ||
// SPDX-License-Identifier: MIT | ||
// | ||
// Copyright 2021 Andrew Bursavich. All rights reserved. | ||
// Use of this source code is governed by The MIT License | ||
// which can be found in the LICENSE file. | ||
|
||
// Package graceful provides graceful shutdown for servers. | ||
package graceful | ||
|
||
import ( | ||
"context" | ||
"net" | ||
"net/http" | ||
"sync" | ||
"time" | ||
|
||
"github.com/go-logr/logr" | ||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
// A Server is a networked server. | ||
type Server interface { | ||
// Serve serves connections accepted from the listener. It does not return | ||
// an error if the listener is closed by the GracefulShutown method. | ||
Serve(net.Listener) error | ||
|
||
// GracefulShutdown immediately closes the server's listener and, if possible, | ||
// signals to clients that it is going away. It waits for pending requests to | ||
// finish or until the context is closed. If all pending requests finish, no | ||
// error is returned. | ||
GracefulShutdown(context.Context) error | ||
} | ||
|
||
// HTTPServer converts an http.Server into a graceful.Server. | ||
func HTTPServer(srv *http.Server) Server { | ||
return (*httpServer)(srv) | ||
} | ||
|
||
type httpServer http.Server | ||
|
||
func (s *httpServer) Serve(lis net.Listener) error { | ||
srv := (*http.Server)(s) | ||
if err := srv.Serve(lis); err != http.ErrServerClosed { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (s *httpServer) GracefulShutdown(ctx context.Context) error { | ||
srv := (*http.Server)(s) | ||
srv.SetKeepAlivesEnabled(false) | ||
return srv.Shutdown(ctx) | ||
} | ||
|
||
// ServerConfig specifies a server with graceful shutdown parameters. | ||
type ServerConfig struct { | ||
_ struct{} | ||
|
||
// Server is the networked server. | ||
Server Server | ||
|
||
// ShutdownDelay gives time for load balancers to remove the server from | ||
// their backend pools before it stops listening. It's inserted after a | ||
// shutdown signal is received and before GracefulShutdown is called on | ||
// the servers. | ||
ShutdownDelay time.Duration | ||
|
||
// ShutdownGrace gives time for pending requests complete before the server | ||
// must forcibly shut down. It's the timeout on the context passed to | ||
// GracefulShutdown. | ||
ShutdownGrace time.Duration | ||
|
||
// Logger optionally specifies a logger which will be used to output info | ||
// and errors. | ||
Logger logr.Logger | ||
|
||
initOnce sync.Once | ||
} | ||
|
||
func (cfg *ServerConfig) init() { | ||
cfg.initOnce.Do(func() { | ||
if cfg.Logger == nil { | ||
cfg.Logger = logr.Discard() | ||
} | ||
}) | ||
} | ||
|
||
// ListenAndServe listens on the given address and calls Serve. | ||
func (cfg *ServerConfig) ListenAndServe(ctx context.Context, addr string) error { | ||
cfg.init() | ||
|
||
lis, err := net.Listen("tcp", addr) | ||
if err != nil { | ||
cfg.Logger.Error(err, "Server failed to listen", "address", addr) | ||
return err | ||
} | ||
cfg.Logger.Info("Server listening", "address", lis.Addr().String()) | ||
|
||
return cfg.Serve(ctx, lis) | ||
} | ||
|
||
// Serve serves with the given listener. It waits for shutdown signals with the | ||
// given context and calls GracefulShutdown as configured. If the context is cancelled | ||
// a hard shutdown is initiated. | ||
func (cfg *ServerConfig) Serve(ctx context.Context, lis net.Listener) error { | ||
cfg.init() | ||
|
||
g, ctx := errgroup.WithContext(ctx) | ||
softCtx, hardCtx := Contexts(ctx, cfg.Logger, cfg.ShutdownDelay, cfg.ShutdownGrace) | ||
// Serve. | ||
g.Go(func() error { | ||
defer lis.Close() | ||
if err := cfg.Server.Serve(lis); err != nil { | ||
cfg.Logger.Error(err, "Server failed") | ||
return err | ||
} | ||
return nil | ||
}) | ||
// Watch for shutdown signal. | ||
g.Go(func() error { | ||
<-softCtx.Done() | ||
if err := cfg.Server.GracefulShutdown(hardCtx); err != nil { | ||
cfg.Logger.Error(err, "Server graceful shutdown failed") | ||
return err | ||
} | ||
return nil | ||
}) | ||
// Wait for shutdown. | ||
return g.Wait() | ||
} | ||
|
||
// DualServerConfig specifies a server with functions split between | ||
// internal and external use cases and graceful shutdown parameters. | ||
type DualServerConfig struct { | ||
_ struct{} | ||
|
||
// ExternalServer is the server for the primary clients. | ||
ExternalServer Server | ||
|
||
// InternalServer is the server for health checks, metrics, debugging, | ||
// profiling, etc. It shuts down after the ExternalServer exits. | ||
InternalServer Server | ||
|
||
// ShutdownDelay gives time for load balancers to remove the server from | ||
// their backend pools before it stops listening. It's inserted after a | ||
// shutdown signal is received and before GracefulShutdown is called on | ||
// the servers. | ||
ShutdownDelay time.Duration | ||
|
||
// ShutdownGrace gives time for pending requests complete before the server | ||
// must forcibly shut down. It's the timeout on the context passed to | ||
// GracefulShutdown. | ||
ShutdownGrace time.Duration | ||
|
||
// Logger optionally specifies a logger which will be used to output info | ||
// and errors. | ||
Logger logr.Logger | ||
|
||
initOnce sync.Once | ||
} | ||
|
||
func (cfg *DualServerConfig) init() { | ||
cfg.initOnce.Do(func() { | ||
if cfg.Logger == nil { | ||
cfg.Logger = logr.Discard() | ||
} | ||
}) | ||
} | ||
|
||
// ListenAndServe listens on the given addresses and calls Serve. | ||
func (cfg *DualServerConfig) ListenAndServe(ctx context.Context, intAddr, extAddr string) error { | ||
cfg.init() | ||
|
||
intLis, err := net.Listen("tcp", intAddr) | ||
if err != nil { | ||
cfg.Logger.Error(err, "Internal server failed to listen", "address", intAddr) | ||
return err | ||
} | ||
cfg.Logger.Info("Internal server listening", "address", intLis.Addr().String()) | ||
|
||
extLis, err := net.Listen("tcp", extAddr) | ||
if err != nil { | ||
cfg.Logger.Error(err, "External server failed to listen", "address", extAddr) | ||
intLis.Close() | ||
return err | ||
} | ||
cfg.Logger.Info("External server listening", "address", extLis.Addr().String()) | ||
|
||
return cfg.Serve(ctx, intLis, extLis) | ||
} | ||
|
||
// Serve serves with the given listeners. It waits for shutdown signals with the | ||
// given context and calls GracefulShutdown as configured. If the context is cancelled | ||
// a hard shutdown is initiated. | ||
func (cfg *DualServerConfig) Serve(ctx context.Context, intLis, extLis net.Listener) error { | ||
cfg.init() | ||
|
||
ctx, cancel := context.WithCancel(ctx) | ||
defer cancel() | ||
|
||
g, ctx := errgroup.WithContext(ctx) | ||
softCtx, hardCtx := Contexts(ctx, cfg.Logger, cfg.ShutdownDelay, cfg.ShutdownGrace) | ||
extShutdownDone := make(chan struct{}) | ||
|
||
// Serve internal. | ||
g.Go(func() error { | ||
defer intLis.Close() | ||
if err := cfg.InternalServer.Serve(intLis); err != nil { | ||
cfg.Logger.Error(err, "Internal server failed") | ||
return err | ||
} | ||
return nil | ||
}) | ||
// Serve external. | ||
g.Go(func() error { | ||
defer extLis.Close() | ||
if err := cfg.ExternalServer.Serve(extLis); err != nil { | ||
cfg.Logger.Error(err, "External server failed") | ||
return err | ||
} | ||
return nil | ||
}) | ||
// Watch for signal and shutdown external first. | ||
g.Go(func() error { | ||
defer close(extShutdownDone) | ||
<-softCtx.Done() | ||
if err := cfg.ExternalServer.GracefulShutdown(hardCtx); err != nil { | ||
cfg.Logger.Error(err, "External server graceful shutdown failed") | ||
return err | ||
} | ||
return nil | ||
}) | ||
// Shutdown internal after external. | ||
g.Go(func() error { | ||
<-extShutdownDone | ||
if err := cfg.InternalServer.GracefulShutdown(hardCtx); err != nil { | ||
cfg.Logger.Error(err, "Internal server graceful shutdown failed") | ||
return err | ||
} | ||
return nil | ||
}) | ||
// Wait for shutdown. | ||
return g.Wait() | ||
} |