Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
abursavich committed Jan 20, 2021
0 parents commit d7f88d7
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
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.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Graceful
[![License](https://img.shields.io/badge/license-mit-blue.svg?style=for-the-badge)](https://raw.githubusercontent.com/abursavich/graceful/main/LICENSE)
[![GoDev Reference](https://img.shields.io/static/v1?logo=go&logoColor=white&color=00ADD8&label=dev&message=reference&style=for-the-badge)](https://pkg.go.dev/bursavich.dev/graceful)
[![Go Report Card](https://goreportcard.com/badge/bursavich.dev/graceful?style=for-the-badge)](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")
}
```
73 changes: 73 additions & 0 deletions context.go
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
}
8 changes: 8 additions & 0 deletions go.mod
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
)
4 changes: 4 additions & 0 deletions go.sum
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=
244 changes: 244 additions & 0 deletions server.go
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()
}

0 comments on commit d7f88d7

Please sign in to comment.