diff options
Diffstat (limited to 'src/runtime/cgroup_linux_test.go')
| -rw-r--r-- | src/runtime/cgroup_linux_test.go | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/src/runtime/cgroup_linux_test.go b/src/runtime/cgroup_linux_test.go new file mode 100644 index 0000000000..0b060572b6 --- /dev/null +++ b/src/runtime/cgroup_linux_test.go @@ -0,0 +1,325 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package runtime_test + +import ( + "fmt" + "internal/cgrouptest" + "runtime" + "strings" + "syscall" + "testing" + "unsafe" +) + +func mustHaveFourCPUs(t *testing.T) { + // If NumCPU is lower than the cgroup limit, GOMAXPROCS will use + // NumCPU. + // + // cgroup GOMAXPROCS also have a minimum of 2. We need some room above + // that to test interesting properies. + if runtime.NumCPU() < 4 { + t.Helper() + t.Skip("skipping test: fewer than 4 CPUs") + } +} + +func TestCgroupGOMAXPROCS(t *testing.T) { + mustHaveFourCPUs(t) + + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + godebug int + want int + }{ + // With containermaxprocs=1, GOMAXPROCS should use the cgroup + // limit. + { + godebug: 1, + want: 3, + }, + // With containermaxprocs=0, it should be ignored. + { + godebug: 0, + want: runtime.NumCPU(), + }, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("containermaxprocs=%d", tc.godebug), func(t *testing.T) { + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + if err := c.SetCPUMax(300000, 100000); err != nil { + t.Fatalf("unable to set CPU limit: %v", err) + } + + got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS", fmt.Sprintf("GODEBUG=containermaxprocs=%d", tc.godebug)) + want := fmt.Sprintf("%d\n", tc.want) + if got != want { + t.Fatalf("output got %q want %q", got, want) + } + }) + }) + } +} + +// Without a cgroup limit, GOMAXPROCS uses NumCPU. +func TestCgroupGOMAXPROCSNoLimit(t *testing.T) { + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + if err := c.SetCPUMax(-1, 100000); err != nil { + t.Fatalf("unable to set CPU limit: %v", err) + } + + got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS") + want := fmt.Sprintf("%d\n", runtime.NumCPU()) + if got != want { + t.Fatalf("output got %q want %q", got, want) + } + }) +} + +// If the cgroup limit is higher than NumCPU, GOMAXPROCS uses NumCPU. +func TestCgroupGOMAXPROCSHigherThanNumCPU(t *testing.T) { + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + if err := c.SetCPUMax(2*int64(runtime.NumCPU())*100000, 100000); err != nil { + t.Fatalf("unable to set CPU limit: %v", err) + } + + got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS") + want := fmt.Sprintf("%d\n", runtime.NumCPU()) + if got != want { + t.Fatalf("output got %q want %q", got, want) + } + }) +} + +func TestCgroupGOMAXPROCSRound(t *testing.T) { + mustHaveFourCPUs(t) + + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + quota int64 + want int + }{ + // We always round the fractional component up. + { + quota: 200001, + want: 3, + }, + { + quota: 250000, + want: 3, + }, + { + quota: 299999, + want: 3, + }, + // Anything less than two rounds up to a minimum of 2. + { + quota: 50000, // 0.5 + want: 2, + }, + { + quota: 100000, + want: 2, + }, + { + quota: 150000, + want: 2, + }, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("%d", tc.quota), func(t *testing.T) { + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + if err := c.SetCPUMax(tc.quota, 100000); err != nil { + t.Fatalf("unable to set CPU limit: %v", err) + } + + got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS") + want := fmt.Sprintf("%d\n", tc.want) + if got != want { + t.Fatalf("output got %q want %q", got, want) + } + }) + }) + } +} + +// Environment variable takes precedence over defaults. +func TestCgroupGOMAXPROCSEnvironment(t *testing.T) { + mustHaveFourCPUs(t) + + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + if err := c.SetCPUMax(200000, 100000); err != nil { + t.Fatalf("unable to set CPU limit: %v", err) + } + + got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS", "GOMAXPROCS=3") + want := "3\n" + if got != want { + t.Fatalf("output got %q want %q", got, want) + } + }) +} + +// CPU affinity takes priority if lower than cgroup limit. +func TestCgroupGOMAXPROCSSchedAffinity(t *testing.T) { + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + if err := c.SetCPUMax(300000, 100000); err != nil { + t.Fatalf("unable to set CPU limit: %v", err) + } + + // CPU affinity is actually a per-thread attribute. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + const maxCPUs = 64 * 1024 + var orig [maxCPUs / 8]byte + _, _, errno := syscall.Syscall6(syscall.SYS_SCHED_GETAFFINITY, 0, unsafe.Sizeof(orig), uintptr(unsafe.Pointer(&orig[0])), 0, 0, 0) + if errno != 0 { + t.Fatalf("unable to get CPU affinity: %v", errno) + } + + // We're going to restrict to CPUs 0 and 1. Make sure those are already available. + if orig[0]&0b11 != 0b11 { + t.Skipf("skipping test: CPUs 0 and 1 not available") + } + + var mask [maxCPUs / 8]byte + mask[0] = 0b11 + _, _, errno = syscall.Syscall6(syscall.SYS_SCHED_SETAFFINITY, 0, unsafe.Sizeof(mask), uintptr(unsafe.Pointer(&mask[0])), 0, 0, 0) + if errno != 0 { + t.Fatalf("unable to set CPU affinity: %v", errno) + } + defer func() { + _, _, errno = syscall.Syscall6(syscall.SYS_SCHED_SETAFFINITY, 0, unsafe.Sizeof(orig), uintptr(unsafe.Pointer(&orig[0])), 0, 0, 0) + if errno != 0 { + t.Fatalf("unable to restore CPU affinity: %v", errno) + } + }() + + got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS") + want := "2\n" + if got != want { + t.Fatalf("output got %q want %q", got, want) + } + }) +} + +func TestCgroupGOMAXPROCSSetDefault(t *testing.T) { + mustHaveFourCPUs(t) + + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + godebug int + want int + }{ + // With containermaxprocs=1, SetDefaultGOMAXPROCS should observe + // the cgroup limit. + { + godebug: 1, + want: 3, + }, + // With containermaxprocs=0, it should be ignored. + { + godebug: 0, + want: runtime.NumCPU(), + }, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("containermaxprocs=%d", tc.godebug), func(t *testing.T) { + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + env := []string{ + fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()), + "GO_TEST_CPU_MAX_QUOTA=300000", + fmt.Sprintf("GODEBUG=containermaxprocs=%d", tc.godebug), + } + got := runBuiltTestProg(t, exe, "SetLimitThenDefaultGOMAXPROCS", env...) + want := fmt.Sprintf("%d\n", tc.want) + if got != want { + t.Fatalf("output got %q want %q", got, want) + } + }) + }) + } +} + +func TestCgroupGOMAXPROCSUpdate(t *testing.T) { + mustHaveFourCPUs(t) + + if testing.Short() { + t.Skip("skipping test: long sleeps") + } + + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + got := runBuiltTestProg(t, exe, "UpdateGOMAXPROCS", fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath())) + if !strings.Contains(got, "OK") { + t.Fatalf("output got %q want OK", got) + } + }) +} + +func TestCgroupGOMAXPROCSDontUpdate(t *testing.T) { + mustHaveFourCPUs(t) + + if testing.Short() { + t.Skip("skipping test: long sleeps") + } + + exe, err := buildTestProg(t, "testprog") + if err != nil { + t.Fatal(err) + } + + // Two ways to disable updates: explicit GOMAXPROCS or GODEBUG for + // update feature. + for _, v := range []string{"GOMAXPROCS=4", "GODEBUG=updatemaxprocs=0"} { + t.Run(v, func(t *testing.T) { + cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) { + got := runBuiltTestProg(t, exe, "DontUpdateGOMAXPROCS", + fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()), + v) + if !strings.Contains(got, "OK") { + t.Fatalf("output got %q want OK", got) + } + }) + }) + } +} |
