carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package edgesfile
2
3import (
4 "context"
5 "encoding/json"
6 "log"
7 "strings"
8 "sync"
9 "time"
10
11 "github.com/carlosframework/platform/internal/boxchoice"
12 "github.com/carlosframework/platform/internal/release"
13 "github.com/carlosframework/platform/internal/status"
14)
15
16// The deployment-wide placement read: every box's edges file joined with
17// its liveness. The console's automatic-eligibility doors and the box
18// agent's relocation chooser (region HA stage B Task 10) read through it,
19// so a relocation's target and a create's executor come from the same
20// evidence. Moved here unchanged from the console (instancesplacement.go,
21// boxchoice.go).
22
23// PlacementView is the reader's half of the edges-file schema this
24// join needs: version, box, region, and the host set. Roles are not
25// consulted — a host a box names under ANY role is served there in some
26// capacity, and "where is this instance" is the question, not "who is its
27// write home" (that is the home map's question, internal/edge).
28type PlacementView struct {
29 V int `json:"v"`
30 Box string `json:"box"`
31 EIP string `json:"eip"`
32 Region string `json:"region"`
33 Placement string `json:"placement"`
34 // DataBucket is the box's CARLOS_HIBERNATE_BUCKET, verbatim
35 // (internal/host/edgesfile.go writes it from that env and omits the key
36 // entirely when it is unset). It carries the move lane's actual
37 // coordinates, and doubles as positive hosting evidence from a writer
38 // too old to publish HibernationConfigured.
39 DataBucket string `json:"data_bucket"`
40 // DataBucketRegion is that bucket's AWS region, as the box publishes it
41 // beside DataBucket. An offline move's stage fixes both on the record.
42 DataBucketRegion string `json:"data_bucket_region"`
43 // HibernationConfigured is the box's own answer to "is my hibernation
44 // activator wired" (internal/host/edgesfile.go). A POINTER because
45 // absent and false are different facts and the difference is the whole
46 // point: absent is a writer that predates the field, whose capability is
47 // UNKNOWN, while false is a box that published the field and said no.
48 // Reading absence as no is what made an earlier version withdraw healthy
49 // regions mid-roll — see placements.
50 HibernationConfigured *bool `json:"hibernation_configured"`
51 // ServesSharedPool and AcceptsInbound: POINTERS, absent = a writer
52 // predating them (unknown). SecretsKeyFP: absent = no key, or a writer
53 // predating it; AcceptsInbound's presence tells the two apart
54 // (nearest-region spec §6.1, R9).
55 ServesSharedPool *bool `json:"serves_shared_pool"`
56 AcceptsInbound *bool `json:"accepts_inbound"`
57 SecretsKeyFP string `json:"secrets_key_fp"`
58 // Box evidence (region HA spec 1 §5.1). All absent on a writer that
59 // predates it: BoxID "", Capabilities nil, Draining false, Headroom nil.
60 BoxID string `json:"box_id"`
61 Capabilities []string `json:"capabilities"`
62 Draining bool `json:"draining"`
63 Headroom *struct {
64 MemFreeMiB int64 `json:"mem_free_mib"`
65 DiskFreeMiB int64 `json:"disk_free_mib"`
66 MemTotalMiB int64 `json:"mem_total_mib"`
67 DiskTotalMiB int64 `json:"disk_total_mib"`
68 Awake int `json:"awake"`
69 } `json:"headroom"`
70 Hosts map[string]json.RawMessage `json:"hosts"`
71
72 // Generation is when the box last wrote the file (RFC 3339), which
73 // region HA spec 2's placement policy reads as headroom's age.
74 Generation string `json:"generation"`
75
76 // The moves protocol (moves spec §11), read only by the Phase 1 move
77 // door's floor check (selectMoveBoxes); Task 13 adds the writer's side.
78 // MovesProtocol is the protocol the box speaks (absent: 0).
79 // MovesFloorMinRelease is the published floor's min_release the box
80 // has armed locally. PrevRelease is its carlos.prev's release, "none"
81 // when it keeps none, and PrevAllowedFor the floor's min_release that
82 // carlos.prev was judged at or above.
83 MovesProtocol int `json:"moves_protocol"`
84 MovesFloorMinRelease string `json:"moves_floor_min_release"`
85 MovesFloorState string `json:"moves_floor_state,omitempty"`
86 MovesFloorGeneration uint64 `json:"moves_floor_generation,omitempty"`
87 PrevRelease string `json:"prev_release"`
88 PrevAllowedFor string `json:"prev_allowed_for"`
89 // The seeds floor (seeded-instances spec §3.3), read only by the
90 // console's seedRegionReady.
91 SeedsFloorState string `json:"seeds_floor_state,omitempty"`
92 SeedsFloorGeneration uint64 `json:"seeds_floor_generation,omitempty"`
93 SeedsProtocol int `json:"seeds_protocol"`
94 SeedsFloorMinRelease string `json:"seeds_floor_min_release"`
95 PrevSeedsAllowedFor string `json:"prev_seeds_allowed_for"`
96 // Fresh is not on the wire: ReadLiveSnapshot sets it when the
97 // box's own status snapshot is still fresh (status.BoxStale). An edges
98 // file is not liveness, see ReadLiveSnapshot.
99 Fresh bool `json:"-"`
100}
101
102// livenessSaid throttles ReadLiveSnapshot's two diagnostics to once
103// per process, the way noPointSaid does for a code with no point.
104var livenessSaid sync.Map
105
106// ReadLiveSnapshot is ReadSnapshot joined with box liveness, for
107// AUTOMATIC eligibility (regionCandidates). An edges file is not evidence
108// that its box is alive: nothing deletes a retired box's file, and the
109// file's own Generation moves only when the file is rewritten, so a
110// healthy box's is arbitrarily old (internal/edgesfile.EdgeIPs's doc
111// comment has the whole argument). The status snapshot is written every
112// tick, so status.BoxStale is a real liveness signal, and this is the same
113// join EdgeIPs makes, over the same period.
114//
115// A box with no fresh snapshot, and every box when the snapshots cannot be
116// loaded, is simply not Fresh. The fold then counts it for nothing, so a
117// deployment whose status store is split from its deployment bucket
118// (CARLOS_STATUS_BUCKET, which the console does not read) degrades to
119// home-only guesses. That is safe but invisible, so both cases are said
120// once: the load failure, and files found with no snapshot matching any.
121// The explicit door's placements stay on the generous file-only read; a
122// person asked for that region and can see it refused later.
123func ReadLiveSnapshot(ctx context.Context, s release.Store, now time.Time) (Snapshot, error) {
124 snap, err := ReadSnapshot(ctx, s)
125 if err != nil {
126 return snap, err
127 }
128 // No edges file at all is not complete evidence by itself (Task 10
129 // review M5): the join still runs, so a fresh status snapshot with no
130 // edges file (ruling (A) below) makes the evidence incomplete, as it
131 // does beside any other file. With no file and no fresh snapshot the
132 // evidence is complete and empty.
133 // Completeness is carried separately from staleness: LoadBoxesComplete
134 // returns what LoadBoxes would, plus whether every listed snapshot was
135 // actually read. A skipped read leaves that box's liveness UNKNOWN,
136 // and unknown is never "stale".
137 boxes, boxesComplete, err := status.LoadBoxesComplete(ctx, s)
138 if err != nil {
139 if _, said := livenessSaid.LoadOrStore("load", true); !said {
140 log.Printf("edges: could not load box snapshots (%v), so no box outside the home counts for automatic region placement until they load", err)
141 }
142 return snap, nil
143 }
144 snap.BoxesComplete = boxesComplete
145 // known is every box with a status object at all; live is the fresh
146 // subset. The join key is the status snapshot's Label against the edges
147 // file's own box field, the pair edgesfile.EdgeIPs joins on; the object
148 // key names play no part.
149 known, live := map[string]bool{}, map[string]bool{}
150 for _, b := range boxes {
151 known[b.Label] = true
152 if !status.BoxStale(b, now, status.SnapshotPeriod) {
153 live[b.Label] = true
154 }
155 }
156 published := make(map[string]bool, len(snap.Files))
157 joined := 0
158 for i := range snap.Files {
159 published[snap.Files[i].Box] = true
160 snap.Files[i].Fresh = live[snap.Files[i].Box]
161 if snap.Files[i].Fresh {
162 joined++
163 }
164 // Ruling (B) reversed, re-review r2, on production evidence: every
165 // box writes both objects and a retired box leaves both behind, so
166 // an ABSENT snapshot is unknown liveness (incomplete evidence,
167 // whole-request) and a STALE one is the retired box (not fresh,
168 // counts for neither side, evidence complete).
169 if !known[snap.Files[i].Box] {
170 snap.BoxesComplete = false
171 if _, said := livenessSaid.LoadOrStore("nostatus", true); !said {
172 log.Printf("edges: box %q has an edges file under %s but no status snapshot at all, so no region outside the home is automatically eligible until it reports; said once", snap.Files[i].Box, Prefix)
173 }
174 }
175 }
176 // Ruling (A), re-review r2: a FRESH snapshot whose box has no edges
177 // file is incomplete evidence, whole-request, like a failed read.
178 // Reconcile admits a box without its edges file (its publication may
179 // simply have failed), so a live box the fold cannot see could still
180 // take the record. A stale snapshot with no file is a retired box and
181 // is ignored.
182 for label := range live {
183 if !published[label] {
184 snap.BoxesComplete = false
185 if _, said := livenessSaid.LoadOrStore("unmatched", true); !said {
186 log.Printf("edges: box %q has a fresh status snapshot but no edges file under %s, so no region outside the home is automatically eligible until it publishes one; said once", label, Prefix)
187 }
188 }
189 }
190 if joined == 0 && len(snap.Files) > 0 {
191 if _, said := livenessSaid.LoadOrStore("none", true); !said {
192 log.Printf("edges: %d edges file(s) under %s but no fresh box snapshot matched any of them, so automatic region placement sees the home alone (are edges and status in different buckets? see CARLOS_STATUS_BUCKET/CARLOS_STATUS_PREFIX)", len(snap.Files), Prefix)
193 }
194 }
195 return snap, nil
196}
197
198// Snapshot is one read of control/edges/. Complete is false when any
199// listed file could not be read or parsed: positive facts about the boxes
200// that WERE read still hold, but nothing may be concluded about "every
201// box" (R9's NoKeyProven, codex plan-R2 1).
202type Snapshot struct {
203 Files []PlacementView
204 Complete bool
205 // BoxesComplete: every listed status snapshot was read, and every box
206 // on either side has its counterpart, so each file's Fresh is a KNOWN
207 // answer. Set only by ReadLiveSnapshot; a plain ReadSnapshot
208 // carries no liveness evidence at all. Unknown liveness is not stale
209 // (re-review r1 hole 2, r2 rulings (A) and (B) reversed): a snapshot
210 // that could not be read, a fresh snapshot with no edges file, or an
211 // edges file with no snapshot at all each make the evidence incomplete.
212 BoxesComplete bool
213}
214
215// EvidenceComplete is the evidence bar for AUTOMATIC non-home eligibility: every
216// listed edges file read and parsed, and every listed status snapshot
217// read. Short of it a failing sibling may simply be missing from the fold,
218// so nothing non-home is eligible for the request (ruling, re-review r1).
219func (s Snapshot) EvidenceComplete() bool { return s.Complete && s.BoxesComplete }
220
221// ReadSnapshot REPORTS a failed listing instead of answering empty:
222// the door must tell "could not read" (503, C28) from "no box publishes
223// this code" (codex R2-2). A single unreadable or malformed file is
224// skipped, as before, and marks the snapshot incomplete.
225func ReadSnapshot(ctx context.Context, s release.Store) (Snapshot, error) {
226 keys, err := s.List(ctx, Prefix)
227 if err != nil {
228 return Snapshot{}, err
229 }
230 snap := Snapshot{Complete: true}
231 for _, key := range keys {
232 name, ok := strings.CutPrefix(key, Prefix)
233 if !ok || !strings.HasSuffix(name, ".json") || strings.Contains(name, "/") {
234 continue
235 }
236 body, _, err := s.Get(ctx, key)
237 if err != nil {
238 log.Printf("edges: could not read %s for the placement join: %v", key, err)
239 snap.Complete = false
240 continue
241 }
242 var f PlacementView
243 if err := json.Unmarshal(body, &f); err != nil || f.V < 1 || f.Box == "" {
244 log.Printf("edges: skipping %s in the placement join: not a versioned edges file", key)
245 snap.Complete = false
246 continue
247 }
248 snap.Files = append(snap.Files, f)
249 }
250 return snap, nil
251}
252
253// BoxChoice is the snapshot as boxchoice's predicate reads it. Every field
254// the predicate reads is carried: a dropped field would silently read as
255// absent, so the console's eligibility tests run through this conversion.
256func (s Snapshot) BoxChoice() boxchoice.Snapshot {
257 out := boxchoice.Snapshot{Complete: s.EvidenceComplete(), Boxes: make([]boxchoice.Box, 0, len(s.Files))}
258 for _, f := range s.Files {
259 b := boxchoice.Box{
260 Box: f.Box, BoxID: f.BoxID, EIP: f.EIP, Region: f.Region, Placement: f.Placement,
261 HibernationConfigured: f.HibernationConfigured, ServesSharedPool: f.ServesSharedPool,
262 AcceptsInbound: f.AcceptsInbound, SecretsKeyFP: f.SecretsKeyFP, Capabilities: f.Capabilities,
263 Draining: f.Draining, Generation: f.Generation, Fresh: f.Fresh,
264 }
265 if f.Headroom != nil {
266 b.Headroom = &boxchoice.Headroom{MemFreeMiB: f.Headroom.MemFreeMiB, DiskFreeMiB: f.Headroom.DiskFreeMiB,
267 MemTotalMiB: f.Headroom.MemTotalMiB, DiskTotalMiB: f.Headroom.DiskTotalMiB, Awake: f.Headroom.Awake}
268 }
269 out.Boxes = append(out.Boxes, b)
270 }
271 return out
272}
273
274// LiveSnapshot is ReadLiveSnapshot in boxchoice's form: the read
275// hostops.ExecutorChooser's Edges takes.
276func LiveSnapshot(ctx context.Context, s release.Store, now time.Time) (boxchoice.Snapshot, error) {
277 snap, err := ReadLiveSnapshot(ctx, s, now)
278 if err != nil {
279 return boxchoice.Snapshot{}, err
280 }
281 return snap.BoxChoice(), nil
282}
283