package boxchoice import ( "crypto/sha256" "encoding/hex" "sort" "time" ) // Region HA spec 2's placement policy (§7). Its rules run over candidates // Eligible has already filtered for identity, placement and configuration // (§25): a provider-neutral box identity (#299) changes only Eligible, // never these. // The thresholds of §7. const ( // MinMemPercent: a box is placeable only while its free memory is at // least this share of its total, the memory alarm's own line. MinMemPercent = 15 // MinDiskPercent: and its data volume's free space at least this // share of the volume. MinDiskPercent = 10 // MaxEdgesAge: and its edges file, which carries the headroom, is no // older than this. Files republish every 5 minutes plus up to a // minute of jitter, so a healthy box always has one within two // intervals and a margin (A1-8). MaxEdgesAge = 15 * time.Minute // maxEdgesSkew is how far in the future a file's generation may be // before it is not believed: box clocks are synchronised, not equal. maxEdgesSkew = time.Minute ) // ExcludeRoles drops every candidate whose roster role is not the default // one (roles is box id -> role, "" for the default). Legacy and platform // boxes, and any role this build does not know, take no new instances. // The order of cands is kept. func ExcludeRoles(cands []Candidate, roles map[string]string) []Candidate { var out []Candidate for _, c := range cands { if roles[c.BoxID] == "" { out = append(out, c) } } return out } // headroomFresh reports whether a candidate's edges file is recent enough // at now for its headroom to be trusted. func headroomFresh(generation string, now time.Time) bool { at, err := time.Parse(time.RFC3339, generation) if err != nil { return false } age := now.Sub(at) return age <= MaxEdgesAge && age >= -maxEdgesSkew } // Placeable is §7's eligibility on top of Eligible's: the default role, // free memory at least MinMemPercent of the total, free disk at least // MinDiskPercent of the volume, and an edges file no older than // MaxEdgesAge at now. A box that publishes no totals is not placeable: its // headroom cannot be judged. The order of cands is kept. func Placeable(cands []Candidate, roles map[string]string, now time.Time) []Candidate { var out []Candidate for _, c := range ExcludeRoles(cands, roles) { switch { case c.MemTotalMiB <= 0 || c.MemFreeMiB*100 < c.MemTotalMiB*MinMemPercent: case c.DiskTotalMiB <= 0 || c.DiskFreeMiB*100 < c.DiskTotalMiB*MinDiskPercent: case !headroomFresh(c.Generation, now): default: out = append(out, c) } } return out } // ChooseRoomiest is §7's choice among placeable candidates: prefer a box // running no instance of app (soft spread: it never refuses), then the // most free memory, then the greatest hexadecimal SHA-256 of host + "\x00" // + box id (claim P5-D2), then the smallest box id. The answer does not // depend on the order of cands. The bool is false only when cands is // empty. func ChooseRoomiest(host string, cands []Candidate, running map[string]Load, app string) (Candidate, bool) { if len(cands) == 0 { return Candidate{}, false } tie := func(ref string) string { sum := sha256.Sum256([]byte(host + "\x00" + ref)) return hex.EncodeToString(sum[:]) } sorted := append([]Candidate(nil), cands...) sort.SliceStable(sorted, func(i, j int) bool { a, b := sorted[i], sorted[j] aRuns, bRuns := running[a.BoxID].Apps[app] > 0, running[b.BoxID].Apps[app] > 0 switch { case aRuns != bRuns: return !aRuns case a.MemFreeMiB != b.MemFreeMiB: return a.MemFreeMiB > b.MemFreeMiB case tie(a.BoxID) != tie(b.BoxID): return tie(a.BoxID) > tie(b.BoxID) } return a.BoxID < b.BoxID }) return sorted[0], true }