package hostfiles import ( "context" "encoding/json" "fmt" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // WakeItem is the wake state's name among the manifest's platform items. const WakeItem = "wake.json" // WakeTimer is registry.Timer on the wire. It exists because // registry.Timer hides Registration from JSON, and an import must keep it: // the runner's delivery bookkeeping is keyed by it. type WakeTimer struct { Name string `json:"name"` Kind string `json:"kind"` Spec string `json:"spec"` Path string `json:"path"` NextAt int64 `json:"next_at"` LastAt int64 `json:"last_at"` LastStatus string `json:"last_status"` LastMs int64 `json:"last_ms"` Attempts int `json:"attempts"` CreatedAt int64 `json:"created_at"` DueAt int64 `json:"due_at"` Registration string `json:"registration"` } // WakeState is everything that decides when a host next runs without a // request: its timers, the app's own next wake, and its replication class // (region HA spec 1 ยง5.7). type WakeState struct { V int `json:"v"` Host string `json:"host"` Timers []WakeTimer `json:"timers"` NextDue int64 `json:"next_due"` Replication string `json:"replication"` } // ExportWake reads host's wake state. A finished once-timer (next_at 0) is // history: it is left out, so the runner's daily prune of such rows never // changes what is exported. func ExportWake(reg *registry.Registry, host string) (WakeState, error) { rt, err := reg.Get(host) if err != nil { return WakeState{}, err } if rt == nil { return WakeState{}, fmt.Errorf("hostfiles: wake state for %s: no route", host) } rows, err := reg.TimersFor(host) if err != nil { return WakeState{}, err } return WakeStateOf(host, rows, rt.NextDue, rt.Replication), nil } // WakeStateOf is host's wake state from its timer rows, next wake and // replication class, as ExportWake reads them from the registry: an // installation that holds the rows it is about to install (a move's // sealed timers snapshot) builds its prepared manifest's wake state with // it. A finished once-timer is left out, as ExportWake leaves it out. func WakeStateOf(host string, rows []registry.Timer, nextDue int64, replication string) WakeState { ws := WakeState{V: 1, Host: host, Timers: []WakeTimer{}, NextDue: nextDue, Replication: replication} for _, t := range rows { if t.Kind == "once" && t.NextAt == 0 { continue } ws.Timers = append(ws.Timers, WakeTimer{Name: t.Name, Kind: t.Kind, Spec: t.Spec, Path: t.Path, NextAt: t.NextAt, LastAt: t.LastAt, LastStatus: t.LastStatus, LastMs: t.LastMs, Attempts: t.Attempts, CreatedAt: t.CreatedAt, DueAt: t.DueAt, Registration: t.Registration}) } return ws } // ImportWake makes host's wake state exactly ws. It validates everything // and checks that host has a route before it writes anything. It is // retry-safe (a repeat is the same full replace) but not atomic across its // three writes. func ImportWake(reg *registry.Registry, host string, ws WakeState) error { if ws.V != 1 { return fmt.Errorf("hostfiles: wake state version %d; this binary reads 1", ws.V) } if ws.Host != host { return fmt.Errorf("hostfiles: wake state is for %q, not %q", ws.Host, host) } if !registry.ValidReplication(ws.Replication) { return fmt.Errorf("hostfiles: wake state for %s names replication class %q", host, ws.Replication) } rt, err := reg.Get(host) if err != nil { return err } if rt == nil { return fmt.Errorf("hostfiles: wake state for %s: no route", host) } rows := make([]registry.Timer, 0, len(ws.Timers)) for _, t := range ws.Timers { rows = append(rows, registry.Timer{Host: host, Name: t.Name, Kind: t.Kind, Spec: t.Spec, Path: t.Path, NextAt: t.NextAt, LastAt: t.LastAt, LastStatus: t.LastStatus, LastMs: t.LastMs, Attempts: t.Attempts, CreatedAt: t.CreatedAt, DueAt: t.DueAt, Registration: t.Registration}) } if err := reg.ReplaceTimers(host, rows); err != nil { return err } if err := reg.SetNextDue(host, ws.NextDue); err != nil { return err } // Always, the empty class included: "" is the default rung, and a // destination left on its previous class would not be the state that // was exported. return reg.SetReplication(host, ws.Replication) } // StageWake stages the wake state as a platform item. Unchanged state // stages nothing. func StageWake(ctx context.Context, p *Publisher, ws WakeState) error { body, err := json.Marshal(ws) if err != nil { return fmt.Errorf("hostfiles: marshal wake state: %w", err) } return p.StagePlatform(ctx, WakeItem, body) } // LoadWake reads the wake state a manifest carries. The bool is false only // when the manifest has no wake entry at all. A manifest that names wake // state whose object is missing or damaged is an error: that is lost // timers, not a host with none, and the caller must not install as if it // were. origins are as for ReadPlatform. func LoadWake(ctx context.Context, s release.Store, prefix string, m Manifest, origins []string) (WakeState, bool, error) { if _, ok := m.Platform[WakeItem]; !ok { return WakeState{}, false, nil } body, err := ReadPlatform(ctx, s, prefix, m, WakeItem, origins) if err != nil { return WakeState{}, false, fmt.Errorf("hostfiles: the manifest names wake state that cannot be read: %w", err) } var ws WakeState if err := json.Unmarshal(body, &ws); err != nil { return WakeState{}, false, fmt.Errorf("hostfiles: wake state: %w", err) } return ws, true, nil }