carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package boxchoice
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "sort"
7 "time"
8)
9
10// Region HA spec 2's placement policy (§7). Its rules run over candidates
11// Eligible has already filtered for identity, placement and configuration
12// (§25): a provider-neutral box identity (#299) changes only Eligible,
13// never these.
14
15// The thresholds of §7.
16const (
17 // MinMemPercent: a box is placeable only while its free memory is at
18 // least this share of its total, the memory alarm's own line.
19 MinMemPercent = 15
20 // MinDiskPercent: and its data volume's free space at least this
21 // share of the volume.
22 MinDiskPercent = 10
23 // MaxEdgesAge: and its edges file, which carries the headroom, is no
24 // older than this. Files republish every 5 minutes plus up to a
25 // minute of jitter, so a healthy box always has one within two
26 // intervals and a margin (A1-8).
27 MaxEdgesAge = 15 * time.Minute
28 // maxEdgesSkew is how far in the future a file's generation may be
29 // before it is not believed: box clocks are synchronised, not equal.
30 maxEdgesSkew = time.Minute
31)
32
33// ExcludeRoles drops every candidate whose roster role is not the default
34// one (roles is box id -> role, "" for the default). Legacy and platform
35// boxes, and any role this build does not know, take no new instances.
36// The order of cands is kept.
37func ExcludeRoles(cands []Candidate, roles map[string]string) []Candidate {
38 var out []Candidate
39 for _, c := range cands {
40 if roles[c.BoxID] == "" {
41 out = append(out, c)
42 }
43 }
44 return out
45}
46
47// headroomFresh reports whether a candidate's edges file is recent enough
48// at now for its headroom to be trusted.
49func headroomFresh(generation string, now time.Time) bool {
50 at, err := time.Parse(time.RFC3339, generation)
51 if err != nil {
52 return false
53 }
54 age := now.Sub(at)
55 return age <= MaxEdgesAge && age >= -maxEdgesSkew
56}
57
58// Placeable is §7's eligibility on top of Eligible's: the default role,
59// free memory at least MinMemPercent of the total, free disk at least
60// MinDiskPercent of the volume, and an edges file no older than
61// MaxEdgesAge at now. A box that publishes no totals is not placeable: its
62// headroom cannot be judged. The order of cands is kept.
63func Placeable(cands []Candidate, roles map[string]string, now time.Time) []Candidate {
64 var out []Candidate
65 for _, c := range ExcludeRoles(cands, roles) {
66 switch {
67 case c.MemTotalMiB <= 0 || c.MemFreeMiB*100 < c.MemTotalMiB*MinMemPercent:
68 case c.DiskTotalMiB <= 0 || c.DiskFreeMiB*100 < c.DiskTotalMiB*MinDiskPercent:
69 case !headroomFresh(c.Generation, now):
70 default:
71 out = append(out, c)
72 }
73 }
74 return out
75}
76
77// ChooseRoomiest is §7's choice among placeable candidates: prefer a box
78// running no instance of app (soft spread: it never refuses), then the
79// most free memory, then the greatest hexadecimal SHA-256 of host + "\x00"
80// + box id (claim P5-D2), then the smallest box id. The answer does not
81// depend on the order of cands. The bool is false only when cands is
82// empty.
83func ChooseRoomiest(host string, cands []Candidate, running map[string]Load, app string) (Candidate, bool) {
84 if len(cands) == 0 {
85 return Candidate{}, false
86 }
87 tie := func(ref string) string {
88 sum := sha256.Sum256([]byte(host + "\x00" + ref))
89 return hex.EncodeToString(sum[:])
90 }
91 sorted := append([]Candidate(nil), cands...)
92 sort.SliceStable(sorted, func(i, j int) bool {
93 a, b := sorted[i], sorted[j]
94 aRuns, bRuns := running[a.BoxID].Apps[app] > 0, running[b.BoxID].Apps[app] > 0
95 switch {
96 case aRuns != bRuns:
97 return !aRuns
98 case a.MemFreeMiB != b.MemFreeMiB:
99 return a.MemFreeMiB > b.MemFreeMiB
100 case tie(a.BoxID) != tie(b.BoxID):
101 return tie(a.BoxID) > tie(b.BoxID)
102 }
103 return a.BoxID < b.BoxID
104 })
105 return sorted[0], true
106}
107