Files
olcrtc/internal/supervisor/supervisor.go
2026-05-16 02:10:33 +03:00

97 lines
2.4 KiB
Go

// Package supervisor runs ordered session profiles with failover.
package supervisor
import (
"context"
"errors"
"fmt"
"time"
"github.com/openlibrecommunity/olcrtc/internal/app/session"
)
const DefaultRetryDelay = 2 * time.Second
var (
// ErrNoProfiles is returned when the supervisor is started without profiles.
ErrNoProfiles = errors.New("supervisor: no profiles configured")
// ErrMaxCyclesExceeded is returned after MaxCycles complete profile-list passes.
ErrMaxCyclesExceeded = errors.New("supervisor: max failover cycles exceeded")
)
// Profile is one runnable session configuration in an ordered failover list.
type Profile struct {
Name string
Config session.Config
}
// Runner starts one session profile and blocks until it ends or fails.
type Runner func(ctx context.Context, cfg session.Config) error
// Config controls ordered failover behavior.
type Config struct {
Profiles []Profile
RetryDelay time.Duration
MaxCycles int
OnProfileStart func(profile Profile, cycle int)
OnProfileEnd func(profile Profile, cycle int, err error)
}
// Run starts profiles in order. If a profile exits while ctx is still active,
// the supervisor waits RetryDelay and advances to the next profile.
func Run(ctx context.Context, cfg Config, run Runner) error {
if len(cfg.Profiles) == 0 {
return ErrNoProfiles
}
if cfg.RetryDelay == 0 {
cfg.RetryDelay = DefaultRetryDelay
}
var lastErr error
for cycle := 1; ; cycle++ {
for i, profile := range cfg.Profiles {
if ctx.Err() != nil {
return nil
}
if cfg.OnProfileStart != nil {
cfg.OnProfileStart(profile, cycle)
}
err := run(ctx, profile.Config)
if ctx.Err() != nil {
return nil
}
if err != nil {
lastErr = fmt.Errorf("profile %q: %w", profile.Name, err)
} else {
lastErr = fmt.Errorf("profile %q ended", profile.Name)
}
if cfg.OnProfileEnd != nil {
cfg.OnProfileEnd(profile, cycle, err)
}
if cfg.MaxCycles > 0 && cycle >= cfg.MaxCycles && i == len(cfg.Profiles)-1 {
return fmt.Errorf("%w after %d cycle(s): %w", ErrMaxCyclesExceeded, cycle, lastErr)
}
if err := waitRetryDelay(ctx, cfg.RetryDelay); err != nil {
return nil
}
}
}
}
func waitRetryDelay(ctx context.Context, delay time.Duration) error {
if delay <= 0 {
return nil
}
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}