-
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 5676b78
Showing
11 changed files
with
820 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,11 @@ | ||
module toolman.org/net/lameduck | ||
|
||
go 1.14 | ||
|
||
require ( | ||
github.com/kr/pretty v0.2.1 | ||
github.com/spf13/pflag v1.0.3 | ||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a | ||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c | ||
toolman.org/base/log/v2 v2.1.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,14 @@ | ||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= | ||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | ||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= | ||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= | ||
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= | ||
golang.org/x/sys v0.0.0-20190203050204-7ae0202eb74c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= | ||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
toolman.org/base/log/v2 v2.1.0 h1://Gx1ca5ri88Z7wTuip9Ja7GPlnvp7BlL7C1VRsmTfA= | ||
toolman.org/base/log/v2 v2.1.0/go.mod h1:S/IHsuY72A9srk+mMzp+QcV4suGhkjlOAeHj5ADURFc= |
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,65 @@ | ||
package lameduck | ||
|
||
import ( | ||
"os" | ||
"time" | ||
) | ||
|
||
// Option is the interface implemented by types that offer optional behavior | ||
// while running a Server with lame-duck support. | ||
type Option interface { | ||
set(*runner) | ||
} | ||
|
||
// Period returns an Option that alters the lame-duck period to the given | ||
// Duration. | ||
func Period(p time.Duration) Option { | ||
return period(p) | ||
} | ||
|
||
type period time.Duration | ||
|
||
func (p period) set(r *runner) { | ||
r.period = time.Duration(p) | ||
} | ||
|
||
// Signals returns an Options that changes the list of Signals that trigger the | ||
// beginning of lame-duck mode. Using this Option fully replaces the previous | ||
// list of triggering signals. | ||
func Signals(s ...os.Signal) Option { | ||
return signals(s) | ||
} | ||
|
||
type signals []os.Signal | ||
|
||
func (s signals) set(r *runner) { | ||
r.signals = ([]os.Signal)(s) | ||
} | ||
|
||
// Logger is the interface needed for the WithLogger Option. | ||
type Logger interface { | ||
Infof(string, ...interface{}) | ||
} | ||
|
||
type loggerOption struct { | ||
logger Logger | ||
} | ||
|
||
// WithLogger returns an Option that alters this package's logging facility | ||
// to the provided Logger. Note, the default Logger is one derived from | ||
// 'github.com/golang/glog'. To prevent all logging, use WithoutLogger. | ||
func WithLogger(l Logger) Option { | ||
return &loggerOption{l} | ||
} | ||
|
||
// WithoutLogger returns an option the disables all logging from this package. | ||
func WithoutLogger() Option { | ||
return &loggerOption{} | ||
} | ||
|
||
func (o *loggerOption) set(r *runner) { | ||
if r.logf = o.logger.Infof; r.logf == nil { | ||
// a "silent" logger | ||
r.logf = func(string, ...interface{}) {} | ||
} | ||
} |
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,75 @@ | ||
package lameduck | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"sync" | ||
"time" | ||
|
||
"golang.org/x/sys/unix" | ||
"toolman.org/base/log/v2" | ||
) | ||
|
||
var ( | ||
defaultPeriod = 3 * time.Second | ||
defaultSignals = []os.Signal{unix.SIGINT, unix.SIGTERM} | ||
) | ||
|
||
type runner struct { | ||
server Server | ||
period time.Duration | ||
signals []os.Signal | ||
logf func(string, ...interface{}) | ||
done chan struct{} | ||
|
||
once sync.Once | ||
} | ||
|
||
func newRunner(svr Server, options []Option) (*runner, error) { | ||
if svr == nil { | ||
return nil, errors.New("nil Server") | ||
} | ||
|
||
r := &runner{ | ||
server: svr, | ||
period: defaultPeriod, | ||
signals: defaultSignals, | ||
logf: log.Infof, | ||
done: make(chan struct{}), | ||
} | ||
|
||
for _, o := range options { | ||
o.set(r) | ||
} | ||
|
||
if r.period <= 0 { | ||
return nil, errors.New("lame-duck period must be greater than zero") | ||
} | ||
|
||
if len(r.signals) == 0 { | ||
return nil, errors.New("no lame-duck signals defined") | ||
} | ||
|
||
return r, nil | ||
} | ||
|
||
func (r *runner) close() { | ||
if r == nil || r.done == nil { | ||
if r != nil { | ||
r.logf("r.done is nil !!!") | ||
} | ||
return | ||
} | ||
|
||
var closed bool | ||
|
||
r.once.Do(func() { | ||
close(r.done) | ||
r.logf("runner closed") | ||
closed = true | ||
}) | ||
|
||
if !closed { | ||
r.logf("runner *NOT* closed") | ||
} | ||
} |
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,207 @@ | ||
// Package lameduck provides coordinated lame-duck behavior for any service | ||
// implementing this package's Server interface. | ||
// | ||
// By default, lame-duck mode is triggered by receipt of SIGINT or SIGTERM | ||
// and the default lame-duck period is 3 seconds. Options are provided to | ||
// alter these (an other) values. | ||
// | ||
// This package is written assuming behavior similar to the standard library's | ||
// http.Server -- in that its Shutdown and Close methods exhibit behavior | ||
// matching the lameduck.Server interface -- however, in order to allow other | ||
// types to be used, a Serve method that returns nil is also needed. | ||
// | ||
// | ||
// type LameDuckServer struct { | ||
// // This embedded http.Server provides Shutdown and Close methods | ||
// // with behavior expected by the lameduck.Server interface. | ||
// *http.Server | ||
// } | ||
// | ||
// // Serve executes ListenAndServe in a manner compliant with the | ||
// // lameduck.Server interface. | ||
// func (s *LameDuckServer) Serve(context.Contxt) error { | ||
// err := s.Server.ListenAndServe() | ||
// | ||
// if err == http.ErrServerClosed { | ||
// err = nil | ||
// } | ||
// | ||
// return err | ||
// } | ||
// | ||
// // Run will run the receiver's embedded http.Server and provide | ||
// // lame-duck coverage on receipt of SIGTERM or SIGINT. | ||
// func (s *LameDuckServer) Run(ctx context.Context) error { | ||
// return lameduck.Run(ctx, s) | ||
// } | ||
package lameduck | ||
|
||
import ( | ||
"context" | ||
"strings" | ||
|
||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
// Server defines the interface that should be implemented by types intended | ||
// for lame-duck support. It is expected that these methods exhibit behavior | ||
// similar to http.Server -- in that a call to Shutdown or Close should cause | ||
// Serve to return immediately. | ||
// | ||
// However, unlike http.Server's Serve, ListenAndServe, and ListenAndServeTLS | ||
// methods (which return http.ErrServerClosed in this situation), this Serve | ||
// method should return a nil error when lame-duck mode is desired. | ||
// | ||
type Server interface { | ||
// Serve executes the Server. If Serve returns an error, that error will be | ||
// returned immediately by Run and no lame-duck coverage will be provided. | ||
// | ||
Serve(context.Context) error | ||
|
||
// Shutdown is called by Run (after catching one of the configured signals) | ||
// to initiate a graceful shutdown of the Server; this marks the beginning | ||
// of lame-duck mode. If Shutdown returns a nil error before the configured | ||
// lame-duck period has elapsed, Run will immediately return nil as well. | ||
// | ||
// The Context provided to Shutdown will have a timeout set to the configured | ||
// lame-duck Period. If Shutdown returns context.DeadlineExceeded, Run will | ||
// return a LameDuckError with its Expired field set to true and Err set to | ||
// the return value from calling Close. | ||
// | ||
// Any other error returned by Shutdown will be wrapped by a LameDuckError | ||
// with its Expired field set to false. | ||
Shutdown(context.Context) error | ||
|
||
// Close is called by Run when Shutdown returns context.DeadlineExceeded and | ||
// its return value will be assigned to the Err field of the LameDuckError | ||
// returned by Run. | ||
Close() error | ||
} | ||
|
||
// Run executes the given Server providing coordinated lame-duck behavior on | ||
// reciept of one or more configurable signals. By default, the lame-duck | ||
// period is 3s and is triggered by SIGINT or SIGTERM. Options are available | ||
// to alter these values. | ||
func Run(ctx context.Context, svr Server, options ...Option) error { | ||
r, err := newRunner(svr, options) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return r.run(ctx) | ||
} | ||
|
||
func (r *runner) run(ctx context.Context) error { | ||
eg, ctx := errgroup.WithContext(ctx) | ||
ctx, cancel := context.WithCancel(ctx) | ||
defer cancel() | ||
|
||
// Goroutine #1 | ||
// | ||
// - Waits for one of the configured signals | ||
// - Calls Shutdown using a Context with a deadline for the configure period | ||
// - If deadline is exceeded, returns the result of calling Close | ||
// - Otherwise, returns the result from the call to Shutdown | ||
// - On return, calls r.close() | ||
// | ||
eg.Go(func() error { | ||
defer r.close() | ||
|
||
r.logf("Waiting for signals: %v", r.signals) | ||
|
||
sig, err := r.waitForSignal(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
r.logf("Received signal [%s]; entering lame-duck mode for %v", sig, r.period) | ||
|
||
ctx, cancel2 := context.WithTimeout(ctx, r.period) | ||
defer cancel2() | ||
|
||
err = r.server.Shutdown(ctx) | ||
switch err { | ||
case nil: | ||
r.logf("Completed lame-duck mode") | ||
return nil | ||
|
||
case context.DeadlineExceeded: | ||
r.logf("Lame-duck period has expired") | ||
return &LameDuckError{ | ||
Expired: true, | ||
Err: r.server.Close(), | ||
} | ||
|
||
default: | ||
r.logf("error shutting down server: %v", err) | ||
cancel() | ||
return &LameDuckError{Err: err} | ||
} | ||
}) | ||
|
||
// Goroutine #2 | ||
// | ||
// - Calls Serve | ||
// - If Server returns a non-nil error, return it immediately | ||
// - Otherwise, wait for the Context or receiver to be "done". | ||
// | ||
eg.Go(func() error { | ||
r.logf("Starting server") | ||
if err := r.server.Serve(ctx); err != nil { | ||
r.logf("Server failed: %v", err) | ||
return err | ||
} | ||
|
||
r.logf("Stopping server") | ||
|
||
select { | ||
case <-ctx.Done(): | ||
r.logf("Context canceled wait for server shutdown") | ||
|
||
case <-r.done: | ||
r.logf("Server stopped") | ||
} | ||
|
||
return nil | ||
}) | ||
|
||
return eg.Wait() | ||
} | ||
|
||
// LameDuckError is the error type returned by Run for errors related to | ||
// lame-duck mode. | ||
type LameDuckError struct { | ||
Expired bool | ||
Err error | ||
} | ||
|
||
func (lde *LameDuckError) Error() string { | ||
if lde == nil { | ||
return "" | ||
} | ||
|
||
var msgs []string | ||
|
||
if lde.Expired { | ||
msgs = append(msgs, "Lame-duck period has expired") | ||
} | ||
|
||
if lde.Err != nil { | ||
if msg := lde.Err.Error(); msg != "" { | ||
msgs = append(msgs, msg) | ||
} | ||
} | ||
|
||
if len(msgs) == 0 { | ||
return "" | ||
} | ||
|
||
return strings.Join(msgs, " + ") | ||
} | ||
|
||
func (lde *LameDuckError) Unwrap() error { | ||
if lde == nil { | ||
return nil | ||
} | ||
return lde.Err | ||
} |
Oops, something went wrong.