You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

131 lines
3.7 KiB

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
}