carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
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.
13const 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.
18type 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).
36type 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.
47func 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.
67func 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.
84func 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.
121func 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.
134func 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