summaryrefslogtreecommitdiffstats
path: root/node-admin
diff options
context:
space:
mode:
authorHÃ¥kon Hallingstad <hakon.hallingstad@gmail.com>2023-05-02 12:20:09 +0200
committerGitHub <noreply@github.com>2023-05-02 12:20:09 +0200
commite23486dfb4adaf88242053c9ce27acdc2beeb6ff (patch)
tree3f89386a4051d9e8c18405593f17410c024e295f /node-admin
parentb83cc57a23c24cba060e884ade5f056cd46c5a82 (diff)
parent531758c14b24543e605f45053ef92724ef79da43 (diff)
Merge pull request #26866 from vespa-engine/hakonhall/improve-cgroup-modeling
Improve cgroup modeling
Diffstat (limited to 'node-admin')
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/Cgroup.java163
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/CgroupCore.java34
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/CpuController.java111
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/MemoryController.java48
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/Size.java67
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/CGroupV2.java188
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperations.java3
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollector.java39
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java24
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/cgroup/CgroupTest.java (renamed from node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/CGroupV2Test.java)68
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperationsTest.java9
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollectorTest.java28
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java4
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPathTest.java20
14 files changed, 546 insertions, 260 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/Cgroup.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/Cgroup.java
new file mode 100644
index 00000000000..e40e3c6c003
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/Cgroup.java
@@ -0,0 +1,163 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.cgroup;
+
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
+import com.yahoo.vespa.hosted.node.admin.container.ContainerId;
+import com.yahoo.vespa.hosted.node.admin.task.util.file.UnixPath;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+/**
+ * Represents a cgroup in the control group v2 hierarchy, see
+ * <a href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html">Control Group v2</a>.
+ *
+ * @author hakonhall
+ */
+public class Cgroup {
+ private static final Logger logger = Logger.getLogger(Cgroup.class.getName());
+
+ private static Map<String, Consumer<UnixPath>> cgroupDirectoryCallbacks = new HashMap<>();
+
+ private final Path root;
+ private final Path relativePath;
+
+ public static Cgroup root(FileSystem fileSystem) {
+ return new Cgroup(fileSystem.getPath("/sys/fs/cgroup"), fileSystem.getPath(""));
+ }
+
+ private Cgroup(Path root, Path relativePath) {
+ this.root = root.normalize();
+ this.relativePath = this.root.relativize(this.root.resolve(relativePath).normalize());
+ if (this.relativePath.toString().equals("..") || this.relativePath.toString().startsWith("../")) {
+ throw new IllegalArgumentException("Invalid cgroup relative path: " + relativePath);
+ }
+ }
+
+ /** Whether this cgroup actually exists in the kernel / on the file system. */
+ public boolean exists() { return unixPath().resolve("cgroup.controllers").exists(); }
+
+ /** Creates this cgroup if it does not already exist, and return this. */
+ public Cgroup create() {
+ if (unixPath().createDirectory()) {
+ // cgroup automatically creates various files in a newly created cgroup directory. A unit test may simulate
+ // this by registering consumers before the test is run.
+ Consumer<UnixPath> callback = cgroupDirectoryCallbacks.get(relativePath.toString());
+ if (callback != null)
+ callback.accept(unixPath());
+ }
+ return this;
+ }
+
+ /** Whether v2 cgroup is enabled on this host. */
+ public boolean v2CgroupIsEnabled() { return resolveRoot().exists(); }
+
+ /**
+ * Resolve the given path against the path of this cgroup, and return the resulting cgroup.
+ * If the given path is absolute, it is resolved against the root of the cgroup hierarchy.
+ */
+ public Cgroup resolve(String path) {
+ Path effectivePath = fileSystem().getPath(path);
+ if (effectivePath.isAbsolute()) {
+ return new Cgroup(root, fileSystem().getPath("/").relativize(effectivePath));
+ } else {
+ return new Cgroup(root, relativePath.resolve(path));
+ }
+ }
+
+ /** Returns the root cgroup, possibly this. */
+ public Cgroup resolveRoot() { return isRoot() ? this : new Cgroup(root, fileSystem().getPath("")); }
+
+ /** Returns the cgroup of a system service assuming this is the root, e.g. vespa-host-admin -> system.slice/vespa-host-admin.service. */
+ public Cgroup resolveSystemService(String name) { return resolve("system.slice").resolve(serviceNameOf(name)); }
+
+ /** Returns the root cgroup of the given Podman container. */
+ public Cgroup resolveContainer(ContainerId containerId) { return resolve("/machine.slice/libpod-" + containerId + ".scope/container"); }
+
+ /** Returns the root cgroup of the container, or otherwise the root cgroup. */
+ public Cgroup resolveRoot(Optional<ContainerId> containerId) { return containerId.map(this::resolveContainer).orElseGet(this::resolveRoot); }
+
+ /** Returns the absolute path to this cgroup. */
+ public Path path() { return root.resolve(relativePath); }
+
+ /** Returns the UnixPath of {@link #path()}. */
+ public UnixPath unixPath() { return new UnixPath(path()); }
+
+ public String read(String filename) {
+ return unixPath().resolve(filename).readUtf8File();
+ }
+
+ public Optional<String> readIfExists(String filename) {
+ return unixPath().resolve(filename).readUtf8FileIfExists().map(String::strip);
+ }
+
+ public List<String> readLines(String filename) {
+ return unixPath().resolve(filename).readUtf8File().lines().toList();
+ }
+
+ public Optional<Integer> readIntIfExists(String filename) {
+ return unixPath().resolve(filename).readUtf8FileIfExists().map(String::strip).map(Integer::parseInt);
+ }
+
+ public Size readSize(String filename) { return Size.from(read(filename).stripTrailing()); }
+
+ public boolean convergeFileContent(TaskContext context, String filename, String content, boolean apply) {
+ UnixPath path = unixPath().resolve(filename);
+ String currentContent = path.readUtf8File();
+ if (ensureSuffixNewline(currentContent).equals(ensureSuffixNewline(content))) return false;
+
+ if (apply) {
+ context.recordSystemModification(logger, "Updating " + path + " from '" + currentContent.stripTrailing() +
+ "' to '" + content.stripTrailing() + "'");
+ path.writeUtf8File(content);
+ }
+ return true;
+ }
+
+ /** The kernel appears to append a newline if none exist, when writing to files in cgroupfs. */
+ private static String ensureSuffixNewline(String content) {
+ return content.endsWith("\n") ? content : content + "\n";
+ }
+
+ /** Returns an instance representing core interface files (cgroup.* files). */
+ public CgroupCore core() { return new CgroupCore(this); }
+
+ /** Returns the CPU controller of this cgroup (cpu.* files). */
+ public CpuController cpu() { return new CpuController(this); }
+
+ /** Returns the memory controller of this cgroup (memory.* files). */
+ public MemoryController memory() { return new MemoryController(this); }
+
+ /**
+ * Wraps {@code command} to ensure it is executed in this cgroup.
+ *
+ * <p>WARNING: This method must be called only after vespa-cgexec has been installed.</p>
+ */
+ public String[] wrapCommandForExecutionInCgroup(String... command) {
+ String[] fullCommand = new String[3 + command.length];
+ fullCommand[0] = Defaults.getDefaults().vespaHome() + "/bin/vespa-cgexec";
+ fullCommand[1] = "-g";
+ fullCommand[2] = relativePath.toString();
+ System.arraycopy(command, 0, fullCommand, 3, command.length);
+ return fullCommand;
+ }
+
+ public static void unitTesting_atCgroupCreation(String relativePath, Consumer<UnixPath> callback) {
+ cgroupDirectoryCallbacks.put(relativePath, callback);
+ }
+
+ private boolean isRoot() { return relativePath.toString().isEmpty(); }
+
+ private static String serviceNameOf(String name) {
+ return name.indexOf('.') == -1 ? name + ".service" : name;
+ }
+
+ private FileSystem fileSystem() { return root.getFileSystem(); }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/CgroupCore.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/CgroupCore.java
new file mode 100644
index 00000000000..68f27549e1b
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/CgroupCore.java
@@ -0,0 +1,34 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.cgroup;
+
+import java.util.List;
+
+/**
+ * Utility methods for accessing the cgroup core interface files, i.e. all cgroup.* files.
+ *
+ * @author hakonhall
+ */
+public class CgroupCore {
+ private final Cgroup cgroup;
+
+ CgroupCore(Cgroup cgroup) { this.cgroup = cgroup; }
+
+ public List<Integer> getPidsInCgroup() {
+ return cgroup.readLines("cgroup.procs")
+ .stream()
+ .map(Integer::parseInt)
+ .toList();
+ }
+
+ /** Whether the given PID is a member of this cgroup. */
+ public boolean isMember(int pid) {
+ return getPidsInCgroup().contains(pid);
+ }
+
+ /** Move the given PID to this cgroup, but return false if it was already a member. */
+ public boolean addMember(int pid) {
+ if (isMember(pid)) return false;
+ cgroup.unixPath().resolve("cgroup.procs").writeUtf8File(Integer.toString(pid));
+ return true;
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/CpuController.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/CpuController.java
new file mode 100644
index 00000000000..c273a6237b4
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/CpuController.java
@@ -0,0 +1,111 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.cgroup;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static java.lang.Integer.parseInt;
+
+/**
+ * Represents a cgroup v2 CPU controller, i.e. all cpu.* files.
+ *
+ * @author hakonhall
+ */
+public class CpuController {
+ private final Cgroup cgroup;
+
+ CpuController(Cgroup cgroup) {
+ this.cgroup = cgroup;
+ }
+
+ /**
+ * The maximum bandwidth limit of the format "QUOTA PERIOD", which indicates that the cgroup may consume
+ * up to QUOTA in each PERIOD duration. A quota of "max" indicates no limit.
+ */
+ public record Max(Size quota, int period) {
+ public String toFileContent() { return quota + " " + period + '\n'; }
+ }
+
+ /**
+ * Returns the maximum CPU usage, or empty if cgroup is not found.
+ *
+ * @see Max
+ */
+ public Optional<Max> readMax() {
+ return cgroup.readIfExists("cpu.max")
+ .map(content -> {
+ String[] parts = content.strip().split(" ");
+ return new Max(Size.from(parts[0]), parseInt(parts[1]));
+ });
+ }
+
+ /**
+ * Update CPU quota and period for the given container ID. Set quota to -1 value for unlimited.
+ *
+ * @see #readMax()
+ * @see Max
+ */
+ public boolean updateMax(TaskContext context, int quota, int period) {
+ Max max = new Max(quota < 0 ? Size.max() : Size.from(quota), period);
+ return cgroup.convergeFileContent(context, "cpu.max", max.toFileContent(), true);
+ }
+
+ /** @return The weight in the range [1, 10000], or empty if not found. */
+ private Optional<Integer> readWeight() {
+ return cgroup.readIntIfExists("cpu.weight");
+ }
+
+ /** @return The number of shares allocated to this cgroup for purposes of CPU time scheduling, or empty if not found. */
+ public Optional<Integer> readShares() {
+ return readWeight().map(CpuController::weightToShares);
+ }
+
+ public boolean updateShares(TaskContext context, int shares) {
+ return cgroup.convergeFileContent(context, "cpu.weight", sharesToWeight(shares) + "\n", true);
+ }
+
+ // Must be same as in crun: https://github.com/containers/crun/blob/72c6e60ade0e4716fe2d8353f0d97d72cc8d1510/src/libcrun/cgroup.c#L3061
+ // TODO: Migrate to weights
+ public static int sharesToWeight(int shares) { return (int) (1 + ((shares - 2L) * 9999) / 262142); }
+ public static int weightToShares(int weight) { return (int) (2 + ((weight - 1L) * 262142) / 9999); }
+
+ public enum StatField {
+ TOTAL_USAGE_USEC("usage_usec"),
+ USER_USAGE_USEC("user_usec"),
+ SYSTEM_USAGE_USEC("system_usec"),
+ TOTAL_PERIODS("nr_periods"),
+ THROTTLED_PERIODS("nr_throttled"),
+ THROTTLED_TIME_USEC("throttled_usec");
+
+ private final String name;
+
+ StatField(String name) {
+ this.name = name;
+ }
+
+ long parseValue(String value) {
+ return Long.parseLong(value);
+ }
+
+ static Optional<StatField> fromField(String fieldName) {
+ return Arrays.stream(values())
+ .filter(field -> fieldName.equals(field.name))
+ .findFirst();
+ }
+ }
+
+ public Map<StatField, Long> readStats() {
+ return cgroup.readLines("cpu.stat")
+ .stream()
+ .map(line -> line.split("\\s+"))
+ .filter(parts -> parts.length == 2)
+ .flatMap(parts -> StatField.fromField(parts[0]).stream().map(field -> new Pair<>(field, field.parseValue(parts[1]))))
+ .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
+ }
+
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/MemoryController.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/MemoryController.java
new file mode 100644
index 00000000000..840cd025917
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/MemoryController.java
@@ -0,0 +1,48 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.cgroup;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Represents a cgroup v2 memory controller, i.e. all memory.* files.
+ *
+ * @author hakonhall
+ */
+public class MemoryController {
+ private final Cgroup cgroup;
+
+ MemoryController(Cgroup cgroup) {
+ this.cgroup = cgroup;
+ }
+
+ /** @return Maximum amount of memory that can be used by the cgroup and its descendants. */
+ public Size readMax() {
+ return cgroup.readSize("memory.max");
+ }
+
+ /** @return The total amount of memory currently being used by the cgroup and its descendants, in bytes. */
+ public Size readCurrent() {
+ return cgroup.readSize("memory.current");
+ }
+
+ /** @return The total amount of memory currently being used by the cgroup and its descendants, in bytes. */
+ public Optional<Size> readCurrentIfExists() {
+ return cgroup.readIfExists("memory.current").map(Size::from);
+ }
+
+ /** @return Number of bytes used to cache filesystem data, including tmpfs and shared memory. */
+ public Size readFileSystemCache() {
+ return Size.from(readField(cgroup.readLines("memory.stat"), "file"));
+ }
+
+ private static String readField(List<String> lines, String fieldName) {
+ return lines.stream()
+ .map(line -> line.split("\\s+"))
+ .filter(fields -> fields.length == 2)
+ .filter(fields -> fieldName.equals(fields[0]))
+ .map(fields -> fields[1])
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("No such field: " + fieldName));
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/Size.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/Size.java
new file mode 100644
index 00000000000..5e6ca7de8bd
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/cgroup/Size.java
@@ -0,0 +1,67 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.cgroup;
+
+import java.util.Objects;
+
+/**
+ * Represents a number of bytes or possibly "max".
+ *
+ * @author hakonhall
+ */
+public class Size {
+ private static final String MAX = "max";
+
+ private final boolean max;
+ private final long value;
+
+ public static Size max() {
+ return new Size(true, 0);
+ }
+
+ public static Size from(long value) {
+ return new Size(false, value);
+ }
+
+ public static Size from(String value) {
+ return value.equals(MAX) ? new Size(true, 0) : new Size(false, Long.parseLong(value));
+ }
+
+ private Size(boolean max, long value) {
+ this.max = max;
+ this.value = value;
+ }
+
+ public boolean isMax() {
+ return max;
+ }
+
+ /** Returns the value, i.e. the number of "bytes" if applicable. Throws if this is max. */
+ public long value() {
+ if (max) throw new IllegalStateException("Value is max");
+ return value;
+ }
+
+ public String toFileContent() { return toString() + '\n'; }
+
+ @Override
+ public String toString() { return max ? MAX : Long.toString(value); }
+
+ public boolean isGreaterThan(Size that) {
+ if (that.max) return false;
+ if (this.max) return true;
+ return this.value > that.value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Size size = (Size) o;
+ return max == size.max && value == size.value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(max, value);
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/CGroupV2.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/CGroupV2.java
deleted file mode 100644
index 3cb34e066ff..00000000000
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/CGroupV2.java
+++ /dev/null
@@ -1,188 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.node.admin.container;
-
-import com.yahoo.collections.Pair;
-import com.yahoo.vespa.defaults.Defaults;
-import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentContext;
-import com.yahoo.vespa.hosted.node.admin.task.util.file.UnixPath;
-
-import java.io.IOException;
-import java.nio.file.FileSystem;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Read and write interface to the cgroup v2 of a Podman container.
- *
- * @see <a href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html">CGroups V2</a>
- * @author freva
- */
-public class CGroupV2 {
-
- private static final Logger logger = Logger.getLogger(CGroupV2.class.getName());
- private static final String MAX = "max";
-
- private final Path rootCgroupPath;
-
- public CGroupV2(FileSystem fileSystem) {
- this.rootCgroupPath = fileSystem.getPath("/sys/fs/cgroup");
- }
-
- /**
- * Wraps {@code command} to ensure it is executed in the given cgroup.
- *
- * <p>WARNING: This method must be called only after vespa-cgexec has been installed.</p>
- *
- * @param cgroup The cgroup to execute the command in, e.g. /sys/fs/cgroup/system.slice/wireguard.scope.
- * @param command The command to execute in the cgroup.
- * @see #cgroupRootPath()
- * @see #cgroupPath(ContainerId)
- */
- public String[] wrapForExecutionIn(Path cgroup, String... command) {
- String[] fullCommand = new String[3 + command.length];
- fullCommand[0] = Defaults.getDefaults().vespaHome() + "/bin/vespa-cgexec";
- fullCommand[1] = "-g";
- fullCommand[2] = cgroup.toString();
- System.arraycopy(command, 0, fullCommand, 3, command.length);
- return fullCommand;
- }
-
- /**
- * Returns quota and period values used for CPU scheduling. This serves as hard cap on CPU usage by allowing
- * the CGroupV2 to use up to {@code quota} each {@code period}. If uncapped, quota will be negative.
- *
- * @param containerId full container ID.
- * @return CPU quota and period for the given container. Empty if CGroupV2 for this container is not found.
- */
- public Optional<Pair<Integer, Integer>> cpuQuotaPeriod(ContainerId containerId) {
- return cpuMaxPath(containerId).readUtf8FileIfExists()
- .map(s -> {
- String[] parts = s.strip().split(" ");
- return new Pair<>(MAX.equals(parts[0]) ? -1 : Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
- });
- }
-
- /** @return number of shares allocated to this CGroupV2 for purposes of CPU time scheduling, empty if CGroupV2 not found */
- public OptionalInt cpuShares(ContainerId containerId) {
- return cpuWeightPath(containerId).readUtf8FileIfExists()
- .map(s -> OptionalInt.of(weightToShares(Integer.parseInt(s.strip()))))
- .orElseGet(OptionalInt::empty);
- }
-
- /** Update CPU quota and period for the given container ID, set quota to -1 value for unlimited */
- public boolean updateCpuQuotaPeriod(NodeAgentContext context, ContainerId containerId, int cpuQuotaUs, int periodUs) {
- String wanted = String.format("%s %d", cpuQuotaUs < 0 ? MAX : cpuQuotaUs, periodUs);
- return writeCGroupsValue(context, cpuMaxPath(containerId), wanted);
- }
-
- public boolean updateCpuShares(NodeAgentContext context, ContainerId containerId, int shares) {
- return writeCGroupsValue(context, cpuWeightPath(containerId), Integer.toString(sharesToWeight(shares)));
- }
-
- enum CpuStatField {
- TOTAL_USAGE_USEC("usage_usec"),
- USER_USAGE_USEC("user_usec"),
- SYSTEM_USAGE_USEC("system_usec"),
- TOTAL_PERIODS("nr_periods"),
- THROTTLED_PERIODS("nr_throttled"),
- THROTTLED_TIME_USEC("throttled_usec");
-
- private final String name;
-
- CpuStatField(String name) {
- this.name = name;
- }
-
- long parseValue(String value) {
- return Long.parseLong(value);
- }
-
- static Optional<CpuStatField> fromField(String fieldName) {
- return Arrays.stream(values())
- .filter(field -> fieldName.equals(field.name))
- .findFirst();
- }
- }
-
- public Map<CpuStatField, Long> cpuStats(ContainerId containerId) throws IOException {
- return Files.readAllLines(cgroupPath(containerId).resolve("cpu.stat")).stream()
- .map(line -> line.split("\\s+"))
- .filter(parts -> parts.length == 2)
- .flatMap(parts -> CpuStatField.fromField(parts[0]).stream().map(field -> new Pair<>(field, field.parseValue(parts[1]))))
- .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
- }
-
- /** @return Maximum amount of memory that can be used by the cgroup and its descendants. */
- public long memoryLimitInBytes(ContainerId containerId) throws IOException {
- String limit = Files.readString(cgroupPath(containerId).resolve("memory.max")).strip();
- return MAX.equals(limit) ? -1L : Long.parseLong(limit);
- }
-
- /** @return The total amount of memory currently being used by the cgroup and its descendants. */
- public long memoryUsageInBytes(ContainerId containerId) throws IOException {
- return parseLong(cgroupPath(containerId).resolve("memory.current"));
- }
-
- /** @return Number of bytes used to cache filesystem data, including tmpfs and shared memory. */
- public long memoryCacheInBytes(ContainerId containerId) throws IOException {
- return parseLong(cgroupPath(containerId).resolve("memory.stat"), "file");
- }
-
- /** Returns the cgroup v2 mount point path (/sys/fs/cgroup). */
- public Path cgroupRootPath() {
- return rootCgroupPath;
- }
-
- /** Returns the cgroup directory of the Podman container, and which appears as the root cgroup within the container. */
- public Path cgroupPath(ContainerId containerId) {
- // crun path, runc path is without the 'container' directory
- return rootCgroupPath.resolve("machine.slice/libpod-" + containerId + ".scope/container");
- }
-
- private UnixPath cpuMaxPath(ContainerId containerId) {
- return new UnixPath(cgroupPath(containerId).resolve("cpu.max"));
- }
-
- private UnixPath cpuWeightPath(ContainerId containerId) {
- return new UnixPath(cgroupPath(containerId).resolve("cpu.weight"));
- }
-
- private static boolean writeCGroupsValue(NodeAgentContext context, UnixPath unixPath, String value) {
- String currentValue = unixPath.readUtf8File().strip();
- if (currentValue.equals(value)) return false;
-
- context.recordSystemModification(logger, "Updating " + unixPath + " from " + currentValue + " to " + value);
- unixPath.writeUtf8File(value);
- return true;
- }
-
- // Must be same as in crun: https://github.com/containers/crun/blob/72c6e60ade0e4716fe2d8353f0d97d72cc8d1510/src/libcrun/cgroup.c#L3061
- static int sharesToWeight(int shares) { return (int) (1 + ((shares - 2L) * 9999) / 262142); }
- static int weightToShares(int weight) { return (int) (2 + ((weight - 1L) * 262142) / 9999); }
-
- static long parseLong(Path path) throws IOException {
- return Long.parseLong(Files.readString(path).trim());
- }
-
- static long parseLong(Path path, String fieldName) throws IOException {
- return parseLong(Files.readAllLines(path), fieldName);
- }
-
- static long parseLong(List<String> lines, String fieldName) {
- for (String line : lines) {
- String[] fields = line.split("\\s+");
- if (fields.length != 2)
- throw new IllegalArgumentException("Expected line on the format 'key value', got: '" + line + "'");
-
- if (fieldName.equals(fields[0])) return Long.parseLong(fields[1]);
- }
- throw new IllegalArgumentException("No such field: " + fieldName);
- }
-}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperations.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperations.java
index ce2a6bb22ac..b9e7ce56c53 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperations.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperations.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.node.admin.container;
import com.yahoo.config.provision.DockerImage;
+import com.yahoo.vespa.hosted.node.admin.cgroup.Cgroup;
import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
import com.yahoo.vespa.hosted.node.admin.container.image.ContainerImageDownloader;
import com.yahoo.vespa.hosted.node.admin.container.image.ContainerImagePruner;
@@ -33,7 +34,7 @@ public class ContainerOperations {
private final ContainerImagePruner imagePruner;
private final ContainerStatsCollector containerStatsCollector;
- public ContainerOperations(ContainerEngine containerEngine, CGroupV2 cgroup, FileSystem fileSystem) {
+ public ContainerOperations(ContainerEngine containerEngine, Cgroup cgroup, FileSystem fileSystem) {
this.containerEngine = Objects.requireNonNull(containerEngine);
this.imageDownloader = new ContainerImageDownloader(containerEngine);
this.imagePruner = new ContainerImagePruner(containerEngine, Clock.systemUTC());
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollector.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollector.java
index 870809123a9..8244666f9e0 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollector.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollector.java
@@ -1,6 +1,10 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.container;
+import com.yahoo.vespa.hosted.node.admin.cgroup.Cgroup;
+import com.yahoo.vespa.hosted.node.admin.cgroup.CpuController;
+import com.yahoo.vespa.hosted.node.admin.cgroup.Size;
+import com.yahoo.vespa.hosted.node.admin.cgroup.MemoryController;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentContext;
import com.yahoo.vespa.hosted.node.admin.task.util.file.UnixUser;
@@ -27,18 +31,18 @@ import java.util.stream.Stream;
class ContainerStatsCollector {
private final ContainerEngine containerEngine;
- private final CGroupV2 cgroup;
private final FileSystem fileSystem;
+ private final Cgroup rootCgroup;
private final int onlineCpus;
- ContainerStatsCollector(ContainerEngine containerEngine, CGroupV2 cgroup, FileSystem fileSystem) {
- this(containerEngine, cgroup, fileSystem, Runtime.getRuntime().availableProcessors());
+ ContainerStatsCollector(ContainerEngine containerEngine, Cgroup rootCgroup, FileSystem fileSystem) {
+ this(containerEngine, rootCgroup, fileSystem, Runtime.getRuntime().availableProcessors());
}
- ContainerStatsCollector(ContainerEngine containerEngine, CGroupV2 cgroup, FileSystem fileSystem, int onlineCpus) {
+ ContainerStatsCollector(ContainerEngine containerEngine, Cgroup rootCgroup, FileSystem fileSystem, int onlineCpus) {
this.containerEngine = Objects.requireNonNull(containerEngine);
- this.cgroup = Objects.requireNonNull(cgroup);
this.fileSystem = Objects.requireNonNull(fileSystem);
+ this.rootCgroup = Objects.requireNonNull(rootCgroup);
this.onlineCpus = onlineCpus;
}
@@ -52,6 +56,10 @@ class ContainerStatsCollector {
return Optional.of(new ContainerStats(networkStats, memoryStats, cpuStats, gpuStats));
} catch (NoSuchFileException ignored) {
return Optional.empty(); // Container disappeared while we collected stats
+ } catch (UncheckedIOException e) {
+ if (e.getCause() != null && e.getCause() instanceof NoSuchFileException)
+ return Optional.empty();
+ throw e;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@@ -83,21 +91,22 @@ class ContainerStatsCollector {
}
private ContainerStats.CpuStats collectCpuStats(ContainerId containerId) throws IOException {
- Map<CGroupV2.CpuStatField, Long> cpuStats = cgroup.cpuStats(containerId);
+ Map<CpuController.StatField, Long> cpuStats = rootCgroup.resolveContainer(containerId).cpu().readStats();
return new ContainerStats.CpuStats(onlineCpus,
systemCpuUsage(),
- cpuStats.get(CGroupV2.CpuStatField.TOTAL_USAGE_USEC),
- cpuStats.get(CGroupV2.CpuStatField.SYSTEM_USAGE_USEC),
- cpuStats.get(CGroupV2.CpuStatField.THROTTLED_TIME_USEC),
- cpuStats.get(CGroupV2.CpuStatField.TOTAL_PERIODS),
- cpuStats.get(CGroupV2.CpuStatField.THROTTLED_PERIODS));
+ cpuStats.get(CpuController.StatField.TOTAL_USAGE_USEC),
+ cpuStats.get(CpuController.StatField.SYSTEM_USAGE_USEC),
+ cpuStats.get(CpuController.StatField.THROTTLED_TIME_USEC),
+ cpuStats.get(CpuController.StatField.TOTAL_PERIODS),
+ cpuStats.get(CpuController.StatField.THROTTLED_PERIODS));
}
private ContainerStats.MemoryStats collectMemoryStats(ContainerId containerId) throws IOException {
- long memoryLimitInBytes = cgroup.memoryLimitInBytes(containerId);
- long memoryUsageInBytes = cgroup.memoryUsageInBytes(containerId);
- long cachedInBytes = cgroup.memoryCacheInBytes(containerId);
- return new ContainerStats.MemoryStats(cachedInBytes, memoryUsageInBytes, memoryLimitInBytes);
+ MemoryController memoryController = rootCgroup.resolveContainer(containerId).memory();
+ Size max = memoryController.readMax();
+ long memoryUsageInBytes = memoryController.readCurrent().value();
+ long cachedInBytes = memoryController.readFileSystemCache().value();
+ return new ContainerStats.MemoryStats(cachedInBytes, memoryUsageInBytes, max.isMax() ? -1 : max.value());
}
private ContainerStats.NetworkStats collectNetworkStats(String iface, int containerPid) throws IOException {
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
index fbef3def446..94c2df1a8b8 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
@@ -23,6 +23,7 @@ import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.time.Instant;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
@@ -99,6 +100,22 @@ public class UnixPath {
}
}
+ public List<String> readLines() {
+ return uncheck(() -> Files.readAllLines(path));
+ }
+
+ /** Create an empty file and return true, or false if the file already exists (the file may not be regular). */
+ public boolean create() {
+ try {
+ Files.createFile(path);
+ return true;
+ } catch (FileAlreadyExistsException ignored) {
+ return false;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
public UnixPath writeUtf8File(String content, OpenOption... options) {
return writeBytes(content.getBytes(StandardCharsets.UTF_8), options);
}
@@ -209,15 +226,16 @@ public class UnixPath {
return this;
}
- /** Create directory with given permissions, unless it already exists, and return this. */
- public UnixPath createDirectory(String... permissions) {
+ /** Create directory with given permissions and return true, or false if it already exists. */
+ public boolean createDirectory(String... permissions) {
try {
Files.createDirectory(path, permissionsAsFileAttributes(permissions));
} catch (FileAlreadyExistsException ignore) {
+ return false;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
- return this;
+ return true;
}
public UnixPath createDirectories(String... permissions) {
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/CGroupV2Test.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/cgroup/CgroupTest.java
index 789f31f75c6..27580082020 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/CGroupV2Test.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/cgroup/CgroupTest.java
@@ -1,7 +1,9 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.node.admin.container;
-import com.yahoo.collections.Pair;
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.node.admin.cgroup;
+
+import com.yahoo.vespa.hosted.node.admin.container.ContainerId;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentContext;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentContextImpl;
import com.yahoo.vespa.hosted.node.admin.task.util.file.UnixPath;
@@ -12,16 +14,15 @@ import java.io.IOException;
import java.nio.file.FileSystem;
import java.util.Map;
import java.util.Optional;
-import java.util.OptionalInt;
-
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.SYSTEM_USAGE_USEC;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.THROTTLED_PERIODS;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.THROTTLED_TIME_USEC;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.TOTAL_PERIODS;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.TOTAL_USAGE_USEC;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.USER_USAGE_USEC;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.sharesToWeight;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.weightToShares;
+
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.SYSTEM_USAGE_USEC;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.THROTTLED_PERIODS;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.THROTTLED_TIME_USEC;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.TOTAL_PERIODS;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.TOTAL_USAGE_USEC;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.USER_USAGE_USEC;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.sharesToWeight;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.weightToShares;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -29,47 +30,48 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @author freva
*/
-public class CGroupV2Test {
+public class CgroupTest {
private static final ContainerId containerId = new ContainerId("4aec78cc");
private final FileSystem fileSystem = TestFileSystem.create();
- private final CGroupV2 cgroup = new CGroupV2(fileSystem);
+ private final Cgroup containerCgroup = Cgroup.root(fileSystem).resolveContainer(containerId);
+ private final CpuController containerCpu = containerCgroup.cpu();
private final NodeAgentContext context = NodeAgentContextImpl.builder("node123.yahoo.com").fileSystem(fileSystem).build();
private final UnixPath cgroupRoot = new UnixPath(fileSystem.getPath("/sys/fs/cgroup/machine.slice/libpod-4aec78cc.scope/container")).createDirectories();
@Test
public void updates_cpu_quota_and_period() {
- assertEquals(Optional.empty(), cgroup.cpuQuotaPeriod(containerId));
+ assertEquals(Optional.empty(), containerCgroup.cpu().readMax());
cgroupRoot.resolve("cpu.max").writeUtf8File("max 100000\n");
- assertEquals(Optional.of(new Pair<>(-1, 100000)), cgroup.cpuQuotaPeriod(containerId));
+ assertEquals(Optional.of(new CpuController.Max(Size.max(), 100000)), containerCpu.readMax());
cgroupRoot.resolve("cpu.max").writeUtf8File("456 123456\n");
- assertEquals(Optional.of(new Pair<>(456, 123456)), cgroup.cpuQuotaPeriod(containerId));
+ assertEquals(Optional.of(new CpuController.Max(Size.from(456), 123456)), containerCpu.readMax());
- assertFalse(cgroup.updateCpuQuotaPeriod(context, containerId, 456, 123456));
+ containerCgroup.cpu().updateMax(context, 456, 123456);
- assertTrue(cgroup.updateCpuQuotaPeriod(context, containerId, 654, 123456));
- assertEquals(Optional.of(new Pair<>(654, 123456)), cgroup.cpuQuotaPeriod(containerId));
- assertEquals("654 123456", cgroupRoot.resolve("cpu.max").readUtf8File());
+ assertTrue(containerCgroup.cpu().updateMax(context, 654, 123456));
+ assertEquals(Optional.of(new CpuController.Max(Size.from(654), 123456)), containerCpu.readMax());
+ assertEquals("654 123456\n", cgroupRoot.resolve("cpu.max").readUtf8File());
- assertTrue(cgroup.updateCpuQuotaPeriod(context, containerId, -1, 123456));
- assertEquals(Optional.of(new Pair<>(-1, 123456)), cgroup.cpuQuotaPeriod(containerId));
- assertEquals("max 123456", cgroupRoot.resolve("cpu.max").readUtf8File());
+ assertTrue(containerCgroup.cpu().updateMax(context, -1, 123456));
+ assertEquals(Optional.of(new CpuController.Max(Size.max(), 123456)), containerCpu.readMax());
+ assertEquals("max 123456\n", cgroupRoot.resolve("cpu.max").readUtf8File());
}
@Test
public void updates_cpu_shares() {
- assertEquals(OptionalInt.empty(), cgroup.cpuShares(containerId));
+ assertEquals(Optional.empty(), containerCgroup.cpu().readShares());
cgroupRoot.resolve("cpu.weight").writeUtf8File("1\n");
- assertEquals(OptionalInt.of(2), cgroup.cpuShares(containerId));
+ assertEquals(Optional.of(2), containerCgroup.cpu().readShares());
- assertFalse(cgroup.updateCpuShares(context, containerId, 2));
+ assertFalse(containerCgroup.cpu().updateShares(context, 2));
- assertTrue(cgroup.updateCpuShares(context, containerId, 12345));
- assertEquals(OptionalInt.of(12323), cgroup.cpuShares(containerId));
+ assertTrue(containerCgroup.cpu().updateShares(context, 12345));
+ assertEquals(Optional.of(12323), containerCgroup.cpu().readShares());
}
@Test
@@ -82,16 +84,16 @@ public class CGroupV2Test {
"throttled_usec 14256\n");
assertEquals(Map.of(TOTAL_USAGE_USEC, 17794243L, USER_USAGE_USEC, 16099205L, SYSTEM_USAGE_USEC, 1695038L,
- TOTAL_PERIODS, 12465L, THROTTLED_PERIODS, 25L, THROTTLED_TIME_USEC, 14256L), cgroup.cpuStats(containerId));
+ TOTAL_PERIODS, 12465L, THROTTLED_PERIODS, 25L, THROTTLED_TIME_USEC, 14256L), containerCgroup.cpu().readStats());
}
@Test
public void reads_memory_metrics() throws IOException {
cgroupRoot.resolve("memory.current").writeUtf8File("2525093888\n");
- assertEquals(2525093888L, cgroup.memoryUsageInBytes(containerId));
+ assertEquals(2525093888L, containerCgroup.memory().readCurrent().value());
cgroupRoot.resolve("memory.max").writeUtf8File("4322885632\n");
- assertEquals(4322885632L, cgroup.memoryLimitInBytes(containerId));
+ assertEquals(4322885632L, containerCgroup.memory().readMax().value());
cgroupRoot.resolve("memory.stat").writeUtf8File("anon 3481600\n" +
"file 69206016\n" +
@@ -102,7 +104,7 @@ public class CGroupV2Test {
"shmem 8380416\n" +
"file_mapped 1081344\n" +
"file_dirty 135168\n");
- assertEquals(69206016L, cgroup.memoryCacheInBytes(containerId));
+ assertEquals(69206016L, containerCgroup.memory().readFileSystemCache().value());
}
@Test
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperationsTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperationsTest.java
index 701dd33cf55..09542c9c10a 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperationsTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerOperationsTest.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.node.admin.container;
import com.yahoo.config.provision.DockerImage;
+import com.yahoo.vespa.hosted.node.admin.cgroup.Cgroup;
import com.yahoo.vespa.hosted.node.admin.component.TestTaskContext;
import com.yahoo.vespa.test.file.TestFileSystem;
import org.junit.jupiter.api.Test;
@@ -11,9 +12,11 @@ import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.stream.Collectors;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
/**
* @author mpolden
@@ -23,7 +26,7 @@ public class ContainerOperationsTest {
private final TestTaskContext context = new TestTaskContext();
private final ContainerEngineMock containerEngine = new ContainerEngineMock();
private final FileSystem fileSystem = TestFileSystem.create();
- private final ContainerOperations containerOperations = new ContainerOperations(containerEngine, new CGroupV2(fileSystem), fileSystem);
+ private final ContainerOperations containerOperations = new ContainerOperations(containerEngine, mock(Cgroup.class), fileSystem);
@Test
void no_managed_containers_running() {
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollectorTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollectorTest.java
index 72c5d016a47..d4598c8923f 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollectorTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/container/ContainerStatsCollectorTest.java
@@ -1,6 +1,8 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.container;
+import com.yahoo.vespa.hosted.node.admin.cgroup.Cgroup;
+import com.yahoo.vespa.hosted.node.admin.cgroup.Size;
import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeSpec;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentContext;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentContextImpl;
@@ -8,6 +10,7 @@ import com.yahoo.vespa.hosted.node.admin.task.util.file.UnixPath;
import com.yahoo.vespa.hosted.node.admin.task.util.process.TestTerminal;
import com.yahoo.vespa.test.file.TestFileSystem;
import org.junit.jupiter.api.Test;
+import org.mockito.Answers;
import java.io.IOException;
import java.nio.file.FileSystem;
@@ -17,12 +20,12 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.SYSTEM_USAGE_USEC;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.THROTTLED_PERIODS;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.THROTTLED_TIME_USEC;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.TOTAL_PERIODS;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.TOTAL_USAGE_USEC;
-import static com.yahoo.vespa.hosted.node.admin.container.CGroupV2.CpuStatField.USER_USAGE_USEC;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.SYSTEM_USAGE_USEC;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.THROTTLED_PERIODS;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.THROTTLED_TIME_USEC;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.TOTAL_PERIODS;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.TOTAL_USAGE_USEC;
+import static com.yahoo.vespa.hosted.node.admin.cgroup.CpuController.StatField.USER_USAGE_USEC;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
@@ -37,11 +40,10 @@ public class ContainerStatsCollectorTest {
private final TestTerminal testTerminal = new TestTerminal();
private final ContainerEngineMock containerEngine = new ContainerEngineMock(testTerminal);
private final FileSystem fileSystem = TestFileSystem.create();
- private final CGroupV2 cgroup = mock(CGroupV2.class);
+ private final Cgroup cgroup = mock(Cgroup.class, Answers.RETURNS_DEEP_STUBS);
private final NodeAgentContext context = NodeAgentContextImpl.builder(NodeSpec.Builder.testSpec("c1").build())
.fileSystem(TestFileSystem.create())
.build();
-
@Test
void collect() throws Exception {
ContainerStatsCollector collector = new ContainerStatsCollector(containerEngine, cgroup, fileSystem, 24);
@@ -92,17 +94,17 @@ public class ContainerStatsCollectorTest {
" eth0: 22280813 118083 3 4 0 0 0 0 19859383 115415 5 6 0 0 0 0\n");
}
- private void mockMemoryStats(ContainerId containerId) throws IOException {
- when(cgroup.memoryUsageInBytes(eq(containerId))).thenReturn(1228017664L);
- when(cgroup.memoryLimitInBytes(eq(containerId))).thenReturn(2147483648L);
- when(cgroup.memoryCacheInBytes(eq(containerId))).thenReturn(470790144L);
+ private void mockMemoryStats(ContainerId containerId) {
+ when(cgroup.resolveContainer(eq(containerId)).memory().readCurrent()).thenReturn(Size.from(1228017664L));
+ when(cgroup.resolveContainer(eq(containerId)).memory().readMax()).thenReturn(Size.from(2147483648L));
+ when(cgroup.resolveContainer(eq(containerId)).memory().readFileSystemCache()).thenReturn(Size.from(470790144L));
}
private void mockCpuStats(ContainerId containerId) throws IOException {
UnixPath proc = new UnixPath(fileSystem.getPath("/proc"));
proc.createDirectories();
- when(cgroup.cpuStats(eq(containerId))).thenReturn(Map.of(
+ when(cgroup.resolveContainer(eq(containerId)).cpu().readStats()).thenReturn(Map.of(
TOTAL_USAGE_USEC, 691675615472L, SYSTEM_USAGE_USEC, 262190000000L, USER_USAGE_USEC, 40900L,
TOTAL_PERIODS, 1L, THROTTLED_PERIODS, 2L, THROTTLED_TIME_USEC, 3L));
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java
index 7676d0e1790..1fe6081f1b7 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java
@@ -5,9 +5,9 @@ import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.flags.InMemoryFlagSource;
+import com.yahoo.vespa.hosted.node.admin.cgroup.Cgroup;
import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeSpec;
import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator;
-import com.yahoo.vespa.hosted.node.admin.container.CGroupV2;
import com.yahoo.vespa.hosted.node.admin.container.ContainerEngineMock;
import com.yahoo.vespa.hosted.node.admin.container.ContainerName;
import com.yahoo.vespa.hosted.node.admin.container.ContainerOperations;
@@ -60,7 +60,7 @@ public class ContainerTester implements AutoCloseable {
private final ContainerEngineMock containerEngine = new ContainerEngineMock();
private final FileSystem fileSystem = TestFileSystem.create();
- final ContainerOperations containerOperations = spy(new ContainerOperations(containerEngine, new CGroupV2(fileSystem), fileSystem));
+ final ContainerOperations containerOperations = spy(new ContainerOperations(containerEngine, mock(Cgroup.class), fileSystem));
final NodeRepoMock nodeRepository = spy(new NodeRepoMock());
final Orchestrator orchestrator = mock(Orchestrator.class);
final StorageMaintainer storageMaintainer = mock(StorageMaintainer.class);
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPathTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPathTest.java
index ad8c6ea3a35..bbe96272b4b 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPathTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPathTest.java
@@ -10,8 +10,14 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* @author hakonhall
@@ -38,6 +44,15 @@ public class UnixPathTest {
path.writeUtf8File(original);
String fromFile = path.readUtf8File();
assertEquals(original, fromFile);
+ assertEquals(List.of("foo", "bar"), path.readLines());
+ }
+
+ @Test
+ void touch() {
+ UnixPath path = new UnixPath(fs.getPath("example.txt"));
+ assertTrue(path.create());
+ assertEquals("", path.readUtf8File());
+ assertFalse(path.create());
}
@Test
@@ -74,9 +89,10 @@ public class UnixPathTest {
Path path = fs.getPath("dir");
UnixPath unixPath = new UnixPath(path);
String permissions = "rwxr-xr--";
- unixPath.createDirectory(permissions);
+ assertTrue(unixPath.createDirectory(permissions));
assertTrue(unixPath.isDirectory());
assertEquals(permissions, unixPath.getPermissions());
+ assertFalse(unixPath.createDirectory(permissions));
}
@Test