//go:build linux package cgroup import ( "context" "errors" "sync" "testing" "time" ) func TestPlacementsUnderCountsTheSubtree(t *testing.T) { m := &Manager{root: "/r", logf: t.Logf} done := m.beginPlacement("/r/carlos-exec@a.example/g2") if m.placementsUnder("/r/carlos-exec@a.example") != 1 || m.placementsAt("/r/carlos-exec@a.example") != 0 { t.Fatal("a child's placement is under its parent, not at it") } if m.placementsUnder("/r/carlos-exec@a.exampleX") != 0 { t.Fatal("a prefix without a separator matched") } if m.placementsAt("/r/carlos-exec@a.example/g2/") != 1 || m.placementsUnder("/r") != 1 { t.Fatal("the target itself or its root does not count it") } done() done() // idempotent if m.placementsUnder("/r/carlos-exec@a.example") != 0 { t.Fatal("done did not drop the count") } } func TestPlacementsCountEachInFlight(t *testing.T) { m := &Manager{root: "/r", logf: t.Logf} a := m.beginPlacement("/r/h") b := m.beginPlacement("/r/h") if m.placementsAt("/r/h") != 2 { t.Fatal("two placements at one target count as one") } a() a() if m.placementsAt("/r/h") != 1 { t.Fatal("a repeated done dropped another placement's count") } b() if m.placementsAt("/r/h") != 0 { t.Fatal("the count did not reach zero") } } func TestWaitPlacementsUnderWakes(t *testing.T) { m := &Manager{root: "/r", logf: t.Logf} if err := m.waitPlacementsUnder(context.Background(), "/r"); err != nil { t.Fatalf("nothing in flight: %v", err) } done := m.beginPlacement("/r/h/g1") ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() if err := m.waitPlacementsUnder(ctx, "/r/h"); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("err=%v, want context.DeadlineExceeded", err) } got := make(chan error, 1) go func() { got <- m.waitPlacementsUnder(context.Background(), "/r/h") }() // An unrelated placement ending wakes the waiter, which must keep waiting. other := m.beginPlacement("/r/elsewhere") other() select { case err := <-got: t.Fatalf("the waiter returned %v while a placement was in flight", err) case <-time.After(50 * time.Millisecond): } done() select { case err := <-got: if err != nil { t.Fatal(err) } case <-time.After(time.Second): t.Fatal("the waiter did not wake within 1s of done") } } func TestNilManagerPlacements(t *testing.T) { var m *Manager done := m.beginPlacement("/r/h") done() if m.placementsUnder("/r") != 0 || m.placementsAt("/r/h") != 0 { t.Fatal("a nil manager counted a placement") } if err := m.waitPlacementsUnder(context.Background(), "/r"); err != nil { t.Fatal(err) } } // readQuiet answers quiet only when nothing under its directory was in // flight at any moment of the read: not at its start, not registered or // ended during it, not at its end. Placements elsewhere do not count. func TestReadQuiet(t *testing.T) { m := &Manager{root: "/r", logf: t.Logf} if !m.readQuiet("/r/h", func() {}) { t.Fatal("nothing in flight read as not quiet") } other := m.beginPlacement("/r/hx/g1") defer other() if !m.readQuiet("/r/h", func() { m.beginPlacement("/r/other")() }) { t.Fatal("placements outside the directory made the read not quiet") } done := m.beginPlacement("/r/h/g1") if m.readQuiet("/r/h", func() {}) { t.Fatal("a placement in flight throughout read as quiet") } if m.readQuiet("/r/h", done) { t.Fatal("a placement that ended during the read read as quiet") } var late func() if m.readQuiet("/r/h", func() { late = m.beginPlacement("/r/h/g2"); late() }) { t.Fatal("a placement that registered and ended during the read read as quiet") } if !m.readQuiet("/r/h", func() {}) { t.Fatal("the read after everything ended is not quiet") } m.placeMu.Lock() open := len(m.windows) m.placeMu.Unlock() if open != 0 { t.Fatalf("%d windows left open", open) } var nilm *Manager ran := false if !nilm.readQuiet("/r", func() { ran = true }) || !ran { t.Fatal("a nil manager's read did not run or was not quiet") } } // holdPlacements stops registrations in and below its directory until it // is released, counts the ones already in flight, and leaves others alone. func TestHoldPlacementsBlocksRegistrationUntilReleased(t *testing.T) { m := &Manager{root: "/r", logf: t.Logf} before := m.beginPlacement("/r/h/g1") at, under, release := m.holdPlacements("/r/h") if at != 0 || under != 1 { t.Fatalf("hold counted %d at, %d under: want 0, 1", at, under) } // A placement already in flight still ends under the hold. before() // One elsewhere is not held. m.beginPlacement("/r/elsewhere")() blocked := make(chan struct{}) var once sync.Once orig := placementHeldHook placementHeldHook = func(string) { once.Do(func() { close(blocked) }) } t.Cleanup(func() { placementHeldHook = orig }) got := make(chan func(), 1) go func() { got <- m.beginPlacement("/r/h/g2") }() select { case <-blocked: case d := <-got: d() t.Fatal("a placement registered under a hold") case <-time.After(5 * time.Second): t.Fatal("the registration neither registered nor waited") } if n := m.placementsUnder("/r/h"); n != 0 { t.Fatalf("%d placements registered under the hold", n) } release() release() // idempotent select { case d := <-got: if n := m.placementsAt("/r/h/g2"); n != 1 { t.Fatalf("after the release %d placements at g2, want 1", n) } d() case <-time.After(5 * time.Second): t.Fatal("the registration did not go ahead once the hold was released") } var nilm *Manager if a, u, r := nilm.holdPlacements("/r"); a != 0 || u != 0 { t.Fatal("a nil manager counted placements") } else { r() } }