package supervisor import ( "encoding/json" "errors" "os" "path/filepath" ) // DurableStore is supervisor-owned external control-plane state. It is not a // candidate Assistant process-local recovery cache. type DurableStore struct{ path string } func NewDurableStore(path string) (*DurableStore, error) { if path == "" || !filepath.IsAbs(path) { return nil, errors.New("supervisor state path must be absolute") } return &DurableStore{path: path}, nil } func (s *DurableStore) Save(value Lifeboat) error { if s == nil || s.path == "" { return errors.New("supervisor state store is unavailable") } payload, err := json.Marshal(value) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(s.path), 0700); err != nil { return err } temporary, err := os.CreateTemp(filepath.Dir(s.path), ".lifeboat-*.tmp") if err != nil { return err } temporaryName := temporary.Name() defer os.Remove(temporaryName) if err = temporary.Chmod(0600); err == nil { _, err = temporary.Write(payload) } if closeErr := temporary.Close(); err == nil { err = closeErr } if err != nil { return err } return os.Rename(temporaryName, s.path) } func (s *DurableStore) Load() (Lifeboat, error) { if s == nil || s.path == "" { return Lifeboat{}, errors.New("supervisor state store is unavailable") } payload, err := os.ReadFile(s.path) if err != nil { return Lifeboat{}, err } var value Lifeboat if err := json.Unmarshal(payload, &value); err != nil { return Lifeboat{}, err } return value, nil } // PersistentLifeboat reloads before every transition and commits after it. type PersistentLifeboat struct{ store *DurableStore } func NewPersistentLifeboat(store *DurableStore) *PersistentLifeboat { return &PersistentLifeboat{store: store} } func (p *PersistentLifeboat) transition(fn func(*Lifeboat) bool) (bool, error) { value, err := p.store.Load() if err != nil && !os.IsNotExist(err) { return false, err } ok := fn(&value) if !ok { return false, nil } if err := p.store.Save(value); err != nil { return false, err } return true, nil } func (p *PersistentLifeboat) Start(candidate Release) (bool, error) { return p.transition(func(value *Lifeboat) bool { return value.Start(candidate) }) } func (p *PersistentLifeboat) Record(observation Observation) (bool, error) { return p.transition(func(value *Lifeboat) bool { return value.Record(observation) }) } func (p *PersistentLifeboat) Rollback() (bool, error) { return p.transition(func(value *Lifeboat) bool { return value.Rollback() }) } func (p *PersistentLifeboat) Snapshot() (Lifeboat, error) { return p.store.Load() }