A service supervisor for Go applications that manages lifecycle for multiple services. Handles graceful shutdown with OS signal support (SIGINT, SIGTERM), configuration hot reloading (SIGHUP), and state monitoring. Service capabilities are added by implementing optional interfaces.
- Service Lifecycle Management: Start, stop, and monitor multiple services
- Graceful Shutdown: Handle OS signals (SIGINT, SIGTERM) for clean termination
- Hot Reloading: Reload service configurations with SIGHUP or programmatically
- State Monitoring: Track and query the state of running services
- Context Propagation: Pass context through service lifecycle for proper cancellation
- Structured Logging: Integrated with Go's
slogpackage - Flexible Configuration: Functional options pattern for easy customization
go get github.com/robbyt/go-supervisorDefine a runnable service by implementing the Runnable interface with Run(ctx context.Context) error and Stop() methods. Additional capabilities (Reloadable, Stateable) can be implemented as needed. See supervisor/interfaces.go for interface details.
package main
import (
"context"
"fmt"
"log/slog"
"os"
"time"
"github.com/robbyt/go-supervisor/supervisor"
)
// Example service that implements Runnable interface
type MyService struct {
name string
}
// Interface guard, ensuring that MyService implements Runnable
var _ supervisor.Runnable = (*MyService)(nil)
func (s *MyService) Run(ctx context.Context) error {
fmt.Printf("%s: Starting\n", s.name)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
fmt.Printf("%s: Context canceled\n", s.name)
return nil
case <-ticker.C:
fmt.Printf("%s: Tick\n", s.name)
}
}
}
func (s *MyService) Stop() {
fmt.Printf("%s: Stopping\n", s.name)
// Perform cleanup if needed
}
func (s *MyService) String() string {
return s.name
}
func main() {
// Create some services
service1 := &MyService{name: "Service1"}
service2 := &MyService{name: "Service2"}
// Create a custom logger
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
// Create a supervisor with our services and custom logger
super, err := supervisor.New(
supervisor.WithRunnables(service1, service2),
supervisor.WithLogHandler(handler),
)
if err != nil {
fmt.Printf("Error creating supervisor: %v\n", err)
os.Exit(1)
}
// Blocking call to Run(), starts listening to signals and starts all Runnables
if err := super.Run(); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}The package is built around the following interfaces. A "Runnable" is any service that can be started and stopped, while "Reloadable" and "Stateable" services can be reloaded or report their state, respectively. The supervisor will discover the capabilities of each service and manage them accordingly.
// Runnable represents a service that can be run and stopped.
// Run blocks until the service exits; Stop blocks until Run returns.
type Runnable interface {
fmt.Stringer
Run(ctx context.Context) error
Stop()
}
// Reloadable represents a service that can be reloaded.
// Reload blocks until the reload completes (or aborts via ctx). A non-nil
// return reports failure. Implementations that ALSO implement Stateable
// are encouraged to mirror failures via an FSM Error transition so
// state-channel observers see the same outcome; plain Reloadable
// implementations have no FSM and only need to return the error.
type Reloadable interface {
Reload(ctx context.Context) error
}
// Stateable represents a service that reports its current state.
// Orthogonal to Readiness: implement Stateable for state-channel
// observability, Readiness for the startup gate, or both.
type Stateable interface {
GetState() string
GetStateChan(context.Context) <-chan string
}
// Readiness reports whether the service has finished its startup phase.
// The supervisor's Run() polls IsReady on every Readiness runnable before
// proceeding to spawn the next.
type Readiness interface {
IsReady() bool
}
// ReloadSender lets a service trigger reloads from inside.
type ReloadSender interface {
GetReloadTrigger() <-chan struct{}
}
// ShutdownSender lets a service trigger supervisor-wide shutdown from inside.
type ShutdownSender interface {
GetShutdownTrigger() <-chan struct{}
}Capabilities are detected by interface assertion — implement only what you need.
WithRunnables(...)— register the services to manage.WithContext(ctx)— provide a parent context for cancellation.WithLogHandler(h)— install a customslog.Handler.WithSignals(...)— override the OS signals to listen for. OnlySIGINT,SIGTERM, andSIGHUPare special-cased; other signals are logged and ignored.WithStartupTimeout(d)— bound how long a runnable'sIsReady()may take to return true before the supervisor gives up on startup.WithShutdownTimeout(d)— TOTAL wall-clock budget for graceful shutdown, shared between per-runnableStop()calls and the final goroutine wait. A runnable whoseStop()overruns the remaining budget is abandoned (logged warning).0disables the deadline.
type ConfigurableService struct {
MyService
config *Config
mu sync.Mutex
}
// Interface guards, ensuring that ConfigurableService implements Runnable and Reloadable
var _ supervisor.Runnable = (*ConfigurableService)(nil)
var _ supervisor.Reloadable = (*ConfigurableService)(nil)
type Config struct {
Interval time.Duration
}
func (s *ConfigurableService) Reload(ctx context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
// Load new config from file or environment
newConfig, err := loadConfig()
if err != nil {
return err
}
s.config = newConfig
fmt.Printf("%s: Configuration reloaded\n", s.name)
return nil
}The package includes the following runnable implementations:
- HTTP Server: A configurable HTTP server with routing and middleware support (
runnables/httpserver) - Composite: A container for managing multiple Runnables using generics (
runnables/composite) - HTTP Cluster: Dynamic management of multiple HTTP servers with hot-reload support using channel-based configuration (
runnables/httpcluster)
Each runnable has its own documentation in its directory (e.g., runnables/httpserver/README.md).
Apache License 2.0 - See LICENSE for details.