| 1 | package boxchoice |
| 2 | |
| 3 | import ( |
| 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. |
| 16 | const ( |
| 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. |
| 37 | func 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. |
| 49 | func 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. |
| 63 | func 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. |
| 83 | func 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 | |