| 
 | 1 | +package appext  | 
 | 2 | + | 
 | 3 | +import (  | 
 | 4 | +	"context"  | 
 | 5 | +	"log"  | 
 | 6 | +	"os"  | 
 | 7 | +	"os/signal"  | 
 | 8 | +	"syscall"  | 
 | 9 | +	"time"  | 
 | 10 | +)  | 
 | 11 | + | 
 | 12 | +type contextBuilder struct {  | 
 | 13 | +	signals   []os.Signal  | 
 | 14 | +	timeout   time.Duration  | 
 | 15 | +	exitFn    func(int)  | 
 | 16 | +	forceExit bool  | 
 | 17 | +}  | 
 | 18 | + | 
 | 19 | +// Context returns a new context builder, with sane defaults, that can be overridden. Calling `Build()` finalizes  | 
 | 20 | +// the new desired context and returns the configured `context.Context`.  | 
 | 21 | +func Context() *contextBuilder {  | 
 | 22 | +	return &contextBuilder{  | 
 | 23 | +		signals: []os.Signal{  | 
 | 24 | +			os.Interrupt,  | 
 | 25 | +			syscall.SIGTERM,  | 
 | 26 | +			syscall.SIGQUIT,  | 
 | 27 | +		},  | 
 | 28 | +		timeout:   30 * time.Second,  | 
 | 29 | +		forceExit: true,  | 
 | 30 | +		exitFn:    os.Exit,  | 
 | 31 | +	}  | 
 | 32 | +}  | 
 | 33 | + | 
 | 34 | +// Signals sets the signals to listen for. Defaults to `os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT`.  | 
 | 35 | +func (c *contextBuilder) Signals(signals ...os.Signal) *contextBuilder {  | 
 | 36 | +	c.signals = signals  | 
 | 37 | +	return c  | 
 | 38 | +}  | 
 | 39 | + | 
 | 40 | +// Timeout sets the timeout for graceful shutdown before forcing the issue exiting with exit code 1.  | 
 | 41 | +// Defaults to 30 seconds.  | 
 | 42 | +//  | 
 | 43 | +// A timeout of <= 0, not recommended, disables the timeout and will wait forever for a seconds signal or application  | 
 | 44 | +// shuts down.  | 
 | 45 | +func (c *contextBuilder) Timeout(timeout time.Duration) *contextBuilder {  | 
 | 46 | +	c.timeout = timeout  | 
 | 47 | +	return c  | 
 | 48 | +}  | 
 | 49 | + | 
 | 50 | +// ForceExit sets whether to force terminate ungracefully upon receipt of a second signal. Defaults to true.  | 
 | 51 | +func (c *contextBuilder) ForceExit(forceExit bool) *contextBuilder {  | 
 | 52 | +	c.forceExit = forceExit  | 
 | 53 | +	return c  | 
 | 54 | +}  | 
 | 55 | + | 
 | 56 | +// ExitFn sets the exit function to use. Defaults to `os.Exit`.  | 
 | 57 | +//  | 
 | 58 | +// This is used in the unit tests but can be used to intercept the exit call and do something else as needed also.  | 
 | 59 | +func (c *contextBuilder) ExitFn(exitFn func(int)) *contextBuilder {  | 
 | 60 | +	c.exitFn = exitFn  | 
 | 61 | +	return c  | 
 | 62 | +}  | 
 | 63 | + | 
 | 64 | +// Build finalizes the context builder and returns the configured `context.Context`.  | 
 | 65 | +//  | 
 | 66 | +// This will spawn another goroutine listening for the configured signals and will cancel the context when received with  | 
 | 67 | +// the configured settings.  | 
 | 68 | +func (c *contextBuilder) Build() context.Context {  | 
 | 69 | +	var sig = make(chan os.Signal, 1)  | 
 | 70 | +	signal.Notify(sig, c.signals...)  | 
 | 71 | + | 
 | 72 | +	ctx, cancel := context.WithCancel(context.Background())  | 
 | 73 | + | 
 | 74 | +	go listen(sig, cancel, c.exitFn, c.timeout, c.forceExit)  | 
 | 75 | + | 
 | 76 | +	return ctx  | 
 | 77 | +}  | 
 | 78 | + | 
 | 79 | +func listen(sig <-chan os.Signal, cancel context.CancelFunc, exitFn func(int), timeout time.Duration, forceExit bool) {  | 
 | 80 | +	s := <-sig  | 
 | 81 | +	cancel()  | 
 | 82 | +	log.Printf("received shutdown signal %q\n", s)  | 
 | 83 | + | 
 | 84 | +	if timeout > 0 {  | 
 | 85 | +		select {  | 
 | 86 | +		case s := <-sig:  | 
 | 87 | +			if forceExit {  | 
 | 88 | +				log.Printf("received second shutdown signal %q, forcing exit\n", s)  | 
 | 89 | +				exitFn(1)  | 
 | 90 | +			}  | 
 | 91 | +		case <-time.After(timeout):  | 
 | 92 | +			log.Printf("timeout of %s reached, forcing exit\n", timeout)  | 
 | 93 | +			exitFn(1)  | 
 | 94 | +		}  | 
 | 95 | +	} else {  | 
 | 96 | +		s = <-sig  | 
 | 97 | +		if forceExit {  | 
 | 98 | +			log.Printf("received second shutdown signal %q, forcing exit\n", s)  | 
 | 99 | +			exitFn(1)  | 
 | 100 | +		}  | 
 | 101 | +	}  | 
 | 102 | +}  | 
0 commit comments