blob: 95cfa060036071d4411d8a61ba8f4a80ba8c183d [file] [log] [blame]
// 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();
}
}