iop/apps/agent/internal/host/host.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
}