| // Copyright 2022 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.devtools.build.lib.sandbox; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.io.Files; |
| import com.google.devtools.build.lib.sandbox.CgroupsInfo.InvalidCgroupsInfo; |
| import com.google.devtools.build.lib.vfs.util.FsApparatus; |
| import java.io.File; |
| import java.io.IOException; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** Tests for {@link CgroupsInfo}, {@link CgroupsInfoV1}, {@link CgroupsInfoV2}. */ |
| @RunWith(JUnit4.class) |
| public class CgroupsInfoTest { |
| private final FsApparatus scratch = FsApparatus.newNative(); |
| |
| /** We use this pseudo-root to get around not being able to replace absolute paths. */ |
| private String root; |
| |
| @Before |
| public void setUp() throws IOException { |
| root = scratch.dir("fake_root").getPathString(); |
| } |
| |
| @Test |
| public void testGetRootCgroup_v1() throws IOException { |
| String pathString = |
| createFakeAbsoluteFile( |
| "/proc/self/mounts", |
| "sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0", |
| "cgroup /dev/cgroup/cpu cgroup rw,cpu,cpuacct 0 0", |
| "cgroup /dev/cgroup/io cgroup rw,io 0 0", |
| "cgroup /dev/cgroup/job cgroup rw,job 0 0", |
| "cgroup /dev/cgroup/memory cgroup rw,memory,hugetlb 0 0", |
| "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0"); |
| |
| CgroupsInfo cgroup = CgroupsInfo.getRootCgroup(new File(pathString)); |
| |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.ROOT); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V1); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo("/dev/cgroup/memory"); |
| } |
| |
| @Test |
| public void testGetRootCgroup_v2() throws IOException { |
| String pathString = |
| createFakeAbsoluteFile( |
| "/proc/self/mounts", |
| "sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0", |
| "cgroup2 /sys/fs/cgroup cgroup2 rw,memory_recursiveprot 0 0", |
| "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0"); |
| |
| CgroupsInfo cgroup = CgroupsInfo.getRootCgroup(new File(pathString)); |
| |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.ROOT); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V2); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo("/sys/fs/cgroup"); |
| } |
| |
| @Test |
| public void testGetRootCgroup_mixed_v1_has_memory() throws IOException { |
| String pathString = |
| createFakeAbsoluteFile( |
| "/proc/self/mounts", |
| "sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0", |
| "cgroup2 /sys/fs/cgroup cgroup2 rw,memory_recursiveprot 0 0", |
| "cgroup /dev/cgroup/job cgroup rw,job 0 0", |
| "cgroup /dev/cgroup/memory cgroup rw,memory,hugetlb 0 0", |
| "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0"); |
| |
| CgroupsInfo cgroup = CgroupsInfo.getRootCgroup(new File(pathString)); |
| |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.ROOT); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V1); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo("/dev/cgroup/memory"); |
| } |
| |
| @Test |
| public void testGetRootCgroup_mixed_v2_has_memory() throws IOException { |
| String pathString = |
| createFakeAbsoluteFile( |
| "/proc/self/mount", |
| "sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0", |
| "cgroup2 /sys/fs/cgroup cgroup2 rw,memory_recursiveprot 0 0", |
| "cgroup /dev/cgroup/job cgroup rw,job 0 0", |
| "cgroup /dev/cgroup/io cgroup rw,io 0 0", |
| "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0"); |
| |
| CgroupsInfo cgroup = CgroupsInfo.getRootCgroup(new File(pathString)); |
| |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.ROOT); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V2); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo("/sys/fs/cgroup"); |
| } |
| |
| @Test |
| public void testCreateBlazeSpawnsCgroup_v1() throws IOException { |
| String mountPath = root + "/dev/cgroup/memory"; |
| CgroupsInfo rootCgroup = |
| new CgroupsInfoV1(CgroupsInfo.Type.ROOT, /* cgroupDir= */ new File(mountPath)); |
| String procSelfCgroupPath = |
| createFakeAbsoluteFile( |
| "/proc/self/cgroup", |
| "8:net:/netdir/action-6", |
| "7:memory,hugetlb:/memdir/action-6", |
| "6:job:/jobdir/action-16", |
| "5:io:/iodir/action-1"); |
| scratch.dir(root + "/dev/cgroup/memory/memdir/action-6").createDirectoryAndParents(); |
| String blazeSpawnsPath = |
| root |
| + "/dev/cgroup/memory/memdir/action-6/blaze_" |
| + ProcessHandle.current().pid() |
| + "_spawns"; |
| scratch.dir(blazeSpawnsPath).createDirectoryAndParents(); |
| |
| CgroupsInfo cgroup = rootCgroup.createBlazeSpawnsCgroup(procSelfCgroupPath); |
| |
| assertThat(cgroup.getCgroupDir().exists()).isTrue(); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo(blazeSpawnsPath); |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.BLAZE_SPAWNS); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V1); |
| } |
| |
| @Test |
| public void testCreateBlazeSpawnsCgroup_v2() throws IOException { |
| String mountPath = root + "/sys/fs/cgroup"; |
| CgroupsInfo rootCgroup = |
| new CgroupsInfoV2(CgroupsInfo.Type.ROOT, /* cgroupDir= */ new File(mountPath)); |
| String procSelfCgroupPath = |
| createFakeAbsoluteFile("/proc/self/cgroup", "0::/user.slice/session.scope"); |
| // In v2, the blaze spawns cgroup is created one step up from where the blaze process is defined |
| // in the /proc/self/cgroup file (defined above). Specifically, here it is in the |
| // ".../user.slice". |
| String blazeSpawnsPath = |
| mountPath + "/user.slice/blaze_" + ProcessHandle.current().pid() + "_spawns.slice"; |
| // Even though the blaze spawn's cgroup directory is meant to be created in the method call, |
| // we create it here so that we can prepare the controller files that are expected beforehand. |
| scratch.dir(blazeSpawnsPath).createDirectoryAndParents(); |
| // Since this controllers file is missing `pids`, we expect that to be written to it. |
| scratch.file(blazeSpawnsPath + "/cgroup.controllers", "memory pids"); |
| scratch.file(blazeSpawnsPath + "/cgroup.subtree_control", "memory"); |
| |
| CgroupsInfo cgroup = rootCgroup.createBlazeSpawnsCgroup(procSelfCgroupPath); |
| |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V2); |
| assertThat(cgroup.getCgroupDir().exists()).isTrue(); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo(blazeSpawnsPath); |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.BLAZE_SPAWNS); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V2); |
| // This is not what an actual cgroups v2 file would contain, but it's what we expect to write to |
| // it to enable subtree control. |
| assertThat(Files.readLines(new File(blazeSpawnsPath + "/cgroup.subtree_control"), UTF_8)) |
| .containsExactly("+memory +pids"); |
| } |
| |
| @Test |
| public void testCreateIndividualSpawnCgroup_withLimit_v1() throws IOException { |
| String blazeSpawnsPath = root + "/dev/cgroup/memory/memdir/action-6/blaze_1234_spawns"; |
| scratch.dir(blazeSpawnsPath).createDirectoryAndParents(); |
| CgroupsInfo blazeSpawnsCgroup = |
| new CgroupsInfoV1(CgroupsInfo.Type.BLAZE_SPAWNS, new File(blazeSpawnsPath)); |
| |
| CgroupsInfo cgroup = blazeSpawnsCgroup.createIndividualSpawnCgroup("spawn_1", 100); |
| |
| assertThat(cgroup.exists()).isTrue(); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo(blazeSpawnsPath + "/spawn_1"); |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.SPAWN); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V1); |
| assertThat(Files.readLines(new File(blazeSpawnsPath + "/spawn_1/memory.limit_in_bytes"), UTF_8)) |
| .containsExactly("104857600"); |
| } |
| |
| @Test |
| public void testCreateIndividualSpawnCgroup_noLimit_v1() throws IOException { |
| String blazeSpawnsPath = root + "/dev/cgroup/memory/memdir/action-6/blaze_1234_spawns"; |
| scratch.dir(blazeSpawnsPath).createDirectoryAndParents(); |
| CgroupsInfo blazeSpawnsCgroup = |
| new CgroupsInfoV1(CgroupsInfo.Type.BLAZE_SPAWNS, new File(blazeSpawnsPath)); |
| |
| CgroupsInfo cgroup = blazeSpawnsCgroup.createIndividualSpawnCgroup("spawn_1", 0); |
| |
| assertThat(cgroup.exists()).isTrue(); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo(blazeSpawnsPath + "/spawn_1"); |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.SPAWN); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V1); |
| // In reality, cgroups should still create this file automatically, but since we don't have |
| // that in our tests, the memory limits file should not have been created since it isn't written |
| // to. |
| assertThat(new File(blazeSpawnsPath + "/spawn_1/memory.limit_in_bytes").exists()).isFalse(); |
| } |
| |
| @Test |
| public void testCreateIndividualSpawnCgroup_withLimit_v2() throws IOException { |
| String blazeSpawnsPath = root + "/sys/fs/cgroup/user.slice/blaze_1234_spawns.slice"; |
| scratch.dir(blazeSpawnsPath).createDirectoryAndParents(); |
| CgroupsInfo blazeSpawnsCgroup = |
| new CgroupsInfoV2(CgroupsInfo.Type.BLAZE_SPAWNS, new File(blazeSpawnsPath)); |
| |
| CgroupsInfo cgroup = blazeSpawnsCgroup.createIndividualSpawnCgroup("spawn_1", 100); |
| |
| assertThat(cgroup.exists()).isTrue(); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo(blazeSpawnsPath + "/spawn_1.scope"); |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.SPAWN); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V2); |
| assertThat( |
| Files.readLines(new File(blazeSpawnsPath + "/spawn_1.scope/memory.oom.group"), UTF_8)) |
| .containsExactly("1"); |
| assertThat(Files.readLines(new File(blazeSpawnsPath + "/spawn_1.scope/memory.max"), UTF_8)) |
| .containsExactly("104857600"); |
| } |
| |
| @Test |
| public void testCreateIndividualSpawnCgroup_noLimit_v2() throws IOException { |
| String blazeSpawnsPath = root + "/sys/fs/cgroup/user.slice/blaze_1234_spawns.slice"; |
| scratch.dir(blazeSpawnsPath).createDirectoryAndParents(); |
| CgroupsInfo blazeSpawnsCgroup = |
| new CgroupsInfoV2(CgroupsInfo.Type.BLAZE_SPAWNS, new File(blazeSpawnsPath)); |
| |
| CgroupsInfo cgroup = blazeSpawnsCgroup.createIndividualSpawnCgroup("spawn_1", 0); |
| |
| assertThat(cgroup.exists()).isTrue(); |
| assertThat(cgroup.getCgroupDir().getPath()).isEqualTo(blazeSpawnsPath + "/spawn_1.scope"); |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.SPAWN); |
| assertThat(cgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V2); |
| // In reality, cgroups should still create this file automatically, but since we don't have |
| // that in our tests, the memory limits files should not have been created since they aren't |
| // written to. |
| assertThat(new File(blazeSpawnsPath + "/spawn_1.scope/memory.oom.group").exists()).isFalse(); |
| assertThat(new File(blazeSpawnsPath + "/spawn_1.scope/memory.max").exists()).isFalse(); |
| } |
| |
| @Test |
| public void testGetMemoryUsageInKb_v1() throws IOException { |
| String cgroupPath = root + "/dev/cgroup/memory/memdir/action-1"; |
| scratch.dir(cgroupPath).createDirectoryAndParents(); |
| CgroupsInfo cgroupsInfo = |
| new CgroupsInfoV1(CgroupsInfo.Type.SPAWN, /* cgroupDir= */ new File(cgroupPath)); |
| |
| // It should return 0 if the /path/to/cgroup/memory.usage_in_bytes does not exist. |
| assertThat(cgroupsInfo.getMemoryUsageInKb()).isEqualTo(0); |
| |
| scratch.file(cgroupPath + "/memory.usage_in_bytes", "1024000"); |
| |
| assertThat(cgroupsInfo.getMemoryUsageInKb()).isEqualTo(1000); |
| } |
| |
| @Test |
| public void testGetMemoryUsageInKb_v2() throws IOException { |
| String cgroupPath = root + "/sys/fs/cgroup/memdir/action-1"; |
| scratch.dir(cgroupPath).createDirectoryAndParents(); |
| CgroupsInfo cgroupsInfo = |
| new CgroupsInfoV2(CgroupsInfo.Type.SPAWN, /* cgroupDir= */ new File(cgroupPath)); |
| |
| // It should return 0 if the /path/to/cgroup/memory.current does not exist. |
| assertThat(cgroupsInfo.getMemoryUsageInKb()).isEqualTo(0); |
| |
| scratch.file(cgroupPath + "/memory.current", "1024000"); |
| // Divided by 1024. |
| |
| assertThat(cgroupsInfo.getMemoryUsageInKb()).isEqualTo(1000); |
| } |
| |
| @Test |
| public void testAddProcess_v1() throws IOException { |
| String cgroupPath = root + "/dev/cgroup/memory/memdir/action-1"; |
| scratch.dir(cgroupPath).createDirectoryAndParents(); |
| CgroupsInfo cgroupsInfo = |
| new CgroupsInfoV1(CgroupsInfo.Type.SPAWN, /* cgroupDir= */ new File(cgroupPath)); |
| |
| cgroupsInfo.addProcess(1234); |
| |
| assertThat(Files.readLines(new File(cgroupsInfo.getCgroupDir(), "cgroup.procs"), UTF_8)) |
| .containsExactly("1234"); |
| } |
| |
| @Test |
| public void testAddProcess_v2() throws IOException { |
| String cgroupPath = root + "/sys/fs/cgroup/memdir/action-1"; |
| scratch.dir(cgroupPath).createDirectoryAndParents(); |
| CgroupsInfo cgroupsInfo = |
| new CgroupsInfoV2(CgroupsInfo.Type.SPAWN, /* cgroupDir= */ new File(cgroupPath)); |
| |
| cgroupsInfo.addProcess(1234); |
| |
| assertThat(Files.readLines(new File(cgroupsInfo.getCgroupDir(), "cgroup.procs"), UTF_8)) |
| .containsExactly("1234"); |
| } |
| |
| @Test |
| public void testGetRootCgroup_returnsInvalidCgroup_whenMountNotFound() throws IOException { |
| String pathString = createFakeAbsoluteFile("/proc/self/mounts", ""); |
| |
| CgroupsInfo cgroup = CgroupsInfo.getRootCgroup(new File(pathString)); |
| |
| assertThat(cgroup.getClass()).isEqualTo(InvalidCgroupsInfo.class); |
| assertThat(cgroup.getType()).isEqualTo(CgroupsInfo.Type.ROOT); |
| } |
| |
| @Test |
| public void testCreateCgroupFromInvalidCgroup_returnsInvalidCgroup() { |
| String errorMessage = "Some error message"; |
| CgroupsInfo invalidRootCgroup = |
| new InvalidCgroupsInfo(CgroupsInfo.Type.ROOT, CgroupsInfo.Version.V1, errorMessage); |
| CgroupsInfo invalidBlazeSpawnsCgroup = |
| new InvalidCgroupsInfo(CgroupsInfo.Type.BLAZE_SPAWNS, CgroupsInfo.Version.V2, errorMessage); |
| |
| CgroupsInfo createdBlazeSpawnsCgroup = invalidRootCgroup.createBlazeSpawnsCgroup(""); |
| CgroupsInfo createdSpawnCgroup = |
| invalidBlazeSpawnsCgroup.createIndividualSpawnCgroup("spawn_1", 1); |
| |
| assertThat(createdBlazeSpawnsCgroup.getClass()).isEqualTo(InvalidCgroupsInfo.class); |
| // Should still have the same version as the parent cgroup that attempted to create it. |
| assertThat(createdBlazeSpawnsCgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V1); |
| assertThat(createdBlazeSpawnsCgroup.getType()).isEqualTo(CgroupsInfo.Type.BLAZE_SPAWNS); |
| |
| assertThat(createdSpawnCgroup.getClass()).isEqualTo(InvalidCgroupsInfo.class); |
| // Should still have the same version as the parent cgroup that attempted to create it. |
| assertThat(createdSpawnCgroup.getVersion()).isEqualTo(CgroupsInfo.Version.V2); |
| assertThat(createdSpawnCgroup.getType()).isEqualTo(CgroupsInfo.Type.SPAWN); |
| } |
| |
| private String createFakeAbsoluteFile(String fileName, String... contents) throws IOException { |
| return scratch.file(root + fileName, contents).getPathString(); |
| } |
| } |