eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// Per-box capacity stats for the public Fleet status page's multi-host
4// aggregation (deploy/status-publish.sh). Each mesh member self-publishes its
5// own snapshot (internal/activator/heartbeat.go's PutStats, a sibling object
6// next to the membership heartbeat) on its own slower cadence; box A's
7// status-publish run reads every live peer's snapshot via the AWS CLI to
8// build fleet.json's `hosts.boxes` array, alongside its own already-local
9// computation.
10//
11// Deliberately narrow, matching status.md's disclosure rule word-for-word:
12// only awake/asleep counts and a banded load WORD ever leave this box — the
13// underlying memory ratio dies right here, in the same process that read it,
14// never serialized anywhere even over an authenticated internal channel.
15
16import (
17 "bufio"
18 "context"
19 "crypto/sha256"
20 "encoding/hex"
21 "log"
22 "os"
23 "strconv"
24 "strings"
25 "time"
26
27 "lchat/internal/activator"
28)
29
30// meshStatsInterval paces the stats PutStats — far slower than the
31// membership heartbeat (meshHeartbeatInterval, 15s): capacity changes slowly,
32// and this is visibility, not coordination.
33const meshStatsInterval = 60 * time.Second
34
35// pctBand mirrors deploy/status-publish.sh's pct_band: 0 below 70%, 1 at
36// 70-90%, 2 above 90%. Both sides must band identically, or the same
37// utilization could publish a different word depending which box you asked.
38func pctBand(pct int) int {
39 switch {
40 case pct < 70:
41 return 0
42 case pct <= 90:
43 return 1
44 default:
45 return 2
46 }
47}
48
49// loadWord mirrors status-publish.sh's load_word.
50func loadWord(band int) string {
51 switch band {
52 case 0:
53 return "comfortable"
54 case 1:
55 return "busy"
56 default:
57 return "critical"
58 }
59}
60
61// meminfoPath is a seam for tests — production always reads the real
62// /proc/meminfo, matching status-publish.sh's own $MEMINFO override convention.
63var meminfoPath = "/proc/meminfo"
64
65// memLoadWord reads /proc/meminfo and bands (MemTotal-MemAvailable)/MemTotal,
66// the same formula and thresholds as status-publish.sh's meminfo_kb/pct_band.
67// ok=false when unreadable (a non-Linux dev machine, a stripped-down
68// container) — unknowns publish as absent, never a guessed word.
69func memLoadWord() (word string, ok bool) {
70 f, err := os.Open(meminfoPath)
71 if err != nil {
72 return "", false
73 }
74 defer f.Close()
75
76 var total, avail int64
77 sc := bufio.NewScanner(f)
78 for sc.Scan() {
79 fields := strings.Fields(sc.Text())
80 if len(fields) < 2 {
81 continue
82 }
83 switch strings.TrimSuffix(fields[0], ":") {
84 case "MemTotal":
85 total, _ = strconv.ParseInt(fields[1], 10, 64)
86 case "MemAvailable":
87 avail, _ = strconv.ParseInt(fields[1], 10, 64)
88 }
89 }
90 if total <= 0 {
91 return "", false
92 }
93 pct := int((total - avail) * 100 / total)
94 return loadWord(pctBand(pct)), true
95}
96
97// boxLabelNames/boxLabelActions are IDENTICAL, in the SAME order, to
98// deploy/status-publish.sh's box_label names[]/actions[] arrays — the same
99// hash must produce the same whimsical label whichever side computes it.
100// Keep the two lists in sync by hand; TestBoxLabelMatchesBash pins one
101// cross-checked example against the real bash script's own output.
102var boxLabelNames = []string{
103 "ada", "alan", "grace", "marie", "nikola", "leonardo", "cleopatra", "homer", "sappho",
104 "darwin", "einstein", "curie", "turing", "hopper", "tesla", "gandalf", "frodo", "sherlock", "watson",
105 "ahab", "ishmael", "gatsby", "scout", "atticus", "hermione", "matilda", "mowgli", "merlin", "arthur",
106 "morgana", "beowulf", "odysseus", "penelope", "achilles", "athena", "apollo", "freya", "thor", "loki",
107 "amelia", "boudicca", "cyrano", "quixote", "sancho", "ophelia", "viola", "rosalind", "puck", "oberon",
108 "titania", "darcy", "pip", "scrooge", "nemo", "phileas", "zorro", "heidi", "galileo", "kepler", "newton",
109 "archimedes", "hypatia", "confucius", "rumi", "frida", "vincent", "wolfgang", "ludwig", "clara",
110}
111
112var boxLabelActions = []string{
113 "stargazing", "juggling", "whistling", "painting", "sailing", "gardening",
114 "baking", "skating", "humming", "doodling", "fishing", "knitting", "strolling", "yodeling",
115 "beekeeping", "fencing", "waltzing", "sketching", "brewing", "climbing", "cartwheeling",
116 "birdwatching", "pondering", "inventing", "exploring", "composing", "conducting",
117 "spelunking", "kayaking", "orienteering", "puppeteering", "tinkering",
118}
119
120// boxRegion resolves this box's coarse region for peer publishing, straight
121// from LCHAT_BOX_REGION — an explicit operator override only. Unlike
122// status-publish.sh's own box_region() (which prefers auto-detecting via
123// Hetzner/AWS instance metadata for the LOCAL box, falling back to a hand-set
124// override), a mesh peer publishes here from the Go binary, which has no
125// metadata-probing code of its own — so this box's operator sets the string
126// once in its router.env, same coarse "continent · country" shape, and it
127// rides through status-publish.sh's mesh_peer_boxes unchanged (that merge is
128// already a raw passthrough of the whole published object). Empty env means
129// omitted, never guessed.
130func boxRegion() string {
131 return os.Getenv("LCHAT_BOX_REGION")
132}
133
134// machineIDPath is a seam for tests, matching meminfoPath's convention.
135var machineIDPath = "/etc/machine-id"
136
137// boxLabel derives this box's stable, whimsical, anonymous name — the exact
138// port of status-publish.sh's box_label (sha256 of /etc/machine-id, first 16
139// hex chars split into two 8-char slices indexing names/actions). Falls back
140// to the `hostname` command's output if the machine-id file is unreadable,
141// matching bash's `cat "$idfile" 2>/dev/null || hostname` byte-for-byte: a
142// machine-id file carries no trailing newline, but the `hostname` command's
143// output does, so only the fallback path appends one.
144func boxLabel() string {
145 var seed []byte
146 if data, err := os.ReadFile(machineIDPath); err == nil {
147 seed = data
148 } else {
149 hn, _ := os.Hostname()
150 seed = []byte(hn + "\n")
151 }
152 sum := sha256.Sum256(seed)
153 hexSum := hex.EncodeToString(sum[:])[:16]
154 n, _ := strconv.ParseUint(hexSum[:8], 16, 32)
155 a, _ := strconv.ParseUint(hexSum[8:16], 16, 32)
156 return boxLabelNames[int(n)%len(boxLabelNames)] + "-" + boxLabelActions[int(a)%len(boxLabelActions)]
157}
158
159// computeBoxStats builds this box's publishable snapshot: asleep is derived
160// (every hibernating route not currently awake), never separately probed, so
161// the two counts can never disagree with each other.
162//
163// hib only counts routes THIS box currently HRW-owns (mesh.owns — nil mesh,
164// e.g. in tests that don't exercise the mesh, counts everything). Without
165// this filter, a mesh-replicated route is known to (and counted by) every
166// peer it's been seeded onto, so two boxes would each report it — correctly,
167// from each one's own local blind spot — and the Fleet page's per-box rows
168// would visibly overlap (the same instance reads as "asleep" on more than one
169// host, with no way to tell it's the same instance). Filtering to the sole
170// current owner makes every box's row a genuine, non-overlapping partition of
171// the fleet: an out-of-scope route (mesh.owns always true for those, since
172// only one box was ever going to have its row anyway) still counts locally,
173// exactly as before mesh placement existed.
174func computeBoxStats(reg *Registry, act *activatorSvc, mesh *meshSvc) activator.BoxStats {
175 routes, err := reg.list()
176 if err != nil {
177 log.Printf("mesh: stats: list routes: %v", err)
178 return activator.BoxStats{}
179 }
180 hib := 0
181 for _, r := range routes {
182 if r.Kind != "instance" || !r.Hibernate {
183 continue
184 }
185 if mesh != nil && !mesh.owns(r.Host) {
186 continue // a live peer owns it — that box's own snapshot counts it, not this one
187 }
188 hib++
189 }
190 awake := act.awakeCount()
191 asleep := hib - awake
192 load, _ := memLoadWord() // "" (omitted) when unknown
193
194 return activator.BoxStats{Label: boxLabel(), Awake: &awake, Asleep: &asleep, Load: load, Region: boxRegion()}
195}
196
197// startStatsReporter runs alongside the mesh (wired only when mesh != nil,
198// router.go): computes and PutStats this box's snapshot on meshStatsInterval,
199// for as long as the process runs — like the router's other background
200// loops (e.g. its registry-watcher poll), it has no graceful stop; the
201// process exiting is what stops it.
202func startStatsReporter(store *activator.HeartbeatStore, self string, reg *Registry, act *activatorSvc, mesh *meshSvc) {
203 report := func() {
204 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
205 defer cancel()
206 if err := store.PutStats(ctx, self, computeBoxStats(reg, act, mesh)); err != nil {
207 log.Printf("mesh: stats: publish failed: %v", err)
208 }
209 }
210 report() // one synchronous publish so a box's very first status read isn't stale
211 go func() {
212 for range time.Tick(meshStatsInterval) {
213 report()
214 }
215 }()
216}
217