Source file src/runtime/cgroup_linux_test.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package runtime_test
     6  
     7  import (
     8  	"fmt"
     9  	"internal/cgrouptest"
    10  	"runtime"
    11  	"strings"
    12  	"syscall"
    13  	"testing"
    14  	"unsafe"
    15  )
    16  
    17  func mustHaveFourCPUs(t *testing.T) {
    18  	// If NumCPU is lower than the cgroup limit, GOMAXPROCS will use
    19  	// NumCPU.
    20  	//
    21  	// cgroup GOMAXPROCS also have a minimum of 2. We need some room above
    22  	// that to test interesting properies.
    23  	if runtime.NumCPU() < 4 {
    24  		t.Helper()
    25  		t.Skip("skipping test: fewer than 4 CPUs")
    26  	}
    27  }
    28  
    29  func TestCgroupGOMAXPROCS(t *testing.T) {
    30  	mustHaveFourCPUs(t)
    31  
    32  	exe, err := buildTestProg(t, "testprog")
    33  	if err != nil {
    34  		t.Fatal(err)
    35  	}
    36  
    37  	tests := []struct {
    38  		godebug int
    39  		want    int
    40  	}{
    41  		// With containermaxprocs=1, GOMAXPROCS should use the cgroup
    42  		// limit.
    43  		{
    44  			godebug: 1,
    45  			want:    3,
    46  		},
    47  		// With containermaxprocs=0, it should be ignored.
    48  		{
    49  			godebug: 0,
    50  			want:    runtime.NumCPU(),
    51  		},
    52  	}
    53  	for _, tc := range tests {
    54  		t.Run(fmt.Sprintf("containermaxprocs=%d", tc.godebug), func(t *testing.T) {
    55  			cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
    56  				if err := c.SetCPUMax(300000, 100000); err != nil {
    57  					t.Fatalf("unable to set CPU limit: %v", err)
    58  				}
    59  
    60  				got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS", fmt.Sprintf("GODEBUG=containermaxprocs=%d", tc.godebug))
    61  				want := fmt.Sprintf("%d\n", tc.want)
    62  				if got != want {
    63  					t.Fatalf("output got %q want %q", got, want)
    64  				}
    65  			})
    66  		})
    67  	}
    68  }
    69  
    70  // Without a cgroup limit, GOMAXPROCS uses NumCPU.
    71  func TestCgroupGOMAXPROCSNoLimit(t *testing.T) {
    72  	exe, err := buildTestProg(t, "testprog")
    73  	if err != nil {
    74  		t.Fatal(err)
    75  	}
    76  
    77  	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
    78  		if err := c.SetCPUMax(-1, 100000); err != nil {
    79  			t.Fatalf("unable to set CPU limit: %v", err)
    80  		}
    81  
    82  		got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS")
    83  		want := fmt.Sprintf("%d\n", runtime.NumCPU())
    84  		if got != want {
    85  			t.Fatalf("output got %q want %q", got, want)
    86  		}
    87  	})
    88  }
    89  
    90  // If the cgroup limit is higher than NumCPU, GOMAXPROCS uses NumCPU.
    91  func TestCgroupGOMAXPROCSHigherThanNumCPU(t *testing.T) {
    92  	exe, err := buildTestProg(t, "testprog")
    93  	if err != nil {
    94  		t.Fatal(err)
    95  	}
    96  
    97  	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
    98  		if err := c.SetCPUMax(2*int64(runtime.NumCPU())*100000, 100000); err != nil {
    99  			t.Fatalf("unable to set CPU limit: %v", err)
   100  		}
   101  
   102  		got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS")
   103  		want := fmt.Sprintf("%d\n", runtime.NumCPU())
   104  		if got != want {
   105  			t.Fatalf("output got %q want %q", got, want)
   106  		}
   107  	})
   108  }
   109  
   110  func TestCgroupGOMAXPROCSRound(t *testing.T) {
   111  	mustHaveFourCPUs(t)
   112  
   113  	exe, err := buildTestProg(t, "testprog")
   114  	if err != nil {
   115  		t.Fatal(err)
   116  	}
   117  
   118  	tests := []struct {
   119  		quota int64
   120  		want  int
   121  	}{
   122  		// We always round the fractional component up.
   123  		{
   124  			quota: 200001,
   125  			want:  3,
   126  		},
   127  		{
   128  			quota: 250000,
   129  			want:  3,
   130  		},
   131  		{
   132  			quota: 299999,
   133  			want:  3,
   134  		},
   135  		// Anything less than two rounds up to a minimum of 2.
   136  		{
   137  			quota: 50000, // 0.5
   138  			want:  2,
   139  		},
   140  		{
   141  			quota: 100000,
   142  			want:  2,
   143  		},
   144  		{
   145  			quota: 150000,
   146  			want:  2,
   147  		},
   148  	}
   149  	for _, tc := range tests {
   150  		t.Run(fmt.Sprintf("%d", tc.quota), func(t *testing.T) {
   151  			cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
   152  				if err := c.SetCPUMax(tc.quota, 100000); err != nil {
   153  					t.Fatalf("unable to set CPU limit: %v", err)
   154  				}
   155  
   156  				got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS")
   157  				want := fmt.Sprintf("%d\n", tc.want)
   158  				if got != want {
   159  					t.Fatalf("output got %q want %q", got, want)
   160  				}
   161  			})
   162  		})
   163  	}
   164  }
   165  
   166  // Environment variable takes precedence over defaults.
   167  func TestCgroupGOMAXPROCSEnvironment(t *testing.T) {
   168  	mustHaveFourCPUs(t)
   169  
   170  	exe, err := buildTestProg(t, "testprog")
   171  	if err != nil {
   172  		t.Fatal(err)
   173  	}
   174  
   175  	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
   176  		if err := c.SetCPUMax(200000, 100000); err != nil {
   177  			t.Fatalf("unable to set CPU limit: %v", err)
   178  		}
   179  
   180  		got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS", "GOMAXPROCS=3")
   181  		want := "3\n"
   182  		if got != want {
   183  			t.Fatalf("output got %q want %q", got, want)
   184  		}
   185  	})
   186  }
   187  
   188  // CPU affinity takes priority if lower than cgroup limit.
   189  func TestCgroupGOMAXPROCSSchedAffinity(t *testing.T) {
   190  	exe, err := buildTestProg(t, "testprog")
   191  	if err != nil {
   192  		t.Fatal(err)
   193  	}
   194  
   195  	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
   196  		if err := c.SetCPUMax(300000, 100000); err != nil {
   197  			t.Fatalf("unable to set CPU limit: %v", err)
   198  		}
   199  
   200  		// CPU affinity is actually a per-thread attribute.
   201  		runtime.LockOSThread()
   202  		defer runtime.UnlockOSThread()
   203  
   204  		const maxCPUs = 64 * 1024
   205  		var orig [maxCPUs / 8]byte
   206  		_, _, errno := syscall.Syscall6(syscall.SYS_SCHED_GETAFFINITY, 0, unsafe.Sizeof(orig), uintptr(unsafe.Pointer(&orig[0])), 0, 0, 0)
   207  		if errno != 0 {
   208  			t.Fatalf("unable to get CPU affinity: %v", errno)
   209  		}
   210  
   211  		// We're going to restrict to CPUs 0 and 1. Make sure those are already available.
   212  		if orig[0]&0b11 != 0b11 {
   213  			t.Skipf("skipping test: CPUs 0 and 1 not available")
   214  		}
   215  
   216  		var mask [maxCPUs / 8]byte
   217  		mask[0] = 0b11
   218  		_, _, errno = syscall.Syscall6(syscall.SYS_SCHED_SETAFFINITY, 0, unsafe.Sizeof(mask), uintptr(unsafe.Pointer(&mask[0])), 0, 0, 0)
   219  		if errno != 0 {
   220  			t.Fatalf("unable to set CPU affinity: %v", errno)
   221  		}
   222  		defer func() {
   223  			_, _, errno = syscall.Syscall6(syscall.SYS_SCHED_SETAFFINITY, 0, unsafe.Sizeof(orig), uintptr(unsafe.Pointer(&orig[0])), 0, 0, 0)
   224  			if errno != 0 {
   225  				t.Fatalf("unable to restore CPU affinity: %v", errno)
   226  			}
   227  		}()
   228  
   229  		got := runBuiltTestProg(t, exe, "PrintGOMAXPROCS")
   230  		want := "2\n"
   231  		if got != want {
   232  			t.Fatalf("output got %q want %q", got, want)
   233  		}
   234  	})
   235  }
   236  
   237  func TestCgroupGOMAXPROCSSetDefault(t *testing.T) {
   238  	mustHaveFourCPUs(t)
   239  
   240  	exe, err := buildTestProg(t, "testprog")
   241  	if err != nil {
   242  		t.Fatal(err)
   243  	}
   244  
   245  	tests := []struct {
   246  		godebug int
   247  		want    int
   248  	}{
   249  		// With containermaxprocs=1, SetDefaultGOMAXPROCS should observe
   250  		// the cgroup limit.
   251  		{
   252  			godebug: 1,
   253  			want:    3,
   254  		},
   255  		// With containermaxprocs=0, it should be ignored.
   256  		{
   257  			godebug: 0,
   258  			want:    runtime.NumCPU(),
   259  		},
   260  	}
   261  	for _, tc := range tests {
   262  		t.Run(fmt.Sprintf("containermaxprocs=%d", tc.godebug), func(t *testing.T) {
   263  			cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
   264  				env := []string{
   265  					fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()),
   266  					"GO_TEST_CPU_MAX_QUOTA=300000",
   267  					fmt.Sprintf("GODEBUG=containermaxprocs=%d", tc.godebug),
   268  				}
   269  				got := runBuiltTestProg(t, exe, "SetLimitThenDefaultGOMAXPROCS", env...)
   270  				want := fmt.Sprintf("%d\n", tc.want)
   271  				if got != want {
   272  					t.Fatalf("output got %q want %q", got, want)
   273  				}
   274  			})
   275  		})
   276  	}
   277  }
   278  
   279  func TestCgroupGOMAXPROCSUpdate(t *testing.T) {
   280  	mustHaveFourCPUs(t)
   281  
   282  	if testing.Short() {
   283  		t.Skip("skipping test: long sleeps")
   284  	}
   285  
   286  	exe, err := buildTestProg(t, "testprog")
   287  	if err != nil {
   288  		t.Fatal(err)
   289  	}
   290  
   291  	cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
   292  		got := runBuiltTestProg(t, exe, "UpdateGOMAXPROCS", fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()))
   293  		if !strings.Contains(got, "OK") {
   294  			t.Fatalf("output got %q want OK", got)
   295  		}
   296  	})
   297  }
   298  
   299  func TestCgroupGOMAXPROCSDontUpdate(t *testing.T) {
   300  	mustHaveFourCPUs(t)
   301  
   302  	if testing.Short() {
   303  		t.Skip("skipping test: long sleeps")
   304  	}
   305  
   306  	exe, err := buildTestProg(t, "testprog")
   307  	if err != nil {
   308  		t.Fatal(err)
   309  	}
   310  
   311  	// Two ways to disable updates: explicit GOMAXPROCS or GODEBUG for
   312  	// update feature.
   313  	for _, v := range []string{"GOMAXPROCS=4", "GODEBUG=updatemaxprocs=0"} {
   314  		t.Run(v, func(t *testing.T) {
   315  			cgrouptest.InCgroupV2(t, func(c *cgrouptest.CgroupV2) {
   316  				got := runBuiltTestProg(t, exe, "DontUpdateGOMAXPROCS",
   317  					fmt.Sprintf("GO_TEST_CPU_MAX_PATH=%s", c.CPUMaxPath()),
   318  					v)
   319  				if !strings.Contains(got, "OK") {
   320  					t.Fatalf("output got %q want OK", got)
   321  				}
   322  			})
   323  		})
   324  	}
   325  }
   326  

View as plain text