exitplan

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2025 License: MIT Imports: 8 Imported by: 0

README

Exitplan

A Go library for managing the lifecycle of an application with graceful shutdown capabilities.

Go Reference

Overview

The Exitplan library provides a simple mechanism for managing the lifetime of an application. It helps you handle application startup, running, and shutdown phases with proper resource cleanup.

Key features include:

  • Distinct application lifecycle phases (starting, 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

Installation

go get github.com/struct0x/exitplan

Lifecycle Phases

Exitplan splits application lifetime into three phases, each with its own context:

  • Starting: before Run() begins. Use StartingContext() for initialization.
    It is canceled immediately when Run() starts.

  • Running: active between Run() and Exit(). Use Context() for workers and other long-running tasks.
    It is canceled as soon as shutdown begins.

  • Teardown: after Exit() is called. Use TeardownContext() in shutdown callbacks.
    It is canceled when the global teardown timeout elapses.

Callback ordering

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.

Usage

Basic Example
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)
}

Advanced Example with Context
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)
		}),
	)

	// Use the starting context for initialization
	startingCtx := ex.StartingContext()
	_ = startingCtx
	// Initialize resources with the starting context

	// For example, pinging a database connection to ensure it is ready, yet it should not freeze the application
	// err := db.Ping(startingCtx)

	// 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
	runningCtx := ex.Context()

	// Start a worker that respects the application lifecycle
	workerDone := make(chan struct{})
	go func() {
		for {
			select {
			case <-runningCtx.Done():
				fmt.Println("Worker shutting down...")
				time.Sleep(100 * time.Millisecond) // Simulate some 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)
}

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Documentation

Overview

Package exitplan implements a simple mechanism for managing a lifetime of an application. It provides a way to register functions that will be called when the application is about to exit. It distinguishes between starting, running and teardown phases.

The application is considered to be starting before calling Exitplan.Run(). You can use Exitplan.StartingContext() to get a context that can be used to control the startup phase. Starting context is canceled when the startup phase is over.

The application is considered to be running after calling Exitplan.Run() and before calling Exitplan.Exit(). You can use Exitplan.Context() to get a context that can be used to control the running phase. It is canceled when the application is about to exit.

The application is considered to be tearing down after calling Exitplan.Exit(). You can use Exitplan.TeardownContext() to get a context that can be used to control the teardown phase. It is canceled when the teardown timeout is reached.

Ordering of shutdown callbacks Exitplan executes registered exit callbacks in LIFO order (last registered, first executed). Async only offloads the execution to a goroutine, but Exitplan still waits for all callbacks up to the teardown timeout.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrSignaled indicates that the application received a termination signal
	ErrSignaled = errors.New("exitplan: received signal")

	// ErrGracefulShutdown indicates that the application was requested to shut down gracefully
	ErrGracefulShutdown = errors.New("exitplan: graceful shutdown requested")

	// ErrStartupTimeout indicates that the application failed to start within the configured timeout
	ErrStartupTimeout = errors.New("exitplan: startup phase timeout exceeded")
)

Functions

func Async

func Async(c *callback)

Async sets the callback to be executed in a separate goroutine. Exitplan will wait for all Async callbacks to complete before exiting. Use Timeout to bound the callback.

func PanicOnError

func PanicOnError(c *callback)

PanicOnError sets the callback to panic with the error returned by the callback.

func Timeout

func Timeout(timeout time.Duration) exitCallbackOpt

Timeout sets the timeout for the callback.

func WithExitError

func WithExitError(callback func(error)) opt

WithExitError sets the callback that will be called when an error occurs during the teardown phase. It will be called for every error that occurs during the teardown phase.

func WithSignal

func WithSignal(s1 os.Signal, sMany ...os.Signal) opt

WithSignal calls Exitplan.Exit() when the specified signal is received.

func WithStartupTimeout

func WithStartupTimeout(timeout time.Duration) opt

WithStartupTimeout sets the timeout for the starting phase.

func WithTeardownTimeout

func WithTeardownTimeout(timeout time.Duration) opt

WithTeardownTimeout sets the timeout for the teardown phase.

Types

type Exitplan

type Exitplan struct {
	// contains filtered or unexported fields
}

func New

func New(opts ...opt) *Exitplan

New creates a new instance of Exitplan. It will start in the starting phase. Use the With* options to configure it.

func (*Exitplan) Context

func (l *Exitplan) Context() context.Context

Context returns a main context. IT will be canceled when the application is about to exit. It can be used to control the lifetime of the application. It will be canceled after calling Exitplan.Exit().

func (*Exitplan) Exit

func (l *Exitplan) Exit(reason error)

Exit stops the application. It will cause the application to exit with the specified reason. If the reason is nil, ErrGracefulShutdown will be used. Multiple calls to Exit are safe, but only the first one will have an effect.

func (*Exitplan) OnExit

func (l *Exitplan) OnExit(callback func(), exitOpts ...exitCallbackOpt)

OnExit registers a callback that will be called when the application is about to exit. Has no effect after calling Exitplan.Run(). Use exitplan.Async option to execute the callback in a separate goroutine. exitplan.PanicOnError has no effect on this function. See also: OnExitWithError, OnExitWithContext, OnExitWithContextError.

func (*Exitplan) OnExitWithContext

func (l *Exitplan) OnExitWithContext(callback func(context.Context), exitOpts ...exitCallbackOpt)

OnExitWithContext registers a callback that will be called when the application is about to exit. Has no effect after calling Exitplan.Run(). The callback will receive a context that will be canceled after the teardown timeout. Use exitplan.Async option to execute the callback in a separate goroutine. exitplan.PanicOnError has no effect on this function.

func (*Exitplan) OnExitWithContextError

func (l *Exitplan) OnExitWithContextError(callback func(context.Context) error, exitOpts ...exitCallbackOpt)

OnExitWithContextError registers a callback that will be called when the application is about to exit. Has no effect after calling Exitplan.Run(). The callback will receive a context that will be canceled after the teardown timeout. The callback can return an error that will be passed to the error handler. Use exitplan.Async option to execute the callback in a separate goroutine. Use exitplan.PanicOnError to panic with the error returned by the callback.

func (*Exitplan) OnExitWithError

func (l *Exitplan) OnExitWithError(callback func() error, exitOpts ...exitCallbackOpt)

OnExitWithError registers a callback that will be called when the application is about to exit. Has no effect after calling Exitplan.Run(). The callback can return an error that will be passed to the error handler. Use exitplan.Async option to execute the callback in a separate goroutine. Use exitplan.PanicOnError to panic with the error returned by the callback.

func (*Exitplan) Run

func (l *Exitplan) Run() (exitCause error)

Run starts the application. It will block until the application is stopped by calling Exit. It will also block until all the registered callbacks are executed. If the teardown timeout is set, it will be used to cancel the context passed to the callbacks. Returns the error that caused the application to stop.

func (*Exitplan) StartingContext

func (l *Exitplan) StartingContext() context.Context

StartingContext returns a context for a starting phase. It can be used to control the startup of the application. StartingContext will be canceled after the starting timeout or when Exitplan.Run() is called.

func (*Exitplan) TeardownContext

func (l *Exitplan) TeardownContext() context.Context

TeardownContext returns a teardown context. It will be canceled after the teardown timeout. It can be used to control the shutdown of the application. This context is the same as the one passed to the callbacks registered with OnExit* methods.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL