| 1 | //go:build linux |
| 2 | |
| 3 | package cgroup |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "reflect" |
| 11 | "strconv" |
| 12 | "testing" |
| 13 | "time" |
| 14 | ) |
| 15 | |
| 16 | func cleanupFixture(t *testing.T) (*Manager, string, string) { |
| 17 | t.Helper() |
| 18 | host, root := "cleanup.example", t.TempDir() |
| 19 | dir := filepath.Join(root, ExecName(host)) |
| 20 | if err := os.Mkdir(dir, 0700); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | for name, body := range map[string]string{"cgroup.events": "populated 1\n", "cgroup.procs": "123\n456\n", "cgroup.kill": ""} { |
| 24 | if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0600); err != nil { |
| 25 | t.Fatal(err) |
| 26 | } |
| 27 | } |
| 28 | return &Manager{root: root, logf: t.Logf}, host, dir |
| 29 | } |
| 30 | |
| 31 | func fixtureCleanup(t *testing.T, m *Manager, host string) *Cleanup { |
| 32 | t.Helper() |
| 33 | c, err := m.openCleanup(host, func(*os.File) error { return nil }) |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | t.Cleanup(func() { _ = c.Close() }) |
| 38 | return c |
| 39 | } |
| 40 | |
| 41 | func TestCleanupReopensAndDrains(t *testing.T) { |
| 42 | m, host, dir := cleanupFixture(t) |
| 43 | c := fixtureCleanup(t, m, host) |
| 44 | if _, exists := reflect.TypeOf(c).MethodByName("Start"); exists { |
| 45 | t.Fatal("cleanup grants spawn") |
| 46 | } |
| 47 | if populated, err := c.Populated(); err != nil || !populated { |
| 48 | t.Fatal(populated, err) |
| 49 | } |
| 50 | if members, err := c.Members(); err != nil || !reflect.DeepEqual(members, []int{123, 456}) { |
| 51 | t.Fatal(members, err) |
| 52 | } |
| 53 | ctx, cancel := context.WithTimeout(t.Context(), 30*time.Millisecond) |
| 54 | defer cancel() |
| 55 | if err := c.KillAndEmpty(ctx); !errors.Is(err, context.DeadlineExceeded) { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | if b, err := os.ReadFile(filepath.Join(dir, "cgroup.kill")); err != nil || string(b) != "1" { |
| 59 | t.Fatalf("kill: %q %v", b, err) |
| 60 | } |
| 61 | if err := os.WriteFile(filepath.Join(dir, "cgroup.events"), []byte("populated 0\n"), 0600); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | ctx2, cancel2 := context.WithTimeout(t.Context(), time.Second) |
| 65 | defer cancel2() |
| 66 | // Population alone is insufficient: the members still prevent success. |
| 67 | ctx3, cancel3 := context.WithTimeout(t.Context(), 30*time.Millisecond) |
| 68 | defer cancel3() |
| 69 | if err := c.KillAndEmpty(ctx3); !errors.Is(err, context.DeadlineExceeded) { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | if err := os.WriteFile(filepath.Join(dir, "cgroup.procs"), nil, 0600); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | if err := c.KillAndEmpty(ctx2); err != nil { |
| 76 | t.Fatal(err) |
| 77 | } |
| 78 | if err := c.Close(); err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | if _, err := c.Populated(); !errors.Is(err, ErrStrictState) { |
| 82 | t.Fatal(err) |
| 83 | } |
| 84 | if _, err := os.Stat(dir); err != nil { |
| 85 | t.Fatal("close removed group", err) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | func TestCleanupRefusals(t *testing.T) { |
| 90 | var absent *Manager |
| 91 | if _, err := absent.OpenCleanup("cleanup.example"); !errors.Is(err, ErrNoDelegation) { |
| 92 | t.Fatal(err) |
| 93 | } |
| 94 | if _, err := (&Manager{}).OpenCleanup("cleanup.example"); !errors.Is(err, ErrNoDelegation) { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | m, host, dir := cleanupFixture(t) |
| 98 | if _, err := m.OpenCleanup(host); !errors.Is(err, ErrStrictUnsupported) { |
| 99 | t.Fatal("ordinary filesystem admitted", err) |
| 100 | } |
| 101 | if _, err := m.openCleanup("../outside", func(*os.File) error { return nil }); !errors.Is(err, ErrStrictRefused) { |
| 102 | t.Fatal(err) |
| 103 | } |
| 104 | if _, err := m.openCleanup("missing.example", func(*os.File) error { return nil }); !errors.Is(err, ErrNoCgroup) { |
| 105 | t.Fatal(err) |
| 106 | } |
| 107 | if _, err := os.Stat(filepath.Join(m.root, ExecName("missing.example"))); !errors.Is(err, os.ErrNotExist) { |
| 108 | t.Fatal("created missing group", err) |
| 109 | } |
| 110 | c := fixtureCleanup(t, m, host) |
| 111 | for _, body := range []string{"", "populated 2\n", "populated 0\npopulated 0\n", "broken\n"} { |
| 112 | if err := os.WriteFile(filepath.Join(dir, "cgroup.events"), []byte(body), 0600); err != nil { |
| 113 | t.Fatal(err) |
| 114 | } |
| 115 | if _, err := c.Populated(); err == nil { |
| 116 | t.Fatal("malformed events accepted") |
| 117 | } |
| 118 | } |
| 119 | for _, body := range []string{"0\n", "-1\n", "oops\n", "9999999999999999999999999\n"} { |
| 120 | if err := os.WriteFile(filepath.Join(dir, "cgroup.procs"), []byte(body), 0600); err != nil { |
| 121 | t.Fatal(err) |
| 122 | } |
| 123 | if _, err := c.Members(); err == nil { |
| 124 | t.Fatal("malformed members accepted") |
| 125 | } |
| 126 | } |
| 127 | if err := c.KillAndEmpty(context.Background()); !errors.Is(err, ErrStrictDeadline) { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | if err := c.KillAndEmpty(nil); !errors.Is(err, ErrStrictDeadline) { |
| 131 | t.Fatal(err) |
| 132 | } |
| 133 | if err := os.Rename(dir, dir+".old"); err != nil { |
| 134 | t.Fatal(err) |
| 135 | } |
| 136 | if err := os.Mkdir(dir, 0700); err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | if _, err := c.Populated(); err == nil { |
| 140 | t.Fatal("replaced identity accepted") |
| 141 | } |
| 142 | if _, err := c.Members(); err == nil { |
| 143 | t.Fatal("replaced membership accepted") |
| 144 | } |
| 145 | if err := c.Close(); err != nil { |
| 146 | t.Fatal(err) |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | func TestCleanupDeadlineWhileLocked(t *testing.T) { |
| 151 | m, host, _ := cleanupFixture(t) |
| 152 | c := fixtureCleanup(t, m, host) |
| 153 | c.state.pin.mu.Lock() |
| 154 | ctx, cancel := context.WithTimeout(t.Context(), 30*time.Millisecond) |
| 155 | defer cancel() |
| 156 | done := make(chan error, 1) |
| 157 | go func() { done <- c.KillAndEmpty(ctx) }() |
| 158 | select { |
| 159 | case err := <-done: |
| 160 | c.state.pin.mu.Unlock() |
| 161 | if !errors.Is(err, context.DeadlineExceeded) { |
| 162 | t.Fatal(err) |
| 163 | } |
| 164 | case <-time.After(time.Second): |
| 165 | c.state.pin.mu.Unlock() |
| 166 | <-done |
| 167 | t.Fatal("cleanup exceeded its deadline waiting for a handle lock") |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | func TestCleanupRejectsMissingControlsAndSymlinks(t *testing.T) { |
| 172 | for _, name := range []string{"cgroup.events", "cgroup.procs", "cgroup.kill"} { |
| 173 | t.Run(name, func(t *testing.T) { |
| 174 | m, host, dir := cleanupFixture(t) |
| 175 | if err := os.Remove(filepath.Join(dir, name)); err != nil { |
| 176 | t.Fatal(err) |
| 177 | } |
| 178 | if c, err := m.openCleanup(host, func(*os.File) error { return nil }); err == nil || c != nil || errors.Is(err, ErrNoCgroup) { |
| 179 | t.Fatalf("missing control: %v %v", c, err) |
| 180 | } |
| 181 | if err := os.Symlink(filepath.Join(dir, "elsewhere"), filepath.Join(dir, name)); err != nil { |
| 182 | t.Fatal(err) |
| 183 | } |
| 184 | if c, err := m.openCleanup(host, func(*os.File) error { return nil }); err == nil || c != nil { |
| 185 | t.Fatalf("symlink control: %v %v", c, err) |
| 186 | } |
| 187 | }) |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | // A kernel before 5.14 has no cgroup.kill. A cleanup opened there names |
| 192 | // that (ErrNoKill), so a caller can fall back to the leader-only proof |
| 193 | // instead of reading it as an unproven kill on every stop. A cgroup.kill |
| 194 | // that is there but a symlink is not that case. |
| 195 | func TestCleanupNamesAMissingKillFile(t *testing.T) { |
| 196 | m, host, dir := cleanupFixture(t) |
| 197 | if err := os.Remove(filepath.Join(dir, "cgroup.kill")); err != nil { |
| 198 | t.Fatal(err) |
| 199 | } |
| 200 | if c, err := m.openCleanup(host, func(*os.File) error { return nil }); c != nil || !errors.Is(err, ErrNoKill) { |
| 201 | t.Fatalf("missing cgroup.kill: %v %v, want ErrNoKill", c, err) |
| 202 | } |
| 203 | if err := os.Symlink(filepath.Join(dir, "elsewhere"), filepath.Join(dir, "cgroup.kill")); err != nil { |
| 204 | t.Fatal(err) |
| 205 | } |
| 206 | if c, err := m.openCleanup(host, func(*os.File) error { return nil }); c != nil || err == nil || errors.Is(err, ErrNoKill) { |
| 207 | t.Fatalf("symlinked cgroup.kill: %v %v, want a refusal that is not ErrNoKill", c, err) |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | func mkCgroupFixture(t *testing.T, dir, procs, events string) { |
| 212 | t.Helper() |
| 213 | if err := os.MkdirAll(dir, 0o700); err != nil { |
| 214 | t.Fatal(err) |
| 215 | } |
| 216 | for name, body := range map[string]string{"cgroup.events": events, "cgroup.procs": procs, "cgroup.kill": ""} { |
| 217 | if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil { |
| 218 | t.Fatal(err) |
| 219 | } |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | func fixtureCleanupAt(t *testing.T, root, host, child string) *Cleanup { |
| 224 | t.Helper() |
| 225 | c, err := (&Manager{root: root}).openCleanupAt(host, child, func(*os.File) error { return nil }) |
| 226 | if err != nil { |
| 227 | t.Fatal(err) |
| 228 | } |
| 229 | t.Cleanup(func() { _ = c.Close() }) |
| 230 | return c |
| 231 | } |
| 232 | |
| 233 | // Stop evidence is taken on the parent (spec §7.3). A generation parent's |
| 234 | // own cgroup.procs is always empty; its generations' are not. |
| 235 | func TestCleanupMembersReachEveryGeneration(t *testing.T) { |
| 236 | host, root := "gens.example", t.TempDir() |
| 237 | parent := filepath.Join(root, ExecName(host)) |
| 238 | mkCgroupFixture(t, parent, "", "populated 1\n") |
| 239 | mkCgroupFixture(t, filepath.Join(parent, "g1"), "11\n", "populated 1\n") |
| 240 | mkCgroupFixture(t, filepath.Join(parent, "g2"), "22\n", "populated 1\n") |
| 241 | // Review focus 3: a flat tenant runs as the agent's uid and can make |
| 242 | // cgroups of its own under its generation. |
| 243 | mkCgroupFixture(t, filepath.Join(parent, "g2", "tenant"), "33\n", "populated 1\n") |
| 244 | c := fixtureCleanupAt(t, root, host, "") |
| 245 | members, err := c.Members() |
| 246 | if err != nil || !reflect.DeepEqual(members, []int{11, 22, 33}) { |
| 247 | t.Fatalf("members = %v, %v; want every generation's", members, err) |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | func TestCleanupMembersRefuseATreeTooDeep(t *testing.T) { |
| 252 | host, root := "deep.example", t.TempDir() |
| 253 | dir := filepath.Join(root, ExecName(host)) |
| 254 | mkCgroupFixture(t, dir, "", "populated 1\n") |
| 255 | for i := range maxCleanupDepth + 1 { |
| 256 | dir = filepath.Join(dir, "d"+strconv.Itoa(i)) |
| 257 | mkCgroupFixture(t, dir, "", "populated 0\n") |
| 258 | } |
| 259 | c := fixtureCleanupAt(t, root, host, "") |
| 260 | if _, err := c.Members(); !errors.Is(err, ErrStrictRefused) { |
| 261 | t.Fatalf("a tree deeper than %d: %v, want ErrStrictRefused", maxCleanupDepth, err) |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | // The group budget is a width bound too, and it is enforced while the |
| 266 | // directory is read: a tree one group too wide is refused before any child |
| 267 | // is opened. Every child here is unreadable (mode 0), so a walk that |
| 268 | // opened one before counting them all would fail with EACCES instead. |
| 269 | func TestCleanupMembersRefuseATreeTooWide(t *testing.T) { |
| 270 | if os.Geteuid() == 0 { |
| 271 | t.Skip("root opens a mode-0 directory; the probe needs an ordinary user") |
| 272 | } |
| 273 | host, root := "wide.example", t.TempDir() |
| 274 | dir := filepath.Join(root, ExecName(host)) |
| 275 | mkCgroupFixture(t, dir, "", "populated 1\n") |
| 276 | for i := range maxCleanupGroups + 1 { |
| 277 | if err := os.Mkdir(filepath.Join(dir, "w"+strconv.Itoa(i)), 0); err != nil { |
| 278 | t.Fatal(err) |
| 279 | } |
| 280 | } |
| 281 | c := fixtureCleanupAt(t, root, host, "") |
| 282 | if _, err := c.Members(); !errors.Is(err, ErrStrictRefused) { |
| 283 | t.Fatalf("%d groups at one level: %v, want ErrStrictRefused before any child is opened", maxCleanupGroups+1, err) |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | func TestCleanupMembersAcceptATreeExactlyAtTheBound(t *testing.T) { |
| 288 | host, root := "bound.example", t.TempDir() |
| 289 | dir := filepath.Join(root, ExecName(host)) |
| 290 | mkCgroupFixture(t, dir, "", "populated 1\n") |
| 291 | for i := range maxCleanupGroups { |
| 292 | if err := os.Mkdir(filepath.Join(dir, "w"+strconv.Itoa(i)), 0o700); err != nil { |
| 293 | t.Fatal(err) |
| 294 | } |
| 295 | } |
| 296 | mkCgroupFixture(t, filepath.Join(dir, "w7"), "77\n", "populated 1\n") |
| 297 | c := fixtureCleanupAt(t, root, host, "") |
| 298 | if members, err := c.Members(); err != nil || !reflect.DeepEqual(members, []int{77}) { |
| 299 | t.Fatalf("%d groups: %v, %v; want [77]", maxCleanupGroups, members, err) |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | // KillAndEmpty on a generation acts only on that generation's child. |
| 304 | func TestCleanupChildKillsOnlyItsGeneration(t *testing.T) { |
| 305 | host, root := "kill.example", t.TempDir() |
| 306 | parent := filepath.Join(root, ExecName(host)) |
| 307 | mkCgroupFixture(t, parent, "", "populated 1\n") |
| 308 | mkCgroupFixture(t, filepath.Join(parent, "g1"), "11\n", "populated 1\n") |
| 309 | mkCgroupFixture(t, filepath.Join(parent, "g2"), "", "populated 0\n") |
| 310 | c := fixtureCleanupAt(t, root, host, "g2") |
| 311 | ctx, cancel := context.WithTimeout(t.Context(), time.Second) |
| 312 | defer cancel() |
| 313 | if err := c.KillAndEmpty(ctx); err != nil { |
| 314 | t.Fatal(err) |
| 315 | } |
| 316 | for dir, want := range map[string]string{filepath.Join(parent, "g2"): "1", filepath.Join(parent, "g1"): "", parent: ""} { |
| 317 | if got := readFixture(t, filepath.Join(dir, "cgroup.kill")); got != want { |
| 318 | t.Errorf("%s/cgroup.kill = %q, want %q", dir, got, want) |
| 319 | } |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | func TestOpenCleanupChildRefusesForeignNamesAndMissingChildren(t *testing.T) { |
| 324 | host, root := "names.example", t.TempDir() |
| 325 | mkCgroupFixture(t, filepath.Join(root, ExecName(host)), "", "populated 0\n") |
| 326 | m := &Manager{root: root} |
| 327 | for _, child := range []string{"child", "../x", "g0"} { |
| 328 | if _, err := m.openCleanupAt(host, child, func(*os.File) error { return nil }); !errors.Is(err, ErrStrictRefused) { |
| 329 | t.Errorf("child %q: %v, want ErrStrictRefused", child, err) |
| 330 | } |
| 331 | } |
| 332 | if _, err := m.openCleanupAt(host, "g9", func(*os.File) error { return nil }); !errors.Is(err, ErrNoCgroup) { |
| 333 | t.Errorf("missing g9: %v, want ErrNoCgroup", err) |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | // Review focus 4, Ruling 6: a process being placed under the host (here a |
| 338 | // generation into g1) is a member the kernel cannot see yet. While it is in |
| 339 | // flight the host's cleanup reports populated, refuses to list members, |
| 340 | // and kills nothing; once it resolves, Populated reads cgroup.events again. |
| 341 | func TestCleanupCountsPlacementInFlight(t *testing.T) { |
| 342 | m, host, dir := cleanupFixture(t) |
| 343 | writeFixture(t, filepath.Join(dir, "cgroup.events"), "populated 0\n") |
| 344 | writeFixture(t, filepath.Join(dir, "cgroup.procs"), "") |
| 345 | c := fixtureCleanup(t, m, host) |
| 346 | done := m.beginPlacement(filepath.Join(dir, "g1")) |
| 347 | defer done() |
| 348 | if populated, err := c.Populated(); err != nil || !populated { |
| 349 | t.Fatalf("Populated = %v, %v with a placement in flight: want true", populated, err) |
| 350 | } |
| 351 | if members, err := c.Members(); !errors.Is(err, ErrPlacementInFlight) { |
| 352 | t.Fatalf("Members = %v, %v with a placement in flight: want ErrPlacementInFlight", members, err) |
| 353 | } |
| 354 | ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond) |
| 355 | defer cancel() |
| 356 | if err := c.KillAndEmpty(ctx); err == nil { |
| 357 | t.Fatal("KillAndEmpty proved the host empty with a placement in flight") |
| 358 | } |
| 359 | if got := readFixture(t, filepath.Join(dir, "cgroup.kill")); got != "" { |
| 360 | t.Fatalf("cgroup.kill = %q: the kill was written before the placement resolved", got) |
| 361 | } |
| 362 | done() |
| 363 | if populated, err := c.Populated(); err != nil || populated { |
| 364 | t.Fatalf("Populated = %v, %v once the placement resolved: want cgroup.events' answer, false", populated, err) |
| 365 | } |
| 366 | if members, err := c.Members(); err != nil || len(members) != 0 { |
| 367 | t.Fatalf("Members = %v, %v once the placement resolved: want none", members, err) |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | // Ruling 6: a placement that registers after KillAndEmpty's wait, here |
| 372 | // right after the kill is written, keeps the proof from finishing: every |
| 373 | // emptiness check after the kill counts it. The fake cgroupfs says empty |
| 374 | // throughout, so only the registry stands between the proof and a false |
| 375 | // empty. |
| 376 | func TestKillAndEmptySeesAPlacementRegisteredMidProof(t *testing.T) { |
| 377 | m, host, dir := cleanupFixture(t) |
| 378 | writeFixture(t, filepath.Join(dir, "cgroup.events"), "populated 0\n") |
| 379 | writeFixture(t, filepath.Join(dir, "cgroup.procs"), "") |
| 380 | c := fixtureCleanup(t, m, host) |
| 381 | var done func() |
| 382 | orig := cleanupKillWrite |
| 383 | cleanupKillWrite = func(f *os.File) error { |
| 384 | err := orig(f) |
| 385 | done = m.beginPlacement(filepath.Join(dir, "g1")) |
| 386 | return err |
| 387 | } |
| 388 | t.Cleanup(func() { |
| 389 | cleanupKillWrite = orig |
| 390 | if done != nil { |
| 391 | done() |
| 392 | } |
| 393 | }) |
| 394 | ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) |
| 395 | defer cancel() |
| 396 | if err := c.KillAndEmpty(ctx); err == nil { |
| 397 | t.Fatal("KillAndEmpty proved the host empty while a placement registered mid-proof was in flight") |
| 398 | } |
| 399 | if done == nil { |
| 400 | t.Fatal("the kill was never written: the test proves nothing") |
| 401 | } |
| 402 | // The control: with nothing in flight the same proof finishes. |
| 403 | done() |
| 404 | ctx2, cancel2 := context.WithTimeout(t.Context(), time.Second) |
| 405 | defer cancel2() |
| 406 | cleanupKillWrite = orig |
| 407 | if err := c.KillAndEmpty(ctx2); err != nil { |
| 408 | t.Fatalf("KillAndEmpty with nothing in flight = %v, want the proof", err) |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | // Whole-branch review, finding 1, first schedule: a placement registers |
| 413 | // while the kernel is being read (the hook runs right after the read, before |
| 414 | // the registry is asked again). The fake cgroupfs says empty, so only the |
| 415 | // registry can stop a false empty: Populated must answer populated and |
| 416 | // Members must refuse, even though nothing was in flight when either began. |
| 417 | func TestCleanupCountsAPlacementRegisteredDuringTheRead(t *testing.T) { |
| 418 | m, host, dir := cleanupFixture(t) |
| 419 | writeFixture(t, filepath.Join(dir, "cgroup.events"), "populated 0\n") |
| 420 | writeFixture(t, filepath.Join(dir, "cgroup.procs"), "") |
| 421 | c := fixtureCleanup(t, m, host) |
| 422 | var dones []func() |
| 423 | t.Cleanup(func() { |
| 424 | for _, d := range dones { |
| 425 | d() |
| 426 | } |
| 427 | }) |
| 428 | reads := 0 |
| 429 | setPlacementReadHook(t, func(string) { |
| 430 | reads++ |
| 431 | dones = append(dones, m.beginPlacement(filepath.Join(dir, "g1"))) |
| 432 | }) |
| 433 | if populated, err := c.Populated(); err != nil || !populated { |
| 434 | t.Fatalf("Populated = %v, %v with a placement registered during the read: want true", populated, err) |
| 435 | } |
| 436 | for _, d := range dones { |
| 437 | d() |
| 438 | } |
| 439 | if members, err := c.Members(); !errors.Is(err, ErrPlacementInFlight) { |
| 440 | t.Fatalf("Members = %v, %v with a placement registered during the read: want ErrPlacementInFlight", members, err) |
| 441 | } |
| 442 | if reads != 2 { |
| 443 | t.Fatalf("the read hook ran %d times, want 2: the test proves nothing", reads) |
| 444 | } |
| 445 | // The control: the same reads with nothing registering answer empty. |
| 446 | for _, d := range dones { |
| 447 | d() |
| 448 | } |
| 449 | placementReadHook = nil |
| 450 | if populated, err := c.Populated(); err != nil || populated { |
| 451 | t.Fatalf("Populated = %v, %v with nothing in flight: want false", populated, err) |
| 452 | } |
| 453 | if members, err := c.Members(); err != nil || len(members) != 0 { |
| 454 | t.Fatalf("Members = %v, %v with nothing in flight: want none", members, err) |
| 455 | } |
| 456 | } |
| 457 | |
| 458 | // Finding 1, second schedule: a placement in flight when the kernel is read |
| 459 | // lands and ends its registration between the kernel read and the |
| 460 | // registry's. Asked in that order, the registry would read zero beside a |
| 461 | // kernel view taken before the process arrived. Both answers must count it. |
| 462 | func TestCleanupCountsAPlacementThatEndsBetweenTheReads(t *testing.T) { |
| 463 | m, host, dir := cleanupFixture(t) |
| 464 | writeFixture(t, filepath.Join(dir, "cgroup.events"), "populated 0\n") |
| 465 | writeFixture(t, filepath.Join(dir, "cgroup.procs"), "") |
| 466 | c := fixtureCleanup(t, m, host) |
| 467 | var done func() |
| 468 | setPlacementReadHook(t, func(string) { |
| 469 | if done != nil { |
| 470 | done() |
| 471 | } |
| 472 | }) |
| 473 | done = m.beginPlacement(filepath.Join(dir, "g1")) |
| 474 | if populated, err := c.Populated(); err != nil || !populated { |
| 475 | t.Fatalf("Populated = %v, %v with a placement that ended after the kernel read: want true", populated, err) |
| 476 | } |
| 477 | done = m.beginPlacement(filepath.Join(dir, "g1")) |
| 478 | if members, err := c.Members(); !errors.Is(err, ErrPlacementInFlight) { |
| 479 | t.Fatalf("Members = %v, %v with a placement that ended after the kernel read: want ErrPlacementInFlight", members, err) |
| 480 | } |
| 481 | if m.placementsUnder(dir) != 0 { |
| 482 | t.Fatal("the hook did not end the placements: the test proves nothing") |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | // Finding 1 through KillAndEmpty: a placement registered during each |
| 487 | // emptiness read after the kill keeps the proof from finishing, so it ends |
| 488 | // unproven at ctx's end and never answers empty. |
| 489 | func TestKillAndEmptyCountsAPlacementRegisteredDuringARead(t *testing.T) { |
| 490 | m, host, dir := cleanupFixture(t) |
| 491 | writeFixture(t, filepath.Join(dir, "cgroup.events"), "populated 0\n") |
| 492 | writeFixture(t, filepath.Join(dir, "cgroup.procs"), "") |
| 493 | c := fixtureCleanup(t, m, host) |
| 494 | var dones []func() |
| 495 | t.Cleanup(func() { |
| 496 | for _, d := range dones { |
| 497 | d() |
| 498 | } |
| 499 | }) |
| 500 | setPlacementReadHook(t, func(string) { |
| 501 | // Each read sees one registration begin; the previous one ends, so |
| 502 | // a check that asked the registry only before its read would see |
| 503 | // nothing in flight. |
| 504 | for _, d := range dones { |
| 505 | d() |
| 506 | } |
| 507 | dones = append(dones, m.beginPlacement(filepath.Join(dir, "g1"))) |
| 508 | }) |
| 509 | ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) |
| 510 | defer cancel() |
| 511 | if err := c.KillAndEmpty(ctx); !errors.Is(err, context.DeadlineExceeded) { |
| 512 | t.Fatalf("KillAndEmpty = %v, want unproven at ctx's end (context.DeadlineExceeded)", err) |
| 513 | } |
| 514 | if len(dones) == 0 { |
| 515 | t.Fatal("no emptiness read ran: the test proves nothing") |
| 516 | } |
| 517 | } |
| 518 | |