supervise providers and reflect terminal state

This commit is contained in:
kami
2026-07-26 20:30:19 +04:00
parent ad32f29cd5
commit ae4d3a57bc
3 changed files with 114 additions and 17 deletions
+84
View File
@@ -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) {