Harden lease lifecycle durability
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"orchestra/internal/buildinfo"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -15,13 +16,15 @@ var ErrUnknownWorker = errors.New("unknown worker")
|
||||
var ErrUnauthorized = errors.New("worker authentication failed")
|
||||
|
||||
type Worker struct {
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
Capacity int `json:"capacity"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Online bool `json:"online"`
|
||||
Health WorkerHealth `json:"health"`
|
||||
Token string `json:"-"`
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
Capacity int `json:"capacity"`
|
||||
SupportedProjects []string `json:"supported_projects"`
|
||||
Build buildinfo.Info `json:"build"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Online bool `json:"online"`
|
||||
Health WorkerHealth `json:"health"`
|
||||
Token string `json:"-"`
|
||||
}
|
||||
|
||||
// WorkerHealth is reported by the worker that owns the local herdr socket.
|
||||
@@ -86,10 +89,12 @@ type persistedState struct {
|
||||
// is mode 0600, and retaining this binding prevents an arbitrary process from
|
||||
// registering a recovered worker ID and executing its pending approval.
|
||||
type persistedWorker struct {
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
Capacity int `json:"capacity"`
|
||||
Token string `json:"token"`
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
Capacity int `json:"capacity"`
|
||||
SupportedProjects []string `json:"supported_projects"`
|
||||
Build buildinfo.Info `json:"build"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// Load restores durable capture/command state. Call this before accepting
|
||||
@@ -123,7 +128,7 @@ func (r *Registry) Load() error {
|
||||
if id == "" || w.ID != id || w.Token == "" {
|
||||
return fmt.Errorf("invalid federation worker %q", id)
|
||||
}
|
||||
r.workers[id] = Worker{ID: w.ID, Address: w.Address, Capacity: w.Capacity, Token: w.Token}
|
||||
r.workers[id] = Worker{ID: w.ID, Address: w.Address, Capacity: w.Capacity, SupportedProjects: w.SupportedProjects, Build: w.Build, Token: w.Token}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -135,7 +140,7 @@ func (r *Registry) persistLocked() error {
|
||||
}
|
||||
workers := make(map[string]persistedWorker, len(r.workers))
|
||||
for id, w := range r.workers {
|
||||
workers[id] = persistedWorker{ID: w.ID, Address: w.Address, Capacity: w.Capacity, Token: w.Token}
|
||||
workers[id] = persistedWorker{ID: w.ID, Address: w.Address, Capacity: w.Capacity, SupportedProjects: w.SupportedProjects, Build: w.Build, Token: w.Token}
|
||||
}
|
||||
b, err := json.Marshal(persistedState{Captures: r.captures, Commands: r.commands, Workers: workers})
|
||||
if err != nil {
|
||||
@@ -145,12 +150,31 @@ func (r *Registry) persistLocked() error {
|
||||
return err
|
||||
}
|
||||
tmp := r.StatePath + ".tmp"
|
||||
if err := os.WriteFile(tmp, b, 0600); err != nil {
|
||||
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = f.Write(b); err == nil {
|
||||
err = f.Sync()
|
||||
}
|
||||
if closeErr := f.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, r.StatePath); err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := os.Open(filepath.Dir(r.StatePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
if err := dir.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(r.StatePath, 0600)
|
||||
}
|
||||
|
||||
@@ -396,10 +420,32 @@ func (r *Registry) Available(id string) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
w.Online = time.Since(w.LastSeen) <= r.TTL
|
||||
// A heartbeat merely proves the worker process can reach the coordinator.
|
||||
// Lease admission additionally requires a fresh probe of the worker's
|
||||
// local herdr; otherwise a partitioned/down herdr still attracts work.
|
||||
w.Online = time.Since(w.LastSeen) <= r.TTL && w.Health.HerdrStatus == "reachable" && !w.Health.CheckedAt.IsZero() && time.Since(w.Health.CheckedAt) <= r.TTL
|
||||
r.workers[id] = w
|
||||
return w.Online
|
||||
}
|
||||
|
||||
// Supports reports whether an online worker explicitly declared the project.
|
||||
// An omitted declaration is deliberately not treated as a wildcard: workers
|
||||
// must never receive a project for which they have no local checkout.
|
||||
func (r *Registry) Supports(id, project string) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.init()
|
||||
w, ok := r.workers[id]
|
||||
if !ok || time.Since(w.LastSeen) > r.TTL {
|
||||
return false
|
||||
}
|
||||
for _, candidate := range w.SupportedProjects {
|
||||
if candidate == project {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (r *Registry) Snapshot() []Worker {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user