180 lines
4.5 KiB
Go
180 lines
4.5 KiB
Go
package host
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Host owns the application-layer lifecycle of shared runtime dependencies.
|
|
// Components are started in declared order; the first failure cancels already
|
|
// started components and records the failing component. Stop shuts components
|
|
// down in reverse order and preserves every individual error. Repeated Stop
|
|
// calls are safe and return nil once the host has fully stopped.
|
|
type Host struct {
|
|
lifecycleMu sync.Mutex
|
|
mu sync.RWMutex
|
|
stopRequested bool
|
|
comps []namedComponent
|
|
started []namedComponent
|
|
cancel context.CancelFunc
|
|
status Status
|
|
}
|
|
|
|
type namedComponent struct {
|
|
name string
|
|
comp Component
|
|
}
|
|
|
|
// Namer is an optional extension of Component that exposes a human-readable
|
|
// name for Status reporting. Components that do not implement Namer receive
|
|
// a synthetic name from the host.
|
|
type Namer interface {
|
|
Name() string
|
|
}
|
|
|
|
// NewHost creates a Host that will manage the given components. Components
|
|
// are recorded in the order supplied; Start runs them in that order.
|
|
func NewHost(components ...Component) *Host {
|
|
h := &Host{}
|
|
for i, c := range components {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
name := resolveName(c, i)
|
|
h.comps = append(h.comps, namedComponent{name: name, comp: c})
|
|
}
|
|
return h
|
|
}
|
|
|
|
// resolveName returns the component's Name if it implements Namer, or a
|
|
// synthetic fallback.
|
|
func resolveName(c Component, idx int) string {
|
|
if namer, ok := c.(Namer); ok && namer.Name() != "" {
|
|
return namer.Name()
|
|
}
|
|
return "component-" + strings.Repeat("x", idx)
|
|
}
|
|
|
|
// Status returns a snapshot of the host's current lifecycle state.
|
|
func (h *Host) Status() Status {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
status := h.status
|
|
status.Started = append([]string(nil), h.status.Started...)
|
|
return status
|
|
}
|
|
|
|
// Start launches every managed component in declared order. On the first
|
|
// failure it cancels the context for the remaining components, rolls back the
|
|
// already-started ones (in reverse), and records the failing component name
|
|
// and error.
|
|
func (h *Host) Start(ctx context.Context) error {
|
|
h.lifecycleMu.Lock()
|
|
defer h.lifecycleMu.Unlock()
|
|
|
|
h.mu.Lock()
|
|
if h.status.Stopped || h.stopRequested {
|
|
h.mu.Unlock()
|
|
return errors.New("host: cannot start after stop")
|
|
}
|
|
if h.status.LaunchErr != nil {
|
|
err := h.status.LaunchErr
|
|
h.mu.Unlock()
|
|
return err
|
|
}
|
|
if len(h.started) > 0 {
|
|
h.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
h.started = nil
|
|
runCtx, cancel := context.WithCancel(ctx)
|
|
h.cancel = cancel
|
|
h.mu.Unlock()
|
|
|
|
started := make([]namedComponent, 0, len(h.comps))
|
|
for _, nc := range h.comps {
|
|
if err := nc.comp.Start(runCtx); err != nil {
|
|
// Cancel first so every already-started component observes the
|
|
// failed launch before rollback begins.
|
|
cancel()
|
|
|
|
rollbackCtx := context.WithoutCancel(ctx)
|
|
var rollbackErrs []error
|
|
for i := len(started) - 1; i >= 0; i-- {
|
|
if rollbackErr := started[i].comp.Stop(rollbackCtx); rollbackErr != nil {
|
|
rollbackErrs = append(rollbackErrs, rollbackErr)
|
|
}
|
|
}
|
|
rollbackErr := errors.Join(rollbackErrs...)
|
|
|
|
h.mu.Lock()
|
|
h.status.Failed = nc.name
|
|
h.status.LaunchErr = err
|
|
h.status.StopErr = rollbackErr
|
|
h.status.Stopped = true
|
|
h.status.Started = nil
|
|
h.started = nil
|
|
h.cancel = nil
|
|
h.mu.Unlock()
|
|
|
|
if rollbackErr != nil {
|
|
return errors.Join(err, rollbackErr)
|
|
}
|
|
return err
|
|
}
|
|
started = append(started, nc)
|
|
}
|
|
|
|
h.mu.Lock()
|
|
h.started = append([]namedComponent(nil), started...)
|
|
h.status.Started = make([]string, 0, len(started))
|
|
for _, nc := range started {
|
|
h.status.Started = append(h.status.Started, nc.name)
|
|
}
|
|
h.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Stop shuts down every started component in reverse order. It preserves
|
|
// every individual error by combining them with errors.Join. Repeated calls
|
|
// return nil once the host has fully stopped.
|
|
func (h *Host) Stop(ctx context.Context) error {
|
|
h.mu.Lock()
|
|
if h.status.Stopped {
|
|
h.mu.Unlock()
|
|
return nil
|
|
}
|
|
h.stopRequested = true
|
|
cancel := h.cancel
|
|
h.mu.Unlock()
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
|
|
h.lifecycleMu.Lock()
|
|
defer h.lifecycleMu.Unlock()
|
|
|
|
started := append([]namedComponent(nil), h.started...)
|
|
|
|
var errs []error
|
|
for i := len(started) - 1; i >= 0; i-- {
|
|
rc := started[i]
|
|
if err := rc.comp.Stop(ctx); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
stopErr := errors.Join(errs...)
|
|
h.mu.Lock()
|
|
h.started = nil
|
|
h.cancel = nil
|
|
h.status.Started = nil
|
|
h.status.StopErr = stopErr
|
|
h.status.Stopped = true
|
|
h.mu.Unlock()
|
|
|
|
return stopErr
|
|
}
|