A lifecycle management library for Go applications. Unixcycle helps you manage the startup, concurrent execution, and graceful shutdown of different parts (components) of your application.
Managing the lifecycle of concurrent components (like servers, workers, background tasks) in a Go application can become cumbersome and require a lot of boilerplate code. Ensuring proper initialization, concurrent operation, and clean shutdown in response to OS signals requires careful orchestration.
Unixcycle provides a structured and centralized way to handle this:
- Define independent components, each needing at least a
Startmethod. - Optionally define
Setupfor initialization andClosefor cleanup. - Ensure components are initialized (
Setup). - Run components concurrently (
Start). - Wait for termination signals (like
SIGINT,SIGTERM). - Gracefully shut down components (
Close) in the correct order.
- Component Lifecycle Management: Manages components that implement
Start() error, and optionallySetup() errorandClose() error. - Concurrent Execution: Runs each component's
Startmethod in its own goroutine. - Ordered Operations: Executes
Setupmethods sequentially in the order added, andClosemethods sequentially in the reverse order added. - Graceful Shutdown: Listens for OS termination signals (
SIGINT,SIGTERMby default) to initiate shutdown. - Configurable Timeouts: Set deadlines for
SetupandCloseoperations to prevent hangs. - Customizable Logging: Integrates with
slog. Provide your ownslog.Handler. - Flexible Component Definition: Components primarily need
Start().SetupandCloseare detected via optional interface implementation (setupable,closable). - Helper Functions: Provides convenient helpers (
Starter,Setup,Closer,Make) for creating components from functions or structs. - Clear Signal Handling: Returns the
syscall.Signalthat triggered the shutdown, allowing for specific exit code logic.
go get github.com/theonewiththewrench/unixcycle
Here's a simplified example showing a struct-based component and a function-based component:
package main
import (
"fmt"
"log/slog"
"os"
"syscall"
"time"
unixcycle "github.com/TheOneWithTheWrench/unixcycle"
)
// Component 1: A service implementing Setup, Start, and Close
type MyService struct {
stopCh chan struct{}
}
func NewMyService() *MyService {
return &MyService{stopCh: make(chan struct{})}
}
// Setup is optional initialization logic.
func (s *MyService) Setup() error {
time.Sleep(50 * time.Millisecond) // Simulate work
return nil
}
// Start is required (unixcycle.Component interface). It should block.
func (s *MyService) Start() error {
<-s.stopCh // Block until Close signals
return nil
}
// Close is optional cleanup logic.
func (s *MyService) Close() error {
close(s.stopCh) // Signal Start to exit
time.Sleep(100 * time.Millisecond) // Simulate cleanup
return nil
}
func main() {
// Setup logger
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// Configure the manager
manager := unixcycle.NewManager(
unixcycle.WithLogger(logger),
unixcycle.WithSetupTimeout(1*time.Second),
unixcycle.WithCloseTimeout(2*time.Second),
// unixcycle.WithLifetime(unixcycle.InterruptSignal), // Default
)
// Add components using helpers
manager.
// Use Make for struct pointers. It checks for Setup/Start/Close methods.
Add("MyService", unixcycle.Make[MyService](NewMyService())).
// Run blocks until a signal or error occurs.
signal := manager.Run()
os.Exit(exitCode)
}unixcycle.NewManager(options ...managerOption) *Manager: Creates a new lifecycle manager. Accepts functional options for configuration.manager.Add(name string, component Component) *Manager: Registers a component. Thenameis for logging.componentmust satisfy theunixcycle.Componentinterface.manager.Run() syscall.Signal: Starts the managed lifecycle:- Calls
Setup()sequentially on components implementingsetupable. - Calls
Start()concurrently on all components. - Waits for a termination signal (via
Lifetimeoption). - Calls
Close()sequentially (in reverse add order) on components implementingclosable.
- Returns the
syscall.Signalcausing shutdown or indicating an error (SIGALRMfor timeout,SIGABRTfor setup/close error).
- Calls
unixcycle.Component: The minimum interface a component must satisfy.type Component interface { Start() error }
unixcycle.setupable: Optional interface for setup logic.type setupable interface { Setup() error }
unixcycle.closable: Optional interface for cleanup logic.The manager uses type assertions to check if a registeredtype closable interface { Close() error }
Componentalso implementssetupableorclosable.
These simplify creating Component values:
unixcycle.Make[T](*T): Takes a pointer to a struct (*T). The struct must implementStart() error. If it also implementsSetup()and/orClose(), those methods will be used. This is the preferred way to add struct-based components.unixcycle.Starter(func() error): Wraps a function to create aComponentwhoseStart()method executes the function. It has noSetuporClosebehavior.unixcycle.Setup(func() error): Wraps a function to create aComponentwhoseSetup()method executes the function. ItsStart()is a no-op. It has noClosebehavior. Useful for initialization-only tasks.unixcycle.Closer(func() error): Wraps a function to create aComponentwhoseClose()method executes the function. ItsStart()is a no-op. It has noSetupbehavior. Useful for cleanup-only tasks run at the end.
Pass these to NewManager using the With... functions:
unixcycle.WithLogger(logger *slog.Logger): Sets thesloglogger for logging. Ifnil, logging is disabled (sent toio.Discard). Defaults to a text handler writing toos.Stdout.unixcycle.WithSetupTimeout(time.Duration): Timeout for each component'sSetup()call. Defaults to 5 seconds.unixcycle.WithCloseTimeout(time.Duration): Timeout for each component'sClose()call. Defaults to 5 seconds.unixcycle.WithLifetime(unixcycle.TerminationSignal): A functionfunc() syscall.Signalthat blocks until termination is requested. Defaults tounixcycle.InterruptSignal(waits forSIGINTorSIGTERM).
- Setup/Close Errors: If
SetuporClosereturns an error, the manager stops immediately, skips subsequent steps in that phase, andRun()returnssyscall.SIGABRT. - Setup/Close Timeouts: If
SetuporCloseexceeds its timeout, the manager stops, andRun()returnssyscall.SIGALRM. - Start Errors: Errors returned from
Start()are logged. They do not automatically stop other components or trigger manager shutdown. The goroutine for the failing component exits. Implement cross-component error handling if needed (e.g., using shared channels or context cancellation propagated from the manager). - Termination Signals:
SIGINT/SIGTERM(by default) trigger graceful shutdown.Run()returns the received signal.
Contributions are welcome! Please feel free to submit issues and pull requests.
- More Tests: Add more tests to ensure reliability and robustness.
This project is licensed under the MIT License - see the LICENSE file for details.