| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- //go:build linux && !android && !e2e_testing
- package util
- import (
- "fmt"
- "os"
- "os/exec"
- "runtime"
- "strconv"
- "sync"
- "testing"
- "unsafe"
- "github.com/stretchr/testify/require"
- "golang.org/x/sys/unix"
- )
- // TestAvoidCPUs changes the affinity of the whole process, so it runs in a child, leaving the test runner untouched.
- func TestAvoidCPUs(t *testing.T) {
- if os.Getenv("NEBULA_AVOID_CPU_TEST") != "1" {
- exe, err := os.Executable()
- require.NoError(t, err)
- cmd := exec.Command(exe, "-test.run=^TestAvoidCPUs$", "-test.v")
- cmd.Env = append(os.Environ(), "NEBULA_AVOID_CPU_TEST=1")
- out, err := cmd.CombinedOutput()
- require.NoError(t, err, "%s", out)
- return
- }
- allowed, err := AllowedCPUs()
- require.NoError(t, err)
- if len(allowed) < 2 {
- t.Skip("requires at least two allowed CPUs")
- }
- cpu := allowed[0]
- release := make(chan struct{})
- var wg sync.WaitGroup
- // lockThreads occupies 8 OS threads until release, so the runtime has to start more
- lockThreads := func() {
- var ready sync.WaitGroup
- for range 8 {
- ready.Add(1)
- wg.Go(func() {
- runtime.LockOSThread()
- ready.Done()
- <-release
- })
- }
- ready.Wait()
- }
- lockThreads()
- require.NoError(t, AvoidCPUs([]int{cpu}, false))
- after, err := AllowedCPUs()
- require.NoError(t, err)
- require.Equal(t, allowed, after, "configuration check must not mutate affinity")
- require.Error(t, AvoidCPUs(allowed, true), "must reject an empty mask before applying")
- require.NoError(t, AvoidCPUs([]int{cpu, cpu}, true))
- requireAvoided(t, cpu, allowed)
- // Existing threads stay locked while new ones inherit the restriction
- lockThreads()
- requireAvoided(t, cpu, allowed)
- close(release)
- wg.Wait()
- }
- // requireAvoided checks no thread may run on cpu, and every thread still may on some CPU of allowed only.
- func requireAvoided(t *testing.T, cpu int, allowed []int) {
- t.Helper()
- entries, err := os.ReadDir("/proc/self/task")
- require.NoError(t, err)
- for _, entry := range entries {
- tid, err := strconv.Atoi(entry.Name())
- require.NoError(t, err)
- var mask unix.CPUSet
- err = unix.SchedGetaffinity(tid, &mask)
- if err == unix.ESRCH {
- continue
- }
- require.NoError(t, err)
- require.False(t, mask.IsSet(cpu), "thread %d", tid)
- require.Positive(t, mask.Count())
- for id := range len(mask) * 64 {
- if mask.IsSet(id) {
- require.Contains(t, allowed, id, "must not widen the inherited mask")
- }
- }
- }
- }
- // TestAvoidCPUsRange checks the bounds of unix.CPUSet, whose words are 32 bits wide on 32-bit platforms.
- func TestAvoidCPUsRange(t *testing.T) {
- capacity := int(unsafe.Sizeof(unix.CPUSet{})) * 8
- require.NoError(t, AvoidCPUs([]int{capacity - 1}, false))
- require.EqualError(t, AvoidCPUs([]int{capacity}, false), fmt.Sprintf("CPU %d is out of range", capacity))
- require.EqualError(t, AvoidCPUs([]int{-1}, false), "CPU -1 is out of range")
- }
|