155 lines
4.6 KiB
Go
155 lines
4.6 KiB
Go
package supervisor
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
)
|
|
|
|
// 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
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func NewPersistentLifeboat(store *DurableStore) *PersistentLifeboat {
|
|
return &PersistentLifeboat{store: store}
|
|
}
|
|
func (p *PersistentLifeboat) transition(fn func(*Lifeboat) bool) (bool, error) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
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) StartSet(candidate ReleaseSet) (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.StartSet(candidate) })
|
|
}
|
|
|
|
func (p *PersistentLifeboat) EstablishBaseline(release Release) (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.EstablishBaseline(release) })
|
|
}
|
|
|
|
func (p *PersistentLifeboat) EstablishBaselineSet(release ReleaseSet) (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.EstablishBaselineSet(release) })
|
|
}
|
|
func (p *PersistentLifeboat) Record(observation Observation) (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.Record(observation) })
|
|
}
|
|
|
|
func (p *PersistentLifeboat) RecordSet(observation ReleaseSetObservation) (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.RecordSet(observation) })
|
|
}
|
|
func (p *PersistentLifeboat) Rollback() (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.Rollback() })
|
|
}
|
|
func (p *PersistentLifeboat) SetTraffic(mode TrafficMode, stableUpstream, candidateUpstream string) (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.SetTraffic(mode, stableUpstream, candidateUpstream) })
|
|
}
|
|
|
|
func (p *PersistentLifeboat) SetTrafficForSets(mode TrafficMode) (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.SetTrafficForSets(mode) })
|
|
}
|
|
|
|
func (p *PersistentLifeboat) Promote() (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool {
|
|
if value.Current.State != ReleaseHealthy || value.Current.ID == "" {
|
|
return false
|
|
}
|
|
return value.SetTraffic(TrafficCandidate, value.StableUpstream, value.CandidateUpstream)
|
|
})
|
|
}
|
|
|
|
func (p *PersistentLifeboat) PromoteSet() (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.PromoteSet() })
|
|
}
|
|
|
|
func (p *PersistentLifeboat) RollbackTraffic() (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool {
|
|
if !value.Rollback() {
|
|
return false
|
|
}
|
|
return value.SetTraffic(TrafficStable, value.StableUpstream, value.CandidateUpstream)
|
|
})
|
|
}
|
|
|
|
func (p *PersistentLifeboat) RollbackSet() (bool, error) {
|
|
return p.transition(func(value *Lifeboat) bool { return value.RollbackSet() })
|
|
}
|
|
|
|
func (p *PersistentLifeboat) Snapshot() (Lifeboat, error) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.store.Load()
|
|
}
|