143 lines
3.2 KiB
Go
143 lines
3.2 KiB
Go
// Package lifecycle provides application lifecycle management using tomb
|
|
package lifecycle
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"go.uber.org/multierr"
|
|
"gopkg.in/tomb.v2"
|
|
|
|
"starter-kit/internal/helper/logger"
|
|
)
|
|
|
|
// Lifecycle manages the application lifecycle
|
|
type Lifecycle struct {
|
|
tomb tomb.Tomb
|
|
shutdownTimeout time.Duration
|
|
mu sync.Mutex
|
|
services []Service
|
|
}
|
|
|
|
// Service represents a service that can be started and stopped
|
|
type Service interface {
|
|
Name() string
|
|
Start() error
|
|
Shutdown(ctx context.Context) error
|
|
}
|
|
|
|
// New creates a new Lifecycle with the given shutdown timeout
|
|
func New(shutdownTimeout time.Duration) *Lifecycle {
|
|
return &Lifecycle{
|
|
shutdownTimeout: shutdownTimeout,
|
|
services: make([]Service, 0),
|
|
}
|
|
}
|
|
|
|
// Register adds a service to the lifecycle manager
|
|
func (l *Lifecycle) Register(service Service) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.services = append(l.services, service)
|
|
}
|
|
|
|
// Start starts all registered services
|
|
func (l *Lifecycle) Start() error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
// Start all services
|
|
for _, svc := range l.services {
|
|
service := svc // Create a new variable for the closure
|
|
l.tomb.Go(func() error {
|
|
logger.Info("Starting service: ", service.Name())
|
|
if err := service.Start(); err != nil {
|
|
logger.Errorf("Service %s failed to start: %v", service.Name(), err)
|
|
return err
|
|
}
|
|
logger.Info("Service started: ", service.Name())
|
|
return nil
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Wait blocks until all services have stopped
|
|
func (l *Lifecycle) Wait() error {
|
|
return l.tomb.Wait()
|
|
}
|
|
|
|
// Shutdown gracefully shuts down all services
|
|
func (l *Lifecycle) Shutdown() error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
logger.Info("Initiating graceful shutdown...")
|
|
|
|
// Create a context with timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), l.shutdownTimeout*time.Second)
|
|
defer cancel()
|
|
|
|
// Notify all services to shut down
|
|
l.tomb.Kill(nil)
|
|
|
|
// Shutdown all services in reverse order
|
|
var errs error
|
|
for i := len(l.services) - 1; i >= 0; i-- {
|
|
svc := l.services[i]
|
|
logger.Infof("Shutting down service: %s", svc.Name())
|
|
if err := svc.Shutdown(ctx); err != nil {
|
|
logger.Errorf("Error shutting down %s: %v", svc.Name(), err)
|
|
errs = multierr.Append(errs, err)
|
|
}
|
|
}
|
|
|
|
// Wait for all services to stop or timeout
|
|
errChan := make(chan error, 1)
|
|
go func() {
|
|
errChan <- l.tomb.Wait()
|
|
}()
|
|
|
|
select {
|
|
case err := <-errChan:
|
|
if err != nil {
|
|
errs = multierr.Append(errs, err)
|
|
}
|
|
case <-ctx.Done():
|
|
errs = multierr.Append(errs, ctx.Err())
|
|
}
|
|
|
|
if errs != nil {
|
|
logger.Errorf("Shutdown completed with errors: %v", errs)
|
|
} else {
|
|
logger.Info("Shutdown completed successfully")
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// ShutdownOnSignal shuts down the lifecycle when a signal is received
|
|
func (l *Lifecycle) ShutdownOnSignal(signals ...os.Signal) {
|
|
if len(signals) == 0 {
|
|
signals = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
|
|
}
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, signals...)
|
|
|
|
l.tomb.Go(func() error {
|
|
select {
|
|
case sig := <-sigChan:
|
|
logger.Infof("Received signal: %v. Initiating shutdown...", sig)
|
|
return l.Shutdown()
|
|
case <-l.tomb.Dying():
|
|
return nil
|
|
}
|
|
})
|
|
}
|