carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package boxchoice decides which boxes in a placement may run a host as
2// its executor, and which of them a new claim should name (region HA spec
3// 1 §2 and §5.2). It is pure: callers read the edges files, the box status
4// snapshots and the roster, and pass what they read.
5//
6// Console's placement door and moves' door both call it, so the two
7// always agree on what "eligible" means.
8package boxchoice
9
10import (
11 "crypto/sha256"
12 "encoding/hex"
13 "github.com/carlosframework/platform/internal/regionname"
14 "slices"
15 "sort"
16
17 "github.com/carlosframework/platform/internal/boxid"
18)
19
20// Capability is the capability an edges file lists when its box can be a
21// region HA executor.
22const Capability = "region-ha-1"
23
24// Headroom is what a box reports it has free, and (region HA spec 2 §7)
25// the totals the free figures are fractions of. A total of 0 is a writer
26// that predates it, or a reading that failed.
27type Headroom struct {
28 MemFreeMiB int64 `json:"mem_free_mib"`
29 DiskFreeMiB int64 `json:"disk_free_mib"`
30 MemTotalMiB int64 `json:"mem_total_mib"`
31 DiskTotalMiB int64 `json:"disk_total_mib"`
32 Awake int `json:"awake"`
33}
34
35// Box is one box's edges file (control/edges/<box>.json, written by
36// internal/host/edgesfile.go), reduced to what eligibility reads, plus
37// Fresh, which is not on the wire. The JSON tags are the edges file's, so
38// a caller may unmarshal a file straight into a Box.
39//
40// The *bool fields are pointers because absent and false are different
41// facts: absent is a writer that predates the field, and is not eligible.
42type Box struct {
43 Box string `json:"box"`
44 BoxID string `json:"box_id"`
45 EIP string `json:"eip"`
46 Region string `json:"region"`
47 Placement string `json:"placement"`
48 HibernationConfigured *bool `json:"hibernation_configured"`
49 ServesSharedPool *bool `json:"serves_shared_pool"`
50 AcceptsInbound *bool `json:"accepts_inbound"`
51 SecretsKeyFP string `json:"secrets_key_fp"`
52 Capabilities []string `json:"capabilities"`
53 Draining bool `json:"draining"`
54 Headroom *Headroom `json:"headroom"`
55 // Generation is when the box last wrote the file (RFC 3339). Spec 2's
56 // policy trusts headroom only from a recent file (Placeable).
57 Generation string `json:"generation"`
58 // Fresh is the caller's verdict that the box's own status snapshot is
59 // still fresh. An edges file outlives its box, so the file alone is
60 // not liveness.
61 Fresh bool `json:"-"`
62}
63
64// Snapshot is one read of every box. Complete is true only when every
65// listed edges file was read and parsed and every box's liveness is known
66// (each live box has a file and each file's Fresh is a known answer). An
67// incomplete snapshot makes nothing eligible: an unread file could be the
68// one box that would change the answer.
69type Snapshot struct {
70 Boxes []Box
71 Complete bool
72}
73
74// RosterBox is what the roster (control/host-actors.json, claim D7) says
75// about one active box: the placement it is enrolled in and the cloud
76// identity enrollment proved from its boxid record (spec §5.1, H11). The
77// caller passes active entries only, keyed by box id.
78type RosterBox struct {
79 Placement string
80 Cloud boxid.Cloud
81}
82
83// Candidate is one box that may be named as a host's executor (region HA
84// spec 1 §2, "eligible box").
85type Candidate struct {
86 BoxID string
87 Region string
88 MemFreeMiB, DiskFreeMiB int64
89 MemTotalMiB, DiskTotalMiB int64
90 Awake int
91 // Generation is the box's edges file's (Box.Generation).
92 Generation string
93}
94
95// Load is what claims already place on a box.
96type Load struct {
97 Hosts int
98 Apps map[string]int // "acct/app" -> hosts of that app
99}
100
101func isTrue(b *bool) bool { return b != nil && *b }
102
103// Eligible returns the boxes that may run a host placed in placement,
104// sorted by box id. A box qualifies on its own evidence alone: it is
105// fresh, has a dial address and a box id, and has a roster entry for that
106// box id with a complete cloud identity, enrolled in this placement, whose
107// proven AWS region is awsRegion (the region the roster records for this
108// placement), as its file's region also is. Its file says hibernation is
109// configured, it serves the shared pool and accepts inbound, its secrets
110// key fingerprint is expectedFP, and it has Capability. It is not
111// draining and not in lost. Two boxes that each satisfy half of this never
112// add up to one. Identity and region come from the roster; the edges file
113// is evidence of liveness and configuration only (spec §5.1).
114//
115// homeName is the home placement's code: a box whose file or roster entry
116// names it is the home placement, as is the empty code.
117//
118// An incomplete snapshot, an empty expectedFP or an empty awsRegion
119// returns nothing.
120func Eligible(snap Snapshot, roster map[string]RosterBox, placement, awsRegion, homeName, expectedFP string, lost map[string]bool) []Candidate {
121 if !snap.Complete || expectedFP == "" || awsRegion == "" {
122 return nil
123 }
124 home := func(code string) string { return regionname.HomePlacementCode(homeName, code) }
125 var out []Candidate
126 for _, f := range snap.Boxes {
127 want := home(placement)
128 rb, ok := roster[f.BoxID]
129 switch {
130 case home(f.Placement) != want:
131 case !f.Fresh, f.EIP == "", f.BoxID == "":
132 case !ok || !rb.Cloud.Complete():
133 case home(rb.Placement) != want:
134 // The file's placement is the box's own word; the roster's is
135 // the enrollment's. Both must agree.
136 case rb.Cloud.Region != awsRegion, f.Region != awsRegion:
137 // The AWS region was proven at enrollment: a box enrolled in
138 // another region is not in this placement, whatever its file
139 // says.
140 case !isTrue(f.HibernationConfigured), !isTrue(f.ServesSharedPool), !isTrue(f.AcceptsInbound):
141 case f.SecretsKeyFP != expectedFP:
142 case !slices.Contains(f.Capabilities, Capability):
143 case f.Draining, lost[f.BoxID]:
144 default:
145 c := Candidate{BoxID: f.BoxID, Region: rb.Cloud.Region, Generation: f.Generation}
146 if f.Headroom != nil {
147 c.MemFreeMiB, c.DiskFreeMiB, c.Awake = f.Headroom.MemFreeMiB, f.Headroom.DiskFreeMiB, f.Headroom.Awake
148 c.MemTotalMiB, c.DiskTotalMiB = f.Headroom.MemTotalMiB, f.Headroom.DiskTotalMiB
149 }
150 out = append(out, c)
151 }
152 }
153 sort.Slice(out, func(i, j int) bool { return out[i].BoxID < out[j].BoxID })
154 return out
155}
156
157// Choose is spec 1 §5.2's choose() until spec 2 (H12): prefer a box that
158// runs no instance of app (soft spread: it never refuses), then the fewest
159// hosts overall, then the greatest hexadecimal SHA-256 of
160// host + "\x00" + box id (claim P5-D2), then the smallest box id if two
161// hashes are equal. The answer does not depend on the order of cands. The
162// bool is false only when cands is empty.
163func Choose(host string, cands []Candidate, running map[string]Load, app string) (Candidate, bool) {
164 if len(cands) == 0 {
165 return Candidate{}, false
166 }
167 tie := func(ref string) string {
168 sum := sha256.Sum256([]byte(host + "\x00" + ref))
169 return hex.EncodeToString(sum[:])
170 }
171 best := cands[0]
172 for _, c := range cands[1:] {
173 cl, bl := running[c.BoxID], running[best.BoxID]
174 cRuns, bRuns := cl.Apps[app] > 0, bl.Apps[app] > 0
175 switch {
176 case cRuns != bRuns:
177 if !cRuns {
178 best = c
179 }
180 case cl.Hosts != bl.Hosts:
181 if cl.Hosts < bl.Hosts {
182 best = c
183 }
184 case tie(c.BoxID) != tie(best.BoxID):
185 if tie(c.BoxID) > tie(best.BoxID) {
186 best = c
187 }
188 case c.BoxID < best.BoxID:
189 best = c
190 }
191 }
192 return best, true
193}
194