Files
nomad/client/lib/cgutil/cpuset_manager_linux_test.go
Seth Hoenig 87d54b8c21 client: change test to not poke cgroupv2 edge case
This PR tweaks the TestCpusetManager_AddAlloc unit test to not break
when being run on a machine using cgroupsv2. The behavior of writing
an empty cpuset.cpu changes in cgroupv2, where such a group now inherits
the value of its parent group, rather than remaining empty.

The test in question was written such that a task would consume all available
cores shared on an alloc, causing the empty set to be written to the shared
group, which works fine on cgroupsv1 but breaks on cgroupsv2. By adjusting
the test to consume only 1 core instead of all cores, it no longer triggers
that edge case.

The actual fix for the new cgroupsv2 behavior will be in #11933
2022-01-27 08:27:40 -06:00

180 lines
6.6 KiB
Go

package cgutil
import (
"io/ioutil"
"path/filepath"
"runtime"
"syscall"
"testing"
"github.com/hashicorp/nomad/lib/cpuset"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/stretchr/testify/require"
"github.com/hashicorp/nomad/helper/testlog"
)
func tmpCpusetManager(t *testing.T) (manager *cpusetManager, cleanup func()) {
if runtime.GOOS != "linux" || syscall.Geteuid() != 0 {
t.Skip("Test only available running as root on linux")
}
mount, err := FindCgroupMountpointDir()
if err != nil || mount == "" {
t.Skipf("Failed to find cgroup mount: %v %v", mount, err)
}
parent := "/gotest-" + uuid.Short()
require.NoError(t, cpusetEnsureParent(parent))
manager = &cpusetManager{
cgroupParent: parent,
cgroupInfo: map[string]allocTaskCgroupInfo{},
logger: testlog.HCLogger(t),
}
parentPath, err := getCgroupPathHelper("cpuset", parent)
require.NoError(t, err)
return manager, func() { require.NoError(t, cgroups.RemovePaths(map[string]string{"cpuset": parentPath})) }
}
func TestCpusetManager_Init(t *testing.T) {
manager, cleanup := tmpCpusetManager(t)
defer cleanup()
require.NoError(t, manager.Init())
require.DirExists(t, filepath.Join(manager.cgroupParentPath, SharedCpusetCgroupName))
require.FileExists(t, filepath.Join(manager.cgroupParentPath, SharedCpusetCgroupName, "cpuset.cpus"))
sharedCpusRaw, err := ioutil.ReadFile(filepath.Join(manager.cgroupParentPath, SharedCpusetCgroupName, "cpuset.cpus"))
require.NoError(t, err)
sharedCpus, err := cpuset.Parse(string(sharedCpusRaw))
require.NoError(t, err)
require.Exactly(t, manager.parentCpuset.ToSlice(), sharedCpus.ToSlice())
require.DirExists(t, filepath.Join(manager.cgroupParentPath, ReservedCpusetCgroupName))
}
func TestCpusetManager_AddAlloc_single(t *testing.T) {
manager, cleanup := tmpCpusetManager(t)
defer cleanup()
require.NoError(t, manager.Init())
alloc := mock.Alloc()
// reserve just one core (the 0th core, which probably exists)
alloc.AllocatedResources.Tasks["web"].Cpu.ReservedCores = cpuset.New(0).ToSlice()
manager.AddAlloc(alloc)
// force reconcile
manager.reconcileCpusets()
// check that the 0th core is no longer available in the shared group
// actual contents of shared group depends on machine core count
require.DirExists(t, filepath.Join(manager.cgroupParentPath, SharedCpusetCgroupName))
require.FileExists(t, filepath.Join(manager.cgroupParentPath, SharedCpusetCgroupName, "cpuset.cpus"))
sharedCpusRaw, err := ioutil.ReadFile(filepath.Join(manager.cgroupParentPath, SharedCpusetCgroupName, "cpuset.cpus"))
require.NoError(t, err)
sharedCpus, err := cpuset.Parse(string(sharedCpusRaw))
require.NoError(t, err)
require.NotEmpty(t, sharedCpus.ToSlice())
require.NotContains(t, sharedCpus.ToSlice(), uint16(0))
// check that the 0th core is allocated to reserved cgroup
require.DirExists(t, filepath.Join(manager.cgroupParentPath, ReservedCpusetCgroupName))
reservedCpusRaw, err := ioutil.ReadFile(filepath.Join(manager.cgroupParentPath, ReservedCpusetCgroupName, "cpuset.cpus"))
require.NoError(t, err)
reservedCpus, err := cpuset.Parse(string(reservedCpusRaw))
require.NoError(t, err)
require.Exactly(t, alloc.AllocatedResources.Tasks["web"].Cpu.ReservedCores, reservedCpus.ToSlice())
// check that task cgroup exists and cpuset matches expected reserved cores
allocInfo, ok := manager.cgroupInfo[alloc.ID]
require.True(t, ok)
require.Len(t, allocInfo, 1)
taskInfo, ok := allocInfo["web"]
require.True(t, ok)
require.DirExists(t, taskInfo.CgroupPath)
taskCpusRaw, err := ioutil.ReadFile(filepath.Join(taskInfo.CgroupPath, "cpuset.cpus"))
require.NoError(t, err)
taskCpus, err := cpuset.Parse(string(taskCpusRaw))
require.NoError(t, err)
require.Exactly(t, alloc.AllocatedResources.Tasks["web"].Cpu.ReservedCores, taskCpus.ToSlice())
}
func TestCpusetManager_AddAlloc_subset(t *testing.T) {
t.Skip("todo: add test for #11933")
}
func TestCpusetManager_AddAlloc_all(t *testing.T) {
// cgroupsv2 changes behavior of writing empty cpuset.cpu, which is what
// happens to the /shared group when one or more allocs consume all available
// cores.
t.Skip("todo: add test for #11933")
}
func TestCpusetManager_RemoveAlloc(t *testing.T) {
manager, cleanup := tmpCpusetManager(t)
defer cleanup()
require.NoError(t, manager.Init())
// this case tests adding 2 allocs, reconciling then removing 1 alloc
// it requires the system to have atleast 2 cpu cores (one for each alloc)
if manager.parentCpuset.Size() < 2 {
t.Skip("test requires atleast 2 cpu cores")
}
alloc1 := mock.Alloc()
alloc1Cpuset := cpuset.New(manager.parentCpuset.ToSlice()[0])
alloc1.AllocatedResources.Tasks["web"].Cpu.ReservedCores = alloc1Cpuset.ToSlice()
manager.AddAlloc(alloc1)
alloc2 := mock.Alloc()
alloc2Cpuset := cpuset.New(manager.parentCpuset.ToSlice()[1])
alloc2.AllocatedResources.Tasks["web"].Cpu.ReservedCores = alloc2Cpuset.ToSlice()
manager.AddAlloc(alloc2)
//force reconcile
manager.reconcileCpusets()
// shared cpuset should not include any expected cores
sharedCpusRaw, err := ioutil.ReadFile(filepath.Join(manager.cgroupParentPath, SharedCpusetCgroupName, "cpuset.cpus"))
require.NoError(t, err)
sharedCpus, err := cpuset.Parse(string(sharedCpusRaw))
require.NoError(t, err)
require.False(t, sharedCpus.ContainsAny(alloc1Cpuset.Union(alloc2Cpuset)))
// reserved cpuset should equal the expected cpus
reservedCpusRaw, err := ioutil.ReadFile(filepath.Join(manager.cgroupParentPath, ReservedCpusetCgroupName, "cpuset.cpus"))
require.NoError(t, err)
reservedCpus, err := cpuset.Parse(string(reservedCpusRaw))
require.NoError(t, err)
require.True(t, reservedCpus.Equals(alloc1Cpuset.Union(alloc2Cpuset)))
// remove first allocation
alloc1TaskPath := manager.cgroupInfo[alloc1.ID]["web"].CgroupPath
manager.RemoveAlloc(alloc1.ID)
manager.reconcileCpusets()
// alloc1's task reserved cgroup should be removed
require.NoDirExists(t, alloc1TaskPath)
// shared cpuset should now include alloc1's cores
sharedCpusRaw, err = ioutil.ReadFile(filepath.Join(manager.cgroupParentPath, SharedCpusetCgroupName, "cpuset.cpus"))
require.NoError(t, err)
sharedCpus, err = cpuset.Parse(string(sharedCpusRaw))
require.NoError(t, err)
require.False(t, sharedCpus.ContainsAny(alloc2Cpuset))
require.True(t, sharedCpus.IsSupersetOf(alloc1Cpuset))
// reserved cpuset should only include alloc2's cores
reservedCpusRaw, err = ioutil.ReadFile(filepath.Join(manager.cgroupParentPath, ReservedCpusetCgroupName, "cpuset.cpus"))
require.NoError(t, err)
reservedCpus, err = cpuset.Parse(string(reservedCpusRaw))
require.NoError(t, err)
require.True(t, reservedCpus.Equals(alloc2Cpuset))
}