cmd/dockerd/trap: don't force exit after cleanup

Always calling os.Exit() on clean shutdown may not always be desirable
as deferred functions are not run. Let the cleanup callback decide
whether or not to call os.Exit() itself. Allow the process to exit the
normal way, by returning from func main().

Simplify the trap.Trap implementation. The signal notifications are
buffered in a channel so there is little need to spawn a new goroutine
for each received signal. With all signals being handled in the same
goroutine, there are no longer any concurrency concerns around the
interrupt counter.

Signed-off-by: Cory Snider <csnider@mirantis.com>
This commit is contained in:
Cory Snider 2023-04-11 20:56:59 -04:00
parent 16d5d4b6e1
commit 0f3c5d3893

View file

@ -4,47 +4,44 @@ import (
"fmt"
"os"
"os/signal"
"sync/atomic"
"syscall"
)
const (
// Immediately terminate the process when this many SIGINT or SIGTERM
// signals are received.
forceQuitCount = 3
)
// Trap sets up a simplified signal "trap", appropriate for common
// behavior expected from a vanilla unix command-line tool in general
// (and the Docker engine in particular).
//
// - If SIGINT or SIGTERM are received, `cleanup` is called, then the process is terminated.
// - If SIGINT or SIGTERM are received 3 times before cleanup is complete, then cleanup is
// skipped and the process is terminated immediately (allows force quit of stuck daemon)
// The first time a SIGINT or SIGTERM signal is received, `cleanup` is called in
// a new goroutine.
//
// If SIGINT or SIGTERM are received 3 times, the process is terminated
// immediately with an exit code of 128 + the signal number.
func Trap(cleanup func(), logger interface {
Info(args ...interface{})
}) {
c := make(chan os.Signal, 1)
// we will handle INT, TERM here
c := make(chan os.Signal, forceQuitCount)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
interruptCount := uint32(0)
var interruptCount int
for sig := range c {
go func(sig os.Signal) {
logger.Info(fmt.Sprintf("Processing signal '%v'", sig))
switch sig {
case os.Interrupt, syscall.SIGTERM:
if atomic.LoadUint32(&interruptCount) < 3 {
// Initiate the cleanup only once
if atomic.AddUint32(&interruptCount, 1) == 1 {
// Call the provided cleanup handler
cleanup()
os.Exit(0)
} else {
return
}
} else {
// 3 SIGTERM/INT signals received; force exit without cleanup
logger.Info("Forcing docker daemon shutdown without cleanup; 3 interrupts received")
}
logger.Info(fmt.Sprintf("Processing signal '%v'", sig))
if interruptCount < forceQuitCount {
interruptCount++
// Initiate the cleanup only once
if interruptCount == 1 {
go cleanup()
}
// for the SIGINT/TERM non-clean shutdown case, exit with 128 + signal #
os.Exit(128 + int(sig.(syscall.Signal)))
}(sig)
continue
}
logger.Info("Forcing docker daemon shutdown without cleanup; 3 interrupts received")
os.Exit(128 + int(sig.(syscall.Signal)))
}
}()
}