package main import ( "context" "errors" "fmt" "log" "tera-clone/services/tera-api/internal/serial" "tera-clone/services/tera-api/internal/sessionhealth" "tera-clone/services/tera-api/internal/startup" "time" ) func retryStartupSerial(ctx context.Context, work func() error) error { for { if err := ctx.Err(); err != nil { return err } err := work() if !errors.Is(err, serial.ErrStreamLeaseBusy) { return err } select { case <-ctx.Done(): return ctx.Err() case <-time.After(125 * time.Millisecond): } } } func logStartupTiming(stage string, elapsed time.Duration, err error) { if err != nil { log.Printf("tera-api startup stage=%s elapsed_ms=%d error=%v", stage, elapsed.Milliseconds(), err) return } log.Printf("tera-api startup stage=%s elapsed_ms=%d", stage, elapsed.Milliseconds()) } func requiredStartupTask(ctx context.Context, boot *startup.Tracker, name string, task func() error) func() { return func() { started := time.Now() err := boot.Retry(ctx, name, 25*time.Second, 3*time.Second, task, func(err error) { log.Printf("tera-api startup retry stage=%s error=%v", name, err) }) logStartupTiming("runtime."+name, time.Since(started), err) } } func waitStartupServices(ctx context.Context, ports serial.Controller, sessions sessionhealth.Controller, requireSession bool) error { for { probeCtx, cancel := context.WithTimeout(ctx, 2*time.Second) states, err := ports.States(probeCtx) if err == nil && len(states) != serial.ChannelCount { err = fmt.Errorf("serial channel count %d", len(states)) } if err == nil { for _, s := range states { if !s.Available || !s.ProfileApplied { err = fmt.Errorf("serial channel unavailable") break } } } if err == nil && requireSession { status, e := sessions.Status(probeCtx) err = e if err == nil && status.Status != "ok" { err = fmt.Errorf("session service not ready") } } cancel() if err == nil { return nil } select { case <-ctx.Done(): return fmt.Errorf("service verification: %w", err) case <-time.After(time.Second): } } } func timedStartupTask(name string, task func()) func() { return func() { started := time.Now() defer func() { logStartupTiming("runtime."+name, time.Since(started), nil) }() task() } } func verifyStartupPortObservation(ctx context.Context, ports serial.Controller, required bool, after ...time.Time) error { observer, ok := ports.(serial.CRProbeStateController) if !ok { if required { return fmt.Errorf("initial port observation unavailable") } return nil } states, err := observer.CRProbeStates(ctx) if err != nil { return err } if len(states) != serial.ChannelCount { return fmt.Errorf("initial port observation incomplete") } seen := make(map[string]bool) for _, state := range states { if state.Channel < "CH01" || state.Channel > "CH48" || seen[state.Channel] { return fmt.Errorf("invalid initial port observation") } seen[state.Channel] = true if !state.Enabled && state.State == serial.CRProbeStateDisabled { continue } if len(after) > 0 && (state.LastProbeAt == nil || state.LastProbeAt.Before(after[0])) { return fmt.Errorf("port %s awaiting observation after settings applied", state.Channel) } switch state.State { case serial.CRProbeStateResponded, serial.CRProbeStateNoResponse: if state.LastProbeAt == nil { return fmt.Errorf("port %s has no observation", state.Channel) } case serial.CRProbeStateBusy, serial.CRProbeStateSuppressed: if state.LastConfirmedAt == nil && state.ConsecutiveMisses == 0 { return fmt.Errorf("port %s awaiting first observation", state.Channel) } default: return fmt.Errorf("port %s initial observation pending (%s)", state.Channel, state.State) } } return nil }