From de29ef3bf3a029f99f340de9f093cd20544217fd Mon Sep 17 00:00:00 2001 From: Sergey Chernyshev Date: Wed, 5 Mar 2025 10:32:36 +0000 Subject: [PATCH] 8343191: Cgroup v1 subsystem fails to set subsystem path Co-authored-by: Severin Gehwolf Reviewed-by: sgehwolf, mbaesken --- src/hotspot/os/linux/cgroupUtil_linux.cpp | 30 ++++- .../os/linux/cgroupV1Subsystem_linux.cpp | 77 +++++++++-- .../os/linux/cgroupV2Subsystem_linux.cpp | 6 +- .../cgroupv1/CgroupV1SubsystemController.java | 42 +++--- .../runtime/test_cgroupSubsystem_linux.cpp | 78 ++++++++++- .../docker/TestMemoryWithSubgroups.java | 126 ++++++++++++++++++ .../CgroupV1SubsystemControllerTest.java | 17 ++- .../cgroup/TestCgroupSubsystemFactory.java | 34 ++++- .../TestDockerMemoryMetricsSubgroup.java | 120 +++++++++++++++++ 9 files changed, 488 insertions(+), 42 deletions(-) create mode 100644 test/hotspot/jtreg/containers/docker/TestMemoryWithSubgroups.java create mode 100644 test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetricsSubgroup.java diff --git a/src/hotspot/os/linux/cgroupUtil_linux.cpp b/src/hotspot/os/linux/cgroupUtil_linux.cpp index bc0e018d6be..b52ef87dcae 100644 --- a/src/hotspot/os/linux/cgroupUtil_linux.cpp +++ b/src/hotspot/os/linux/cgroupUtil_linux.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Red Hat, Inc. + * Copyright (c) 2024, 2025, Red Hat, Inc. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,12 +49,18 @@ int CgroupUtil::processor_count(CgroupCpuController* cpu_ctrl, int host_cpus) { } void CgroupUtil::adjust_controller(CgroupMemoryController* mem) { + assert(mem->cgroup_path() != nullptr, "invariant"); + if (strstr(mem->cgroup_path(), "../") != nullptr) { + log_warning(os, container)("Cgroup memory controller path at '%s' seems to have moved to '%s', detected limits won't be accurate", + mem->mount_point(), mem->cgroup_path()); + mem->set_subsystem_path("/"); + return; + } if (!mem->needs_hierarchy_adjustment()) { // nothing to do return; } log_trace(os, container)("Adjusting controller path for memory: %s", mem->subsystem_path()); - assert(mem->cgroup_path() != nullptr, "invariant"); char* orig = os::strdup(mem->cgroup_path()); char* cg_path = os::strdup(orig); char* last_slash; @@ -62,7 +68,8 @@ void CgroupUtil::adjust_controller(CgroupMemoryController* mem) { julong phys_mem = os::Linux::physical_memory(); char* limit_cg_path = nullptr; jlong limit = mem->read_memory_limit_in_bytes(phys_mem); - jlong lowest_limit = phys_mem; + jlong lowest_limit = limit < 0 ? phys_mem : limit; + julong orig_limit = ((julong)lowest_limit) != phys_mem ? lowest_limit : phys_mem; while ((last_slash = strrchr(cg_path, '/')) != cg_path) { *last_slash = '\0'; // strip path // update to shortened path and try again @@ -83,7 +90,7 @@ void CgroupUtil::adjust_controller(CgroupMemoryController* mem) { limit_cg_path = os::strdup("/"); } assert(lowest_limit >= 0, "limit must be positive"); - if ((julong)lowest_limit != phys_mem) { + if ((julong)lowest_limit != orig_limit) { // we've found a lower limit anywhere in the hierarchy, // set the path to the limit path assert(limit_cg_path != nullptr, "limit path must be set"); @@ -93,6 +100,7 @@ void CgroupUtil::adjust_controller(CgroupMemoryController* mem) { mem->subsystem_path(), lowest_limit); } else { + log_trace(os, container)("Lowest limit was: " JLONG_FORMAT, lowest_limit); log_trace(os, container)("No lower limit found for memory in hierarchy %s, " "adjusting to original path %s", mem->mount_point(), orig); @@ -104,19 +112,26 @@ void CgroupUtil::adjust_controller(CgroupMemoryController* mem) { } void CgroupUtil::adjust_controller(CgroupCpuController* cpu) { + assert(cpu->cgroup_path() != nullptr, "invariant"); + if (strstr(cpu->cgroup_path(), "../") != nullptr) { + log_warning(os, container)("Cgroup cpu controller path at '%s' seems to have moved to '%s', detected limits won't be accurate", + cpu->mount_point(), cpu->cgroup_path()); + cpu->set_subsystem_path("/"); + return; + } if (!cpu->needs_hierarchy_adjustment()) { // nothing to do return; } log_trace(os, container)("Adjusting controller path for cpu: %s", cpu->subsystem_path()); - assert(cpu->cgroup_path() != nullptr, "invariant"); char* orig = os::strdup(cpu->cgroup_path()); char* cg_path = os::strdup(orig); char* last_slash; assert(cg_path[0] == '/', "cgroup path must start with '/'"); int host_cpus = os::Linux::active_processor_count(); int cpus = CgroupUtil::processor_count(cpu, host_cpus); - int lowest_limit = host_cpus; + int lowest_limit = cpus < host_cpus ? cpus: host_cpus; + int orig_limit = lowest_limit != host_cpus ? lowest_limit : host_cpus; char* limit_cg_path = nullptr; while ((last_slash = strrchr(cg_path, '/')) != cg_path) { *last_slash = '\0'; // strip path @@ -138,7 +153,7 @@ void CgroupUtil::adjust_controller(CgroupCpuController* cpu) { limit_cg_path = os::strdup(cg_path); } assert(lowest_limit >= 0, "limit must be positive"); - if (lowest_limit != host_cpus) { + if (lowest_limit != orig_limit) { // we've found a lower limit anywhere in the hierarchy, // set the path to the limit path assert(limit_cg_path != nullptr, "limit path must be set"); @@ -148,6 +163,7 @@ void CgroupUtil::adjust_controller(CgroupCpuController* cpu) { cpu->subsystem_path(), lowest_limit); } else { + log_trace(os, container)("Lowest limit was: %d", lowest_limit); log_trace(os, container)("No lower limit found for cpu in hierarchy %s, " "adjusting to original path %s", cpu->mount_point(), orig); diff --git a/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp index a6ac2822b25..8d9c3edb72a 100644 --- a/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,6 +37,47 @@ /* * Set directory to subsystem specific files based * on the contents of the mountinfo and cgroup files. + * + * The method determines whether it runs in + * - host mode + * - container mode + * + * In the host mode, _root is equal to "/" and + * the subsystem path is equal to the _mount_point path + * joined with cgroup_path. + * + * In the container mode, it can be two possibilities: + * - private namespace (cgroupns=private) + * - host namespace (cgroupns=host, default mode in cgroup V1 hosts) + * + * Private namespace is equivalent to the host mode, i.e. + * the subsystem path is set by concatenating + * _mount_point and cgroup_path. + * + * In the host namespace, _root is equal to host's cgroup path + * of the control group to which the containerized process + * belongs to at the moment of creation. The mountinfo and + * cgroup files are mirrored from the host, while the subsystem + * specific files are mapped directly at _mount_point, i.e. + * at /sys/fs/cgroup//, the subsystem path is + * then set equal to _mount_point. + * + * A special case of the subsystem path is when a cgroup path + * includes a subgroup, when a containerized process was associated + * with an existing cgroup, that is different from cgroup + * in which the process has been created. + * Here, the _root is equal to the host's initial cgroup path, + * cgroup_path will be equal to host's new cgroup path. + * As host cgroup hierarchies are not accessible in the container, + * it needs to be determined which part of cgroup path + * is accessible inside container, i.e. mapped under + * /sys/fs/cgroup//. + * In Docker default setup, host's cgroup path can be + * of the form: /docker//, + * from which only is mapped. + * The method trims cgroup path from left, until the subgroup + * component is found. The subsystem path will be set to + * the _mount_point joined with the subgroup path. */ void CgroupV1Controller::set_subsystem_path(const char* cgroup_path) { if (_cgroup_path != nullptr) { @@ -49,28 +90,36 @@ void CgroupV1Controller::set_subsystem_path(const char* cgroup_path) { _cgroup_path = os::strdup(cgroup_path); stringStream ss; if (_root != nullptr && cgroup_path != nullptr) { + ss.print_raw(_mount_point); if (strcmp(_root, "/") == 0) { - ss.print_raw(_mount_point); + // host processes and containers with cgroupns=private if (strcmp(cgroup_path,"/") != 0) { ss.print_raw(cgroup_path); } - _path = os::strdup(ss.base()); } else { - if (strcmp(_root, cgroup_path) == 0) { - ss.print_raw(_mount_point); - _path = os::strdup(ss.base()); - } else { - char *p = strstr((char*)cgroup_path, _root); - if (p != nullptr && p == _root) { - if (strlen(cgroup_path) > strlen(_root)) { - ss.print_raw(_mount_point); - const char* cg_path_sub = cgroup_path + strlen(_root); - ss.print_raw(cg_path_sub); - _path = os::strdup(ss.base()); + // containers with cgroupns=host, default setting is _root==cgroup_path + if (strcmp(_root, cgroup_path) != 0) { + if (*cgroup_path != '\0' && strcmp(cgroup_path, "/") != 0) { + // When moved to a subgroup, between subgroups, the path suffix will change. + const char *suffix = cgroup_path; + while (suffix != nullptr) { + stringStream pp; + pp.print_raw(_mount_point); + pp.print_raw(suffix); + if (os::file_exists(pp.base())) { + ss.print_raw(suffix); + if (suffix != cgroup_path) { + log_trace(os, container)("set_subsystem_path: cgroup v1 path reduced to: %s.", suffix); + } + break; + } + log_trace(os, container)("set_subsystem_path: skipped non-existent directory: %s.", suffix); + suffix = strchr(suffix + 1, '/'); } } } } + _path = os::strdup(ss.base()); } } diff --git a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp index 62e8cac3a62..cbadbb9db02 100644 --- a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022, Red Hat Inc. + * Copyright (c) 2020, 2025, Red Hat Inc. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -292,6 +292,10 @@ jlong memory_swap_limit_value(CgroupV2Controller* ctrl) { } void CgroupV2Controller::set_subsystem_path(const char* cgroup_path) { + if (_cgroup_path != nullptr) { + os::free(_cgroup_path); + } + _cgroup_path = os::strdup(cgroup_path); if (_path != nullptr) { os::free(_path); } diff --git a/src/java.base/linux/classes/jdk/internal/platform/cgroupv1/CgroupV1SubsystemController.java b/src/java.base/linux/classes/jdk/internal/platform/cgroupv1/CgroupV1SubsystemController.java index 051b4da5f78..fd325a8f8b4 100644 --- a/src/java.base/linux/classes/jdk/internal/platform/cgroupv1/CgroupV1SubsystemController.java +++ b/src/java.base/linux/classes/jdk/internal/platform/cgroupv1/CgroupV1SubsystemController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,9 @@ package jdk.internal.platform.cgroupv1; +import java.lang.System.Logger.Level; +import java.nio.file.Path; +import java.nio.file.Files; import jdk.internal.platform.CgroupSubsystem; import jdk.internal.platform.CgroupSubsystemController; @@ -44,27 +47,36 @@ public class CgroupV1SubsystemController implements CgroupSubsystemController { public void setPath(String cgroupPath) { if (root != null && cgroupPath != null) { + String path = mountPoint; if (root.equals("/")) { + // host processes and containers with cgroupns=private if (!cgroupPath.equals("/")) { - path = mountPoint + cgroupPath; + path += cgroupPath; } - else { - path = mountPoint; - } - } - else { - if (root.equals(cgroupPath)) { - path = mountPoint; - } - else { - if (cgroupPath.startsWith(root)) { - if (cgroupPath.length() > root.length()) { - String cgroupSubstr = cgroupPath.substring(root.length()); - path = mountPoint + cgroupSubstr; + } else { + // containers with cgroupns=host, default setting is _root==cgroup_path + if (!cgroupPath.equals(root)) { + if (!cgroupPath.equals("") && !cgroupPath.equals("/")) { + // When moved to a subgroup, between subgroups, the path suffix will change. + Path cgp = Path.of(cgroupPath); + int nameCount = cgp.getNameCount(); + for (int i=0; i < nameCount; i++) { + Path dir = Path.of(mountPoint, cgp.toString()); + if (Files.isDirectory(dir)) { + path = dir.toString(); + if (i > 0) { + System.getLogger("jdk.internal.platform").log(Level.DEBUG, String.format( + "Cgroup v1 path reduced to: %s.", cgp)); + } + break; + } + int currentNameCount = cgp.getNameCount(); + cgp = (currentNameCount > 1) ? cgp.subpath(1, currentNameCount) : Path.of(""); } } } } + this.path = path; } } diff --git a/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp b/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp index c8e2beff52b..c090aa28e9a 100644 --- a/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp +++ b/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp @@ -25,6 +25,7 @@ #include "runtime/os.hpp" #include "cgroupSubsystem_linux.hpp" +#include "cgroupUtil_linux.hpp" #include "cgroupV1Subsystem_linux.hpp" #include "cgroupV2Subsystem_linux.hpp" #include "unittest.hpp" @@ -432,9 +433,16 @@ TEST(cgroupTest, set_cgroupv1_subsystem_path) { "/user.slice/user-1000.slice/user@1000.service", // cgroup_path "/sys/fs/cgroup/mem" // expected_path }; - int length = 2; + TestCase container_moving_cgroup = { + "/sys/fs/cgroup/cpu,cpuacct", // mount_path + "/system.slice/garden.service/garden/good/2f57368b-0eda-4e52-64d8-af5c", // root_path + "/system.slice/garden.service/garden/bad/2f57368b-0eda-4e52-64d8-af5c", // cgroup_path + "/sys/fs/cgroup/cpu,cpuacct" // expected_path + }; + int length = 3; TestCase* testCases[] = { &host, - &container_engine }; + &container_engine, + &container_moving_cgroup }; for (int i = 0; i < length; i++) { CgroupV1Controller* ctrl = new CgroupV1Controller( (char*)testCases[i]->root_path, (char*)testCases[i]->mount_path, @@ -444,6 +452,72 @@ TEST(cgroupTest, set_cgroupv1_subsystem_path) { } } +TEST(cgroupTest, set_cgroupv1_subsystem_path_adjusted) { + TestCase memory = { + "/sys/fs/cgroup/memory", // mount_path + "/", // root_path + "../test1", // cgroup_path + "/sys/fs/cgroup/memory" // expected_path + }; + TestCase cpu = { + "/sys/fs/cgroup/cpu", // mount_path + "/", // root_path + "../../test2", // cgroup_path + "/sys/fs/cgroup/cpu" // expected_path + }; + CgroupCpuController* ccc = new CgroupV1CpuController(CgroupV1Controller((char*)cpu.root_path, + (char*)cpu.mount_path, + true /* read-only mount */)); + ccc->set_subsystem_path((char*)cpu.cgroup_path); + EXPECT_TRUE(ccc->needs_hierarchy_adjustment()); + + CgroupUtil::adjust_controller(ccc); + ASSERT_STREQ(cpu.expected_path, ccc->subsystem_path()); + EXPECT_FALSE(ccc->needs_hierarchy_adjustment()); + + CgroupMemoryController* cmc = new CgroupV1MemoryController(CgroupV1Controller((char*)memory.root_path, + (char*)memory.mount_path, + true /* read-only mount */)); + cmc->set_subsystem_path((char*)memory.cgroup_path); + EXPECT_TRUE(cmc->needs_hierarchy_adjustment()); + + CgroupUtil::adjust_controller(cmc); + ASSERT_STREQ(memory.expected_path, cmc->subsystem_path()); + EXPECT_FALSE(cmc->needs_hierarchy_adjustment()); +} + +TEST(cgroupTest, set_cgroupv2_subsystem_path_adjusted) { + TestCase memory = { + "/sys/fs/cgroup", // mount_path + "/", // root_path + "../test1", // cgroup_path + "/sys/fs/cgroup" // expected_path + }; + TestCase cpu = { + "/sys/fs/cgroup", // mount_path + "/", // root_path + "../../test2", // cgroup_path + "/sys/fs/cgroup" // expected_path + }; + CgroupCpuController* ccc = new CgroupV2CpuController(CgroupV2Controller((char*)cpu.mount_path, + (char*)cpu.cgroup_path, + true /* read-only mount */)); + EXPECT_TRUE(ccc->needs_hierarchy_adjustment()); + + CgroupUtil::adjust_controller(ccc); + ASSERT_STREQ(cpu.expected_path, ccc->subsystem_path()); + EXPECT_FALSE(ccc->needs_hierarchy_adjustment()); + + CgroupMemoryController* cmc = new CgroupV2MemoryController(CgroupV2Controller((char*)memory.mount_path, + (char*)memory.cgroup_path, + true /* read-only mount */)); + EXPECT_TRUE(cmc->needs_hierarchy_adjustment()); + + CgroupUtil::adjust_controller(cmc); + ASSERT_STREQ(memory.expected_path, cmc->subsystem_path()); + EXPECT_FALSE(cmc->needs_hierarchy_adjustment()); +} + TEST(cgroupTest, set_cgroupv2_subsystem_path) { TestCase at_mount_root = { "/sys/fs/cgroup", // mount_path diff --git a/test/hotspot/jtreg/containers/docker/TestMemoryWithSubgroups.java b/test/hotspot/jtreg/containers/docker/TestMemoryWithSubgroups.java new file mode 100644 index 00000000000..e7989874d7a --- /dev/null +++ b/test/hotspot/jtreg/containers/docker/TestMemoryWithSubgroups.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025, BELLSOFT. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.test.lib.containers.docker.Common; +import jdk.test.lib.containers.docker.DockerTestUtils; +import jdk.test.lib.containers.docker.DockerRunOptions; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; +import jdk.internal.platform.Metrics; + +import java.util.ArrayList; + +import jtreg.SkippedException; + +/* + * @test + * @bug 8343191 + * @requires os.family == "linux" + * @modules java.base/jdk.internal.platform + * @library /test/lib + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar whitebox.jar jdk.test.whitebox.WhiteBox + * @run main TestMemoryWithSubgroups + */ +public class TestMemoryWithSubgroups { + + private static final String imageName = Common.imageName("subgroup"); + + public static void main(String[] args) throws Exception { + Metrics metrics = Metrics.systemMetrics(); + if (metrics == null) { + System.out.println("Cgroup not configured."); + return; + } + if (!DockerTestUtils.canTestDocker()) { + System.out.println("Unable to run docker tests."); + return; + } + Common.prepareWhiteBox(); + DockerTestUtils.buildJdkContainerImage(imageName); + + if ("cgroupv1".equals(metrics.getProvider())) { + try { + testMemoryLimitSubgroupV1("200m", "100m", "104857600", false); + testMemoryLimitSubgroupV1("1g", "500m", "524288000", false); + testMemoryLimitSubgroupV1("200m", "100m", "104857600", true); + testMemoryLimitSubgroupV1("1g", "500m", "524288000", true); + } finally { + DockerTestUtils.removeDockerImage(imageName); + } + } else if ("cgroupv2".equals(metrics.getProvider())) { + try { + testMemoryLimitSubgroupV2("200m", "100m", "104857600", false); + testMemoryLimitSubgroupV2("1g", "500m", "524288000", false); + testMemoryLimitSubgroupV2("200m", "100m", "104857600", true); + testMemoryLimitSubgroupV2("1g", "500m", "524288000", true); + } finally { + DockerTestUtils.removeDockerImage(imageName); + } + } else { + throw new SkippedException("Metrics are from neither cgroup v1 nor v2, skipped for now."); + } + } + + private static void testMemoryLimitSubgroupV1(String containerMemorySize, String valueToSet, String expectedValue, boolean privateNamespace) + throws Exception { + + Common.logNewTestCase("Cgroup V1 subgroup memory limit: " + valueToSet); + + DockerRunOptions opts = new DockerRunOptions(imageName, "sh", "-c"); + opts.javaOpts = new ArrayList<>(); + opts.appendTestJavaOptions = false; + opts.addDockerOpts("--privileged") + .addDockerOpts("--cgroupns=" + (privateNamespace ? "private" : "host")) + .addDockerOpts("--memory", containerMemorySize); + opts.addClassOptions("mkdir -p /sys/fs/cgroup/memory/test ; " + + "echo " + valueToSet + " > /sys/fs/cgroup/memory/test/memory.limit_in_bytes ; " + + "echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs ; " + + "/jdk/bin/java -Xlog:os+container=trace -version"); + + Common.run(opts) + .shouldMatch("Lowest limit was:.*" + expectedValue); + } + + private static void testMemoryLimitSubgroupV2(String containerMemorySize, String valueToSet, String expectedValue, boolean privateNamespace) + throws Exception { + + Common.logNewTestCase("Cgroup V2 subgroup memory limit: " + valueToSet); + + DockerRunOptions opts = new DockerRunOptions(imageName, "sh", "-c"); + opts.javaOpts = new ArrayList<>(); + opts.appendTestJavaOptions = false; + opts.addDockerOpts("--privileged") + .addDockerOpts("--cgroupns=" + (privateNamespace ? "private" : "host")) + .addDockerOpts("--memory", containerMemorySize); + opts.addClassOptions("mkdir -p /sys/fs/cgroup/memory/test ; " + + "echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs ; " + + "echo '+memory' > /sys/fs/cgroup/cgroup.subtree_control ; " + + "echo '+memory' > /sys/fs/cgroup/memory/cgroup.subtree_control ; " + + "echo " + valueToSet + " > /sys/fs/cgroup/memory/test/memory.max ; " + + "/jdk/bin/java -Xlog:os+container=trace -version"); + + Common.run(opts) + .shouldMatch("Lowest limit was:.*" + expectedValue); + } +} diff --git a/test/jdk/jdk/internal/platform/cgroup/CgroupV1SubsystemControllerTest.java b/test/jdk/jdk/internal/platform/cgroup/CgroupV1SubsystemControllerTest.java index a97edd581fe..3ab8b35ae0a 100644 --- a/test/jdk/jdk/internal/platform/cgroup/CgroupV1SubsystemControllerTest.java +++ b/test/jdk/jdk/internal/platform/cgroup/CgroupV1SubsystemControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Red Hat, Inc. + * Copyright (c) 2022, 2025, Red Hat, Inc. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -64,6 +64,9 @@ public class CgroupV1SubsystemControllerTest { assertEquals(expectedPath, ctrl.path()); } + /* + * Less common cases: Containers + */ @Test public void testCgPathSubstring() { String root = "/foo/bar/baz"; @@ -71,8 +74,18 @@ public class CgroupV1SubsystemControllerTest { CgroupV1SubsystemController ctrl = new CgroupV1SubsystemController(root, mountPoint); String cgroupPath = "/foo/bar/baz/some"; ctrl.setPath(cgroupPath); - String expectedPath = mountPoint + "/some"; + String expectedPath = mountPoint; assertEquals(expectedPath, ctrl.path()); } + @Test + public void testCgPathToMovedPath() { + String root = "/system.slice/garden.service/garden/good/2f57368b-0eda-4e52-64d8-af5c"; + String mountPoint = "/sys/fs/cgroup/cpu,cpuacct"; + CgroupV1SubsystemController ctrl = new CgroupV1SubsystemController(root, mountPoint); + String cgroupPath = "/system.slice/garden.service/garden/bad/2f57368b-0eda-4e52-64d8-af5c"; + ctrl.setPath(cgroupPath); + String expectedPath = mountPoint; + assertEquals(expectedPath, ctrl.path()); + } } diff --git a/test/jdk/jdk/internal/platform/cgroup/TestCgroupSubsystemFactory.java b/test/jdk/jdk/internal/platform/cgroup/TestCgroupSubsystemFactory.java index ede74b5011e..8cf53c66e8a 100644 --- a/test/jdk/jdk/internal/platform/cgroup/TestCgroupSubsystemFactory.java +++ b/test/jdk/jdk/internal/platform/cgroup/TestCgroupSubsystemFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022, Red Hat Inc. + * Copyright (c) 2020, 2025, Red Hat Inc. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -44,6 +44,7 @@ import jdk.internal.platform.CgroupSubsystemFactory; import jdk.internal.platform.CgroupSubsystemFactory.CgroupTypeResult; import jdk.internal.platform.CgroupV1MetricsImpl; import jdk.internal.platform.cgroupv1.CgroupV1Subsystem; +import jdk.internal.platform.cgroupv1.CgroupV1SubsystemController; import jdk.internal.platform.Metrics; import jdk.test.lib.Utils; import jdk.test.lib.util.FileUtils; @@ -75,8 +76,10 @@ public class TestCgroupSubsystemFactory { private Path cgroupv1MntInfoDoubleControllers; private Path cgroupv1MntInfoDoubleControllers2; private Path cgroupv1MntInfoColonsHierarchy; + private Path cgroupv1MntInfoNonTrivialRoot; private Path cgroupv1SelfCgroup; private Path cgroupv1SelfColons; + private Path cgroupv1SelfNonTrivialRoot; private Path cgroupv2SelfCgroup; private Path cgroupv1SelfCgroupJoinCtrl; private Path cgroupv1CgroupsOnlyCPUCtrl; @@ -175,6 +178,7 @@ public class TestCgroupSubsystemFactory { "42 30 0:38 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:14 - cgroup none rw,seclabel,cpuset\n" + "43 30 0:39 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:15 - cgroup none rw,seclabel,blkio\n" + "44 30 0:40 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:16 - cgroup none rw,seclabel,freezer\n"; + private String mntInfoNonTrivialRoot = "2207 2196 0:43 /system.slice/garden.service/garden/good/2f57368b-0eda-4e52-64d8-af5c /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpu,cpuacct\n"; private String cgroupsNonZeroHierarchy = "#subsys_name hierarchy num_cgroups enabled\n" + "cpuset 9 1 1\n" + @@ -230,6 +234,7 @@ public class TestCgroupSubsystemFactory { "2:cpu,cpuacct:/\n" + "1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-3c00b338-5b65-439f-8e97-135e183d135d.scope\n" + "0::/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-3c00b338-5b65-439f-8e97-135e183d135d.scope\n"; + private String cgroupv1SelfNTRoot = "11:cpu,cpuacct:/system.slice/garden.service/garden/bad/2f57368b-0eda-4e52-64d8-af5c\n"; private String cgroupv2SelfCgroupContent = "0::/user.slice/user-1000.slice/session-2.scope"; // We have a mix of V1 and V2 controllers, but none of the V1 controllers @@ -294,12 +299,18 @@ public class TestCgroupSubsystemFactory { cgroupv1MntInfoColonsHierarchy = Paths.get(existingDirectory.toString(), "mountinfo_colons"); Files.writeString(cgroupv1MntInfoColonsHierarchy, mntInfoColons); + cgroupv1MntInfoNonTrivialRoot = Paths.get(existingDirectory.toString(), "mountinfo_nt_root"); + Files.writeString(cgroupv1MntInfoNonTrivialRoot, mntInfoNonTrivialRoot); + cgroupv1SelfCgroup = Paths.get(existingDirectory.toString(), "self_cgroup_cgv1"); Files.writeString(cgroupv1SelfCgroup, cgroupv1SelfCgroupContent); cgroupv1SelfColons = Paths.get(existingDirectory.toString(), "self_colons_cgv1"); Files.writeString(cgroupv1SelfColons, cgroupv1SelfColonsContent); + cgroupv1SelfNonTrivialRoot = Paths.get(existingDirectory.toString(), "self_nt_root_cgv1"); + Files.writeString(cgroupv1SelfNonTrivialRoot, cgroupv1SelfNTRoot); + cgroupv2SelfCgroup = Paths.get(existingDirectory.toString(), "self_cgroup_cgv2"); Files.writeString(cgroupv2SelfCgroup, cgroupv2SelfCgroupContent); @@ -449,6 +460,27 @@ public class TestCgroupSubsystemFactory { assertEquals(memoryInfo.getMountRoot(), memoryInfo.getCgroupPath()); } + @Test + public void testMountPrefixCgroupsV1() throws IOException { + String cgroups = cgroupv1CgInfoNonZeroHierarchy.toString(); + String mountInfo = cgroupv1MntInfoNonTrivialRoot.toString(); + String selfCgroup = cgroupv1SelfNonTrivialRoot.toString(); + Optional result = CgroupSubsystemFactory.determineType(mountInfo, cgroups, selfCgroup); + + assertTrue("Expected non-empty cgroup result", result.isPresent()); + CgroupTypeResult res = result.get(); + CgroupInfo cpuInfo = res.getInfos().get("cpu"); + assertEquals(cpuInfo.getCgroupPath(), "/system.slice/garden.service/garden/bad/2f57368b-0eda-4e52-64d8-af5c"); + String expectedMountPoint = "/sys/fs/cgroup/cpu,cpuacct"; + assertEquals(expectedMountPoint, cpuInfo.getMountPoint()); + CgroupV1SubsystemController cgroupv1MemoryController = new CgroupV1SubsystemController(cpuInfo.getMountRoot(), cpuInfo.getMountPoint()); + cgroupv1MemoryController.setPath(cpuInfo.getCgroupPath()); + String actualPath = cgroupv1MemoryController.path(); + assertNotNull(actualPath); + String expectedPath = expectedMountPoint; + assertEquals("Should be equal to the mount point path", expectedPath, actualPath); + } + @Test public void testZeroHierarchyCgroupsV1() throws IOException { String cgroups = cgroupv1CgInfoZeroHierarchy.toString(); diff --git a/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetricsSubgroup.java b/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetricsSubgroup.java new file mode 100644 index 00000000000..2ac79c173ef --- /dev/null +++ b/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetricsSubgroup.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025, BELLSOFT. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.platform.Metrics; +import jdk.test.lib.Utils; +import jdk.test.lib.containers.docker.Common; +import jdk.test.lib.containers.docker.DockerfileConfig; +import jdk.test.lib.containers.docker.DockerRunOptions; +import jdk.test.lib.containers.docker.DockerTestUtils; + +import java.util.ArrayList; + +import jtreg.SkippedException; + +/* + * @test + * @bug 8343191 + * @key cgroups + * @summary Cgroup v1 subsystem fails to set subsystem path + * @requires container.support + * @library /test/lib + * @modules java.base/jdk.internal.platform + * @build MetricsMemoryTester + * @run main TestDockerMemoryMetricsSubgroup + */ + +public class TestDockerMemoryMetricsSubgroup { + private static final String imageName = + DockerfileConfig.getBaseImageName() + ":" + + DockerfileConfig.getBaseImageVersion(); + + public static void main(String[] args) throws Exception { + Metrics metrics = Metrics.systemMetrics(); + if (metrics == null) { + System.out.println("Cgroup not configured."); + return; + } + if (!DockerTestUtils.canTestDocker()) { + System.out.println("Unable to run docker tests."); + return; + } + if ("cgroupv1".equals(metrics.getProvider())) { + testMemoryLimitSubgroupV1("200m", "400m", false); + testMemoryLimitSubgroupV1("500m", "1G", false); + testMemoryLimitSubgroupV1("200m", "400m", true); + testMemoryLimitSubgroupV1("500m", "1G", true); + } else if ("cgroupv2".equals(metrics.getProvider())) { + testMemoryLimitSubgroupV2("200m", "400m", false); + testMemoryLimitSubgroupV2("500m", "1G", false); + testMemoryLimitSubgroupV2("200m", "400m", true); + testMemoryLimitSubgroupV2("500m", "1G", true); + } else { + throw new SkippedException("Metrics are from neither cgroup v1 nor v2, skipped for now."); + } + } + + private static void testMemoryLimitSubgroupV1(String innerSize, String outerGroupMemorySize, boolean privateNamespace) throws Exception { + Common.logNewTestCase("testMemoryLimitSubgroup, innerSize = " + innerSize); + DockerRunOptions opts = + new DockerRunOptions(imageName, "sh", "-c"); + opts.javaOpts = new ArrayList<>(); + opts.appendTestJavaOptions = false; + opts.addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/") + .addDockerOpts("--volume", Utils.TEST_JDK + ":/jdk") + .addDockerOpts("--privileged") + .addDockerOpts("--cgroupns=" + (privateNamespace ? "private" : "host")) + .addDockerOpts("--memory", outerGroupMemorySize); + opts.addClassOptions("mkdir -p /sys/fs/cgroup/memory/test ; " + + "echo " + innerSize + " > /sys/fs/cgroup/memory/test/memory.limit_in_bytes ; " + + "echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs ; " + + "/jdk/bin/java -cp /test-classes/ " + + "--add-exports java.base/jdk.internal.platform=ALL-UNNAMED " + + "MetricsMemoryTester memory " + innerSize); + + DockerTestUtils.dockerRunJava(opts).shouldHaveExitValue(0).shouldContain("TEST PASSED!!!"); + } + + private static void testMemoryLimitSubgroupV2(String innerSize, String outerGroupMemorySize, boolean privateNamespace) throws Exception { + Common.logNewTestCase("testMemoryLimitSubgroup, innerSize = " + innerSize); + DockerRunOptions opts = + new DockerRunOptions(imageName, "sh", "-c"); + opts.javaOpts = new ArrayList<>(); + opts.appendTestJavaOptions = false; + opts.addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/") + .addDockerOpts("--volume", Utils.TEST_JDK + ":/jdk") + .addDockerOpts("--privileged") + .addDockerOpts("--cgroupns=" + (privateNamespace ? "private" : "host")) + .addDockerOpts("--memory", outerGroupMemorySize); + opts.addClassOptions("mkdir -p /sys/fs/cgroup/memory/test ; " + + "echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs ; " + + "echo '+memory' > /sys/fs/cgroup/cgroup.subtree_control ; " + + "echo '+memory' > /sys/fs/cgroup/memory/cgroup.subtree_control ; " + + "echo " + innerSize + " > /sys/fs/cgroup/memory/test/memory.max ; " + + "/jdk/bin/java -cp /test-classes/ " + + "--add-exports java.base/jdk.internal.platform=ALL-UNNAMED " + + "MetricsMemoryTester memory " + innerSize); + + DockerTestUtils.dockerRunJava(opts).shouldHaveExitValue(0).shouldContain("TEST PASSED!!!"); + } +}