zdd plan 2 task 5: the sampler counts a populated generation parent
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2 files changed,
+49
−1
internal/host/metrics_probes.go+14 −1internal/host/metrics_probes_test.go+35 −0
diff --git a/internal/host/metrics_probes.go b/internal/host/metrics_probes.go| index 255e33d9c..6ebe91951 100644 |
| --- a/internal/host/metrics_probes.go |
| +++ b/internal/host/metrics_probes.go |
| @@ -265,10 +265,23 @@ func listExecCgroups(root string) []unitCgroup { |
| } |
| dir := filepath.Join(root, entry.Name()) |
| procs, err := os.ReadFile(filepath.Join(dir, "cgroup.procs")) |
| - if err != nil || len(strings.TrimSpace(string(procs))) == 0 { |
| + if err != nil { |
| + continue |
| + } |
| + // A generation parent (zero-downtime spec §7.3) holds no process of |
| + // its own; its generations do. cgroup.events "populated 1" is the |
| + // kernel's word that something under it is alive. |
| + if len(strings.TrimSpace(string(procs))) == 0 && !subtreePopulated(dir) { |
| continue |
| } |
| units = append(units, unitCgroup{name: entry.Name(), dir: dir}) |
| } |
| return units |
| } |
| + |
| +// subtreePopulated reports whether dir's cgroup.events says a process is |
| +// alive anywhere under it. Unreadable is no: absence is the data. |
| +func subtreePopulated(dir string) bool { |
| + b, err := os.ReadFile(filepath.Join(dir, "cgroup.events")) |
| + return err == nil && strings.Contains(string(b), "populated 1") |
| +} |
diff --git a/internal/host/metrics_probes_test.go b/internal/host/metrics_probes_test.go| index 65100a32a..b155c8cee 100644 |
| --- a/internal/host/metrics_probes_test.go |
| +++ b/internal/host/metrics_probes_test.go |
| @@ -263,3 +263,38 @@ func TestDBFileBytes(t *testing.T) { |
| t.Fatalf("a directory = %d, want nil", *got) |
| } |
| } |
| + |
| +// A generation parent (zero-downtime spec §7.3) holds no process of its |
| +// own, so its cgroup.procs is empty while its generations run. It must |
| +// still be sampled, under its own name: its memory, cpu, io and pids |
| +// files already count the whole subtree. |
| +func TestListExecCgroupsCountsAGenerationParent(t *testing.T) { |
| + root := t.TempDir() |
| + mk := func(rel, file, body string) { |
| + t.Helper() |
| + dir := filepath.Join(root, rel) |
| + if err := os.MkdirAll(dir, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if file != "" { |
| + if err := os.WriteFile(filepath.Join(dir, file), []byte(body), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + mk("carlos-exec@flat.example.com", "cgroup.procs", "1234\n") |
| + mk("carlos-exec@gens.example.com", "cgroup.procs", "") |
| + mk("carlos-exec@gens.example.com", "cgroup.events", "populated 1\nfrozen 0\n") |
| + mk("carlos-exec@gens.example.com/g7", "cgroup.procs", "5678\n") |
| + mk("carlos-exec@empty.example.com", "cgroup.procs", "") |
| + mk("carlos-exec@empty.example.com", "cgroup.events", "populated 0\nfrozen 0\n") |
| + |
| + got := listExecCgroups(root) |
| + names := map[string]bool{} |
| + for _, u := range got { |
| + names[u.name] = true |
| + } |
| + if len(got) != 2 || !names["carlos-exec@flat.example.com"] || !names["carlos-exec@gens.example.com"] { |
| + t.Fatalf("sampled %+v, want the flat host and the populated generation parent", got) |
| + } |
| +} |