Exit Manager is a module for graceful shutdown coordination to ensure critical operations complete and cleanup runs before your application exits. See full documentation.
- Signal handling: Listens for SIGINT (Ctrl+C) and SIGTERM
- Notifications:
Notify()channel closes when shutdown starts - Shutdown locks: Prevents exit until critical operations finish
- Cleanup functions: Runs cleanup in reverse registration order
- Context cancellation: Cancels contexts when shutdown begins
go get github.com/mcwalrus/exit-managerListen for shutdown in your goroutines:
package main
import (
"log"
"time"
exitmanager "github.com/mcwalrus/exit-manager"
)
func main() {
em := exitmanager.Global()
go worker(em)
<-em.Notify()
log.Println("Shutting down...")
select {}
}
func worker(em *exitmanager.ExitManager) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-em.Notify():
log.Println("Worker stopping")
return
case <-ticker.C:
log.Println("Working...")
}
}
}Use locks to ensure operations complete before shutdown:
package main
import (
"log"
"time"
exitmanager "github.com/mcwalrus/exit-manager"
)
func main() {
em := exitmanager.Global()
go processData(em)
log.Println("Running... Press Ctrl+C")
<-em.Notify()
log.Println("Waiting for operations to complete...")
select {}
}
// performs critical operation
func processData(em *exitmanager.ExitManager) {
// exit-manager will wait for process to complete
_ = em.WithShutdownLock(func() error {
time.Sleep(10 * time.Second)
log.Println("Data processed")
})
}Register cleanup functions that run automatically:
package main
import (
"log"
exitmanager "github.com/mcwalrus/exit-manager"
)
func main() {
em := exitmanager.Global()
em.RegisterCleanup(func() {
log.Println("Closing database...")
})
em.RegisterCleanup(func() {
log.Println("Closing cache...")
})
log.Println("Running... Press Ctrl+C")
<-em.Notify()
select {}
}Use contexts that cancel on shutdown:
package main
import (
"context"
"log"
"time"
exitmanager "github.com/mcwalrus/exit-manager"
)
func main() {
em := exitmanager.Global()
ctx, cancel := em.NotifyContext(context.Background())
defer cancel()
go longTask(ctx)
<-em.Notify()
select {}
}
func longTask(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
log.Println("Task cancelled")
return
case <-ticker.C:
log.Println("Processing...")
}
}
}Set a timeout to prevent shutdown from hanging:
package main
import (
"log"
"time"
exitmanager "github.com/mcwalrus/exit-manager"
)
func main() {
em := exitmanager.Global()
em.SetTimeout(exitmanager.TimeoutModeGraceful, 10*time.Second)
em.RegisterCleanup(func() {
log.Println("Cleaning up...")
time.Sleep(120 * time.Second) // timeout occurs first ...
})
log.Println("Running... Press Ctrl+C")
<-em.Notify()
select {}
}Configure how additional shutdown signals are handled:
package main
import (
"log"
"time"
exitmanager "github.com/mcwalrus/exit-manager"
)
func main() {
em := exitmanager.Global()
em.SetMultipleSignals(exitmanager.MultipleSignalsModeForcefulExit)
em.RegisterCleanup(func() {
log.Println("Cleaning up...")
time.Sleep(30 * time.Second)
})
log.Println("Running... Press Ctrl+C twice for immediate exit")
<-em.Notify()
select {}
}See graceful shutdown in action:
cd cmd/exit-manager && go run .go test -race ./...Report issues and feature requests at the GitHub repository.
This module is available under the MIT License.