Skip to content

Commit

Permalink
Start
Browse files Browse the repository at this point in the history
  • Loading branch information
tep committed Jan 25, 2021
0 parents commit 5676b78
Show file tree
Hide file tree
Showing 11 changed files with 820 additions and 0 deletions.
11 changes: 11 additions & 0 deletions go.mod
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
)
14 changes: 14 additions & 0 deletions go.sum
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=
65 changes: 65 additions & 0 deletions options.go
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{}) {}
}
}
75 changes: 75 additions & 0 deletions runner.go
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")
}
}
207 changes: 207 additions & 0 deletions server.go
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
}
Loading

0 comments on commit 5676b78

Please sign in to comment.