supervise providers and reflect terminal state
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"orchestra/internal/domain"
|
||||
@@ -29,6 +30,89 @@ type TaskReflector interface {
|
||||
ReflectTask(domain.Task, domain.Event) error
|
||||
}
|
||||
|
||||
type TaskLookup interface {
|
||||
Task(string) (domain.Task, bool)
|
||||
}
|
||||
|
||||
// ReflectingSink preserves the append-first rule while asynchronously
|
||||
// reflecting terminal state to an external provider. Reflection failures are
|
||||
// returned to the caller so the supervisor can retry and surface health.
|
||||
type ReflectingSink struct {
|
||||
Sink Sink
|
||||
Tasks TaskLookup
|
||||
Reflector TaskReflector
|
||||
}
|
||||
|
||||
func (s ReflectingSink) Append(e domain.Event) error {
|
||||
if err := s.Sink.Append(e); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Reflector == nil || s.Tasks == nil {
|
||||
return nil
|
||||
}
|
||||
if t, ok := s.Tasks.Task(e.TaskID); ok {
|
||||
return s.Reflector.ReflectTask(t, e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Health struct {
|
||||
Name string `json:"name"`
|
||||
Running bool `json:"running"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Supervise restarts a provider loop with bounded backoff and exposes its
|
||||
// current health. The loop exits only when its context is cancelled.
|
||||
type Supervisor struct {
|
||||
Name string
|
||||
Run func(context.Context) error
|
||||
Backoff time.Duration
|
||||
mu sync.RWMutex
|
||||
health Health
|
||||
}
|
||||
|
||||
func (s *Supervisor) Start(ctx context.Context) {
|
||||
if s.Backoff <= 0 {
|
||||
s.Backoff = time.Second
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.health = Health{Name: s.Name, Running: true, UpdatedAt: time.Now().UTC()}
|
||||
s.mu.Unlock()
|
||||
go func() {
|
||||
for {
|
||||
err := s.Run(ctx)
|
||||
if ctx.Err() != nil {
|
||||
s.mu.Lock()
|
||||
s.health.Running = false
|
||||
s.health.UpdatedAt = time.Now().UTC()
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.health.LastError = errString(err)
|
||||
s.health.UpdatedAt = time.Now().UTC()
|
||||
s.mu.Unlock()
|
||||
t := time.NewTimer(s.Backoff)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return "provider stopped"
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
func (s *Supervisor) Health() Health { s.mu.RLock(); defer s.mu.RUnlock(); return s.health }
|
||||
|
||||
type JSONL struct{ Source string }
|
||||
|
||||
func (j JSONL) Ingest(r io.Reader, sink Sink) (int, error) {
|
||||
|
||||
Reference in New Issue
Block a user