edges: headroom is published only with the HA capability, and the meminfo parser lives in one file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 files changed,
+50
−48
internal/host/edgesfile.go+2 −2internal/host/headroom.go+38 −0internal/host/headroom_linux.go+0 −22internal/host/headroom_other.go+0 −24internal/host/headroom_test.go+10 −0
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go| index f25501684..0bb51d361 100644 |
| --- a/internal/host/edgesfile.go |
| +++ b/internal/host/edgesfile.go |
| @@ -284,7 +284,7 @@ type edgesFile struct { |
| Draining bool `json:"draining,omitempty"` |
| // Headroom is what placement chooses by (spec 2). It is refreshed when |
| // the file is written for another reason and on the loop's own tick; a |
| - // headroom change alone never forces a write (see edgesFilePass). |
| + // headroom change alone never forces a write (see edgesFileWrite). |
| Headroom *edgeHeadroom `json:"headroom,omitempty"` |
| Generation string `json:"generation"` |
| @@ -767,7 +767,7 @@ func edgesFileLoop(reg *registry.Registry) { |
| if sched.timed { |
| eip = readPublicIP() |
| } |
| - ident.Headroom = readHeadroom(dataDir, awakeCount(reg)) |
| + ident.Headroom = headroomFor(ident.Capabilities, dataDir, reg) |
| err = edgesFileWrite(ctx, store, reg, ident, eip, &last, sched.timed) |
| if err != nil { |
| // Throttled: a bucket that refuses every write (an IAM Deny, an |
diff --git a/internal/host/headroom.go b/internal/host/headroom.go| new file mode 100644 |
| index 000000000..756bbf914 |
| --- /dev/null |
| +++ b/internal/host/headroom.go |
| @@ -0,0 +1,38 @@ |
| +package host |
| + |
| +import ( |
| + "strconv" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +// headroomFor is the reading the edges loop publishes: nil unless the box |
| +// has the region-HA capability. A box that publishes headroom is rewritten |
| +// on every timed pass, so a one-box region (flag off) must not carry one. |
| +func headroomFor(caps []string, dataDir string, reg *registry.Registry) *edgeHeadroom { |
| + if len(caps) == 0 { |
| + return nil |
| + } |
| + return readHeadroom(dataDir, awakeCount(reg)) |
| +} |
| + |
| +// parseMemAvailableMiB returns the MemAvailable line in MiB, or 0. |
| +func parseMemAvailableMiB(meminfo string) int64 { |
| + for _, line := range strings.Split(meminfo, "\n") { |
| + rest, ok := strings.CutPrefix(line, "MemAvailable:") |
| + if !ok { |
| + continue |
| + } |
| + fields := strings.Fields(rest) |
| + if len(fields) == 0 { |
| + return 0 |
| + } |
| + kb, err := strconv.ParseInt(fields[0], 10, 64) |
| + if err != nil { |
| + return 0 |
| + } |
| + return kb / 1024 |
| + } |
| + return 0 |
| +} |
diff --git a/internal/host/headroom_linux.go b/internal/host/headroom_linux.go| index 6a23ca9c0..9fbeccd70 100644 |
| --- a/internal/host/headroom_linux.go |
| +++ b/internal/host/headroom_linux.go |
| @@ -4,8 +4,6 @@ package host |
| import ( |
| "os" |
| - "strconv" |
| - "strings" |
| "golang.org/x/sys/unix" |
| ) |
| @@ -23,23 +21,3 @@ func readHeadroom(dataDir string, awake int) *edgeHeadroom { |
| } |
| return h |
| } |
| - |
| -// parseMemAvailableMiB returns the MemAvailable line in MiB, or 0. |
| -func parseMemAvailableMiB(meminfo string) int64 { |
| - for _, line := range strings.Split(meminfo, "\n") { |
| - rest, ok := strings.CutPrefix(line, "MemAvailable:") |
| - if !ok { |
| - continue |
| - } |
| - fields := strings.Fields(rest) |
| - if len(fields) == 0 { |
| - return 0 |
| - } |
| - kb, err := strconv.ParseInt(fields[0], 10, 64) |
| - if err != nil { |
| - return 0 |
| - } |
| - return kb / 1024 |
| - } |
| - return 0 |
| -} |
diff --git a/internal/host/headroom_other.go b/internal/host/headroom_other.go| index 84d4fa384..543706db6 100644 |
| --- a/internal/host/headroom_other.go |
| +++ b/internal/host/headroom_other.go |
| @@ -2,29 +2,5 @@ |
| package host |
| -import ( |
| - "strconv" |
| - "strings" |
| -) |
| - |
| // readHeadroom has no readings off Linux: only the awake count. |
| func readHeadroom(dataDir string, awake int) *edgeHeadroom { return &edgeHeadroom{Awake: awake} } |
| - |
| -func parseMemAvailableMiB(meminfo string) int64 { |
| - for _, line := range strings.Split(meminfo, "\n") { |
| - rest, ok := strings.CutPrefix(line, "MemAvailable:") |
| - if !ok { |
| - continue |
| - } |
| - fields := strings.Fields(rest) |
| - if len(fields) == 0 { |
| - return 0 |
| - } |
| - kb, err := strconv.ParseInt(fields[0], 10, 64) |
| - if err != nil { |
| - return 0 |
| - } |
| - return kb / 1024 |
| - } |
| - return 0 |
| -} |
diff --git a/internal/host/headroom_test.go b/internal/host/headroom_test.go| index c5c870160..5292dc0ba 100644 |
| --- a/internal/host/headroom_test.go |
| +++ b/internal/host/headroom_test.go |
| @@ -14,3 +14,13 @@ func TestParseMemAvailable(t *testing.T) { |
| t.Fatalf("unparseable: got %d, want 0", got) |
| } |
| } |
| + |
| +func TestHeadroomOnlyPublishedWithTheCapability(t *testing.T) { |
| + reg := edgesTestRegistry(t) |
| + if got := headroomFor(nil, t.TempDir(), reg); got != nil { |
| + t.Fatalf("flag off: headroom = %+v, want nil", got) |
| + } |
| + if got := headroomFor([]string{capRegionHA1}, t.TempDir(), reg); got == nil { |
| + t.Fatal("flag on: headroom is nil") |
| + } |
| +} |