A Go library for managing the lifecycle of an application with graceful shutdown capabilities.
The Exitplan library provides a simple mechanism for managing the lifetime of an application. It helps you handle application running, and shutdown phases with proper resource cleanup.
Key features include:
- Distinct application lifecycle phases (running, teardown)
- Context-based lifecycle management
- Graceful shutdown with customizable timeout
- Flexible callback registration for cleanup operations
- Signal handling for clean application termination
- Synchronous and asynchronous shutdown callbacks
- Error handling during shutdown
go get github.com/struct0x/exitplanExitplan manages two lifecycle phases:
-
Running: active between
Run()andExit(). UseContext()for workers and other long-running tasks.
It is canceled as soon as shutdown begins (viaExit(), signal, or startup timeout). -
Teardown: calling
Exit(reason or nil)starts the teardown.
It blocks until all registered callbacks are complete. UseTeardownContext()in shutdown callbacks.
It is canceled when the global teardown timeout elapses.
Note
Calling Exit() before Run() starts the teardown immediately.
If no teardown timeout is set and a callback hangs, Exit() will block indefinitely.
Use
Started() to receive a signal when the application enters the running phase.
This is useful for readiness probes or coordinating dependent services.
Use Stopping() to receive a signal when the application enters the teardown phase.
Use Completed() to receive a signal when the teardown phase completes.
Use WithStartupTimeout() to detect stuck initialization:
package main
import (
"time"
"github.com/struct0x/exitplan"
)
func main() {
_ = exitplan.New(
exitplan.WithStartupTimeout(10 * time.Second),
)
// If Run() isn't called within 10 seconds,
// Context() is canceled and teardown begins
}This is useful when initialization depends on external services that might hang.
Shutdown callbacks registered with OnExit* are executed in **LIFO order
** (last registered, first executed).
This mirrors resource lifecycles: if you start DB then HTTP, shutdown runs HTTP then DB.
Callbacks marked with Async are awaited up to the teardown timeout.
package main
import (
"fmt"
"syscall"
"time"
"github.com/struct0x/exitplan"
)
func main() {
// Create a new Exitplan instance with signal handling for graceful shutdown
ex := exitplan.New(
exitplan.WithSignal(syscall.SIGINT, syscall.SIGTERM),
exitplan.WithTeardownTimeout(5*time.Second),
)
// Register cleanup functions
ex.OnExit(func() {
fmt.Println("Cleaning up resources...")
time.Sleep(1 * time.Second)
fmt.Println("Cleanup complete")
})
// Start your application
fmt.Println("Application starting...")
// Run the application (blocks until Exit() is called)
exitCause := ex.Run()
fmt.Printf("Application exited: %v\n", exitCause)
}package main
import (
"context"
"fmt"
"syscall"
"time"
"github.com/struct0x/exitplan"
)
func main() {
// Create a new Exitplan instance with options
ex := exitplan.New(
exitplan.WithSignal(syscall.SIGINT, syscall.SIGTERM),
exitplan.WithTeardownTimeout(10*time.Second),
exitplan.WithExitError(func(err error) {
fmt.Printf("Error during shutdown: %v\n", err)
}),
)
// Signal readiness when Run() starts
go func() {
select {
case <-ex.Started():
fmt.Println("Application is now running")
case <-ex.Stopping():
fmt.Println("Application is shutting down before it was ready")
}
// e.g., signal readiness probe, notify dependent services
}()
// Initialize resources before Run()
// Use context.WithTimeout() if you need bounded initialization
// ctx, cancel := context.WithTimeout(ex.Context(), 5*time.Second)
// defer cancel()
// err := db.Ping(ctx)
// Register cleanup with context awareness
ex.OnExitWithContext(func(ctx context.Context) {
fmt.Println("Starting cleanup...")
select {
case <-time.After(2 * time.Second):
fmt.Println("Cleanup completed successfully")
case <-ctx.Done():
fmt.Println("Cleanup was interrupted by timeout")
}
})
// Register cleanup that might return an error
ex.OnExitWithContextError(func(ctx context.Context) error {
fmt.Println("Closing database connection...")
time.Sleep(1 * time.Second)
return nil
})
// Register an async cleanup task
ex.OnExit(func() {
fmt.Println("Performing async cleanup...")
time.Sleep(3 * time.Second)
fmt.Println("Async cleanup complete")
}, exitplan.Async)
// Start your application
fmt.Println("Application starting...")
// Get the running context to use in your application
ctx := ex.Context()
// Start a worker that respects the application lifecycle
workerDone := make(chan struct{})
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Worker shutting down...")
time.Sleep(100 * time.Millisecond) // Simulate some teardown work
close(workerDone)
return
case <-time.After(1 * time.Second):
fmt.Println("Worker doing work...")
}
}
}()
ex.OnExitWithContext(func(ctx context.Context) {
select {
case <-workerDone:
fmt.Println("Worker shutdown complete")
case <-ctx.Done():
fmt.Println("Worker shutdown interrupted")
}
})
// Run the application (blocks until Exit() is called)
exitCause := ex.Run()
fmt.Printf("Application exited: %v\n", exitCause)
}If initialization fails, use Exit() to short-circuit and unwind callbacks without calling Run():
package main
import (
"fmt"
"syscall"
"time"
"github.com/struct0x/exitplan"
)
func run() error {
ex := exitplan.New(
exitplan.WithSignal(syscall.SIGINT, syscall.SIGTERM),
exitplan.WithTeardownTimeout(5*time.Second),
)
db, err := connectDB()
if err != nil {
return ex.Exit(err) // teardown runs, then returns err
}
ex.OnExit(func() { db.Close() })
cache, err := connectCache()
if err != nil {
return ex.Exit(err) // db.Close() runs, then returns err
}
ex.OnExit(func() { cache.Close() })
return ex.Run()
}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.