169 lines
4.0 KiB
Go
169 lines
4.0 KiB
Go
// Package app wires the Notification Service process lifecycle and
|
|
// coordinates component startup and graceful shutdown.
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"galaxy/notification/internal/config"
|
|
)
|
|
|
|
// Component is a long-lived Notification Service subsystem that participates
|
|
// in coordinated startup and graceful shutdown.
|
|
type Component interface {
|
|
// Run starts the component and blocks until it stops.
|
|
Run(context.Context) error
|
|
|
|
// Shutdown stops the component within the provided timeout-bounded context.
|
|
Shutdown(context.Context) error
|
|
}
|
|
|
|
// App owns the process-level lifecycle of Notification Service and its
|
|
// registered components.
|
|
type App struct {
|
|
cfg config.Config
|
|
components []Component
|
|
}
|
|
|
|
// New constructs App with a defensive copy of the supplied components.
|
|
func New(cfg config.Config, components ...Component) *App {
|
|
clonedComponents := append([]Component(nil), components...)
|
|
|
|
return &App{
|
|
cfg: cfg,
|
|
components: clonedComponents,
|
|
}
|
|
}
|
|
|
|
// Run starts all configured components, waits for cancellation or the first
|
|
// component failure, and then executes best-effort graceful shutdown.
|
|
func (app *App) Run(ctx context.Context) error {
|
|
if ctx == nil {
|
|
return errors.New("run notification app: nil context")
|
|
}
|
|
if err := app.validate(); err != nil {
|
|
return err
|
|
}
|
|
if len(app.components) == 0 {
|
|
<-ctx.Done()
|
|
return nil
|
|
}
|
|
|
|
runCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
results := make(chan componentResult, len(app.components))
|
|
var runWaitGroup sync.WaitGroup
|
|
|
|
for index, component := range app.components {
|
|
runWaitGroup.Add(1)
|
|
|
|
go func(componentIndex int, component Component) {
|
|
defer runWaitGroup.Done()
|
|
results <- componentResult{
|
|
index: componentIndex,
|
|
err: component.Run(runCtx),
|
|
}
|
|
}(index, component)
|
|
}
|
|
|
|
var runErr error
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
case result := <-results:
|
|
runErr = classifyComponentResult(ctx, result)
|
|
}
|
|
|
|
cancel()
|
|
|
|
shutdownErr := app.shutdownComponents()
|
|
waitErr := app.waitForComponents(&runWaitGroup)
|
|
|
|
return errors.Join(runErr, shutdownErr, waitErr)
|
|
}
|
|
|
|
type componentResult struct {
|
|
index int
|
|
err error
|
|
}
|
|
|
|
func (app *App) validate() error {
|
|
if app.cfg.ShutdownTimeout <= 0 {
|
|
return fmt.Errorf("run notification app: shutdown timeout must be positive, got %s", app.cfg.ShutdownTimeout)
|
|
}
|
|
|
|
for index, component := range app.components {
|
|
if component == nil {
|
|
return fmt.Errorf("run notification app: component %d is nil", index)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func classifyComponentResult(parentCtx context.Context, result componentResult) error {
|
|
switch {
|
|
case result.err == nil:
|
|
if parentCtx.Err() != nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("run notification app: component %d exited without error before shutdown", result.index)
|
|
case errors.Is(result.err, context.Canceled) && parentCtx.Err() != nil:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("run notification app: component %d: %w", result.index, result.err)
|
|
}
|
|
}
|
|
|
|
func (app *App) shutdownComponents() error {
|
|
var shutdownWaitGroup sync.WaitGroup
|
|
errs := make(chan error, len(app.components))
|
|
|
|
for index, component := range app.components {
|
|
shutdownWaitGroup.Add(1)
|
|
|
|
go func(componentIndex int, component Component) {
|
|
defer shutdownWaitGroup.Done()
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), app.cfg.ShutdownTimeout)
|
|
defer cancel()
|
|
|
|
if err := component.Shutdown(shutdownCtx); err != nil {
|
|
errs <- fmt.Errorf("shutdown notification component %d: %w", componentIndex, err)
|
|
}
|
|
}(index, component)
|
|
}
|
|
|
|
shutdownWaitGroup.Wait()
|
|
close(errs)
|
|
|
|
var joined error
|
|
for err := range errs {
|
|
joined = errors.Join(joined, err)
|
|
}
|
|
|
|
return joined
|
|
}
|
|
|
|
func (app *App) waitForComponents(runWaitGroup *sync.WaitGroup) error {
|
|
done := make(chan struct{})
|
|
go func() {
|
|
runWaitGroup.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
waitCtx, cancel := context.WithTimeout(context.Background(), app.cfg.ShutdownTimeout)
|
|
defer cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
return nil
|
|
case <-waitCtx.Done():
|
|
return fmt.Errorf("wait for notification components: %w", waitCtx.Err())
|
|
}
|
|
}
|