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 }