| 1 | package hostfiles |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/registry" |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | ) |
| 11 | |
| 12 | // WakeItem is the wake state's name among the manifest's platform items. |
| 13 | const WakeItem = "wake.json" |
| 14 | |
| 15 | // WakeTimer is registry.Timer on the wire. It exists because |
| 16 | // registry.Timer hides Registration from JSON, and an import must keep it: |
| 17 | // the runner's delivery bookkeeping is keyed by it. |
| 18 | type WakeTimer struct { |
| 19 | Name string `json:"name"` |
| 20 | Kind string `json:"kind"` |
| 21 | Spec string `json:"spec"` |
| 22 | Path string `json:"path"` |
| 23 | NextAt int64 `json:"next_at"` |
| 24 | LastAt int64 `json:"last_at"` |
| 25 | LastStatus string `json:"last_status"` |
| 26 | LastMs int64 `json:"last_ms"` |
| 27 | Attempts int `json:"attempts"` |
| 28 | CreatedAt int64 `json:"created_at"` |
| 29 | DueAt int64 `json:"due_at"` |
| 30 | Registration string `json:"registration"` |
| 31 | } |
| 32 | |
| 33 | // WakeState is everything that decides when a host next runs without a |
| 34 | // request: its timers, the app's own next wake, and its replication class |
| 35 | // (region HA spec 1 §5.7). |
| 36 | type WakeState struct { |
| 37 | V int `json:"v"` |
| 38 | Host string `json:"host"` |
| 39 | Timers []WakeTimer `json:"timers"` |
| 40 | NextDue int64 `json:"next_due"` |
| 41 | Replication string `json:"replication"` |
| 42 | } |
| 43 | |
| 44 | // ExportWake reads host's wake state. A finished once-timer (next_at 0) is |
| 45 | // history: it is left out, so the runner's daily prune of such rows never |
| 46 | // changes what is exported. |
| 47 | func ExportWake(reg *registry.Registry, host string) (WakeState, error) { |
| 48 | rt, err := reg.Get(host) |
| 49 | if err != nil { |
| 50 | return WakeState{}, err |
| 51 | } |
| 52 | if rt == nil { |
| 53 | return WakeState{}, fmt.Errorf("hostfiles: wake state for %s: no route", host) |
| 54 | } |
| 55 | rows, err := reg.TimersFor(host) |
| 56 | if err != nil { |
| 57 | return WakeState{}, err |
| 58 | } |
| 59 | return WakeStateOf(host, rows, rt.NextDue, rt.Replication), nil |
| 60 | } |
| 61 | |
| 62 | // WakeStateOf is host's wake state from its timer rows, next wake and |
| 63 | // replication class, as ExportWake reads them from the registry: an |
| 64 | // installation that holds the rows it is about to install (a move's |
| 65 | // sealed timers snapshot) builds its prepared manifest's wake state with |
| 66 | // it. A finished once-timer is left out, as ExportWake leaves it out. |
| 67 | func WakeStateOf(host string, rows []registry.Timer, nextDue int64, replication string) WakeState { |
| 68 | ws := WakeState{V: 1, Host: host, Timers: []WakeTimer{}, NextDue: nextDue, Replication: replication} |
| 69 | for _, t := range rows { |
| 70 | if t.Kind == "once" && t.NextAt == 0 { |
| 71 | continue |
| 72 | } |
| 73 | ws.Timers = append(ws.Timers, WakeTimer{Name: t.Name, Kind: t.Kind, Spec: t.Spec, Path: t.Path, |
| 74 | NextAt: t.NextAt, LastAt: t.LastAt, LastStatus: t.LastStatus, LastMs: t.LastMs, Attempts: t.Attempts, |
| 75 | CreatedAt: t.CreatedAt, DueAt: t.DueAt, Registration: t.Registration}) |
| 76 | } |
| 77 | return ws |
| 78 | } |
| 79 | |
| 80 | // ImportWake makes host's wake state exactly ws. It validates everything |
| 81 | // and checks that host has a route before it writes anything. It is |
| 82 | // retry-safe (a repeat is the same full replace) but not atomic across its |
| 83 | // three writes. |
| 84 | func ImportWake(reg *registry.Registry, host string, ws WakeState) error { |
| 85 | if ws.V != 1 { |
| 86 | return fmt.Errorf("hostfiles: wake state version %d; this binary reads 1", ws.V) |
| 87 | } |
| 88 | if ws.Host != host { |
| 89 | return fmt.Errorf("hostfiles: wake state is for %q, not %q", ws.Host, host) |
| 90 | } |
| 91 | if !registry.ValidReplication(ws.Replication) { |
| 92 | return fmt.Errorf("hostfiles: wake state for %s names replication class %q", host, ws.Replication) |
| 93 | } |
| 94 | rt, err := reg.Get(host) |
| 95 | if err != nil { |
| 96 | return err |
| 97 | } |
| 98 | if rt == nil { |
| 99 | return fmt.Errorf("hostfiles: wake state for %s: no route", host) |
| 100 | } |
| 101 | rows := make([]registry.Timer, 0, len(ws.Timers)) |
| 102 | for _, t := range ws.Timers { |
| 103 | rows = append(rows, registry.Timer{Host: host, Name: t.Name, Kind: t.Kind, Spec: t.Spec, Path: t.Path, |
| 104 | NextAt: t.NextAt, LastAt: t.LastAt, LastStatus: t.LastStatus, LastMs: t.LastMs, Attempts: t.Attempts, |
| 105 | CreatedAt: t.CreatedAt, DueAt: t.DueAt, Registration: t.Registration}) |
| 106 | } |
| 107 | if err := reg.ReplaceTimers(host, rows); err != nil { |
| 108 | return err |
| 109 | } |
| 110 | if err := reg.SetNextDue(host, ws.NextDue); err != nil { |
| 111 | return err |
| 112 | } |
| 113 | // Always, the empty class included: "" is the default rung, and a |
| 114 | // destination left on its previous class would not be the state that |
| 115 | // was exported. |
| 116 | return reg.SetReplication(host, ws.Replication) |
| 117 | } |
| 118 | |
| 119 | // StageWake stages the wake state as a platform item. Unchanged state |
| 120 | // stages nothing. |
| 121 | func StageWake(ctx context.Context, p *Publisher, ws WakeState) error { |
| 122 | body, err := json.Marshal(ws) |
| 123 | if err != nil { |
| 124 | return fmt.Errorf("hostfiles: marshal wake state: %w", err) |
| 125 | } |
| 126 | return p.StagePlatform(ctx, WakeItem, body) |
| 127 | } |
| 128 | |
| 129 | // LoadWake reads the wake state a manifest carries. The bool is false only |
| 130 | // when the manifest has no wake entry at all. A manifest that names wake |
| 131 | // state whose object is missing or damaged is an error: that is lost |
| 132 | // timers, not a host with none, and the caller must not install as if it |
| 133 | // were. origins are as for ReadPlatform. |
| 134 | func LoadWake(ctx context.Context, s release.Store, prefix string, m Manifest, origins []string) (WakeState, bool, error) { |
| 135 | if _, ok := m.Platform[WakeItem]; !ok { |
| 136 | return WakeState{}, false, nil |
| 137 | } |
| 138 | body, err := ReadPlatform(ctx, s, prefix, m, WakeItem, origins) |
| 139 | if err != nil { |
| 140 | return WakeState{}, false, fmt.Errorf("hostfiles: the manifest names wake state that cannot be read: %w", err) |
| 141 | } |
| 142 | var ws WakeState |
| 143 | if err := json.Unmarshal(body, &ws); err != nil { |
| 144 | return WakeState{}, false, fmt.Errorf("hostfiles: wake state: %w", err) |
| 145 | } |
| 146 | return ws, true, nil |
| 147 | } |
| 148 | |