RFR: 8370572: Cgroups hierarchical memory limit is not honored after JDK-8322420 [v2]
Volker Simonis
simonis at openjdk.org
Wed Oct 29 17:21:30 UTC 2025
On Wed, 29 Oct 2025 12:01:22 GMT, Aleksey Shipilev <shade at openjdk.org> wrote:
>>> I tried to come up with a regression test for it, but could not: local reproducers require amending _host_ configuration, which requires superuser privileges, among other hassle it introduces.
>>
>> Without a proper regression test this is bound to fall through the cracks again. So are you sure this cannot be tested? It should be fine if the test needs root privileges (we could skip it if not root). But it would be better than not having one.
>
>> Without a proper regression test this is bound to fall through the cracks again. So are you sure this cannot be tested? It should be fine if the test needs root privileges (we could skip it if not root). But it would be better than not having one.
>
> Yes, I tried to write a test, but it was not simple at all. AFAICS, you need to configure the _host_ in a particular way to get to the interesting configuration, when part of hierarchy is hidden. So not only it would require root, it would also make changes to the host cgroup config (and properly revert them at the end of testing!). It would be better if we could come up with something like Docker-in-Docker kind of test, but that is probably a headache as well.
>
> Anyway, we are dealing with the real-world, customer-facing breakage here, so I reasoned it was unwise to delay the immediately deployable fix, just because it was unclear how to write a reliable regression test for it :)
So here's a simple test (based on [`test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetrics.java`](https://github.com/openjdk/jdk/blob/28f2591bad49c4d1590325c3d315d850ab6bcc7d/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetrics.java) which basically implements @shipilev Docker reproducer. It only runs if the user is `root` or a `sudo` user and if the system supports cgroup v1. The test fails with the current implementation and succeeds with the patch proposed in this PR.
/*
* 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
* 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 java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import jdk.internal.platform.Metrics;
import jdk.test.lib.Utils;
import jdk.test.lib.containers.docker.Common;
import jdk.test.lib.containers.docker.DockerRunOptions;
import jdk.test.lib.containers.docker.DockerTestUtils;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jtreg.SkippedException;
/*
* @test
* @key cgroups
* @bug 8370572
* @summary Test that the Cgroups hierarchical memory limit is honored
* @requires container.support
* @requires !vm.asan
* @library /test/lib
* @modules java.base/jdk.internal.platform
* @run main/timeout=360 TestHierarchicalMemoryLimit
*/
public class TestHierarchicalMemoryLimit {
private static final String imageName = Common.imageName("metrics-memory");
private static final long memLimit = 1024 * 1024 * 1024; // Should probably be a multiple of page size
private static final String memLimitString = String.valueOf(memLimit);
public static void main(String[] args) throws Exception {
if (!DockerTestUtils.canTestDocker()) {
return;
}
try {
OutputAnalyzer oa = ProcessTools.executeProcess("sudo", "-n", "mkdir", "/sys/fs/cgroup/memory/jtreg-test-parent");
if (oa.getExitValue() != 0) {
if (oa.getStderr().contains("Permission denied")) {
throw new SkippedException("Must be root or sudo user for this test. (original error was: " + oa.getStderr() + ")");
} else {
throw new SkippedException("Must have cgroup v1 for this test. (original error was: " + oa.getStderr() + ")");
}
}
Path setMemoryLimit = Files.createTempFile("setMemoryLimit", ".sh");
setMemoryLimit.toFile().deleteOnExit();
try (BufferedWriter writer = Files.newBufferedWriter(setMemoryLimit)) {
writer.write("echo " + memLimitString + " > /sys/fs/cgroup/memory/jtreg-test-parent/memory.limit_in_bytes");
setMemoryLimit.toFile().setExecutable(true);
}
oa = ProcessTools.executeProcess("sudo", "-n", setMemoryLimit.toString());
if (oa.getExitValue() != 0) {
throw new SkippedException("Can't set jtreg-test-parent/memory.limit_in_bytes. (original error was: " + oa.getStderr() + ")");
}
// These tests create a docker image and run this image with
// varying docker memory options. The arguments passed to the docker
// container include the Java test class to be run along with the
// resource to be examined and expected result.
DockerTestUtils.buildJdkContainerImage(imageName);
try {
testHierarchicalMemoryLimit(memLimitString);
} finally {
if (!DockerTestUtils.RETAIN_IMAGE_AFTER_TEST) {
DockerTestUtils.removeDockerImage(imageName);
}
}
} finally {
ProcessTools.executeProcess("sudo", "-n", "rmdir", "/sys/fs/cgroup/memory/jtreg-test-parent");
}
}
private static void testHierarchicalMemoryLimit(String value) throws Exception {
Common.logNewTestCase("testHierarchicalMemoryLimit, value = " + value);
DockerRunOptions opts =
new DockerRunOptions(imageName, "/jdk/bin/java", "-version");
opts.addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/")
.addDockerOpts("--cgroup-parent=/jtreg-test-parent")
.addJavaOpts("-cp", "/test-classes/")
.addJavaOpts("--add-exports", "java.base/jdk.internal.platform=ALL-UNNAMED", "-Xlog:os+container=trace", "-XX:InitialRAMPercentage=25", "-XX:MaxRAMPercentage=25", "-Xlog:gc+init");
OutputAnalyzer oa = DockerTestUtils.dockerRunJava(opts).shouldHaveExitValue(0);
String gcMemoryLine = oa.asLines().stream()
.filter(line -> line.contains("gc,init"))
.filter(line -> line.contains("Memory"))
.findFirst().get();
String gcMemory = gcMemoryLine.substring(gcMemoryLine.lastIndexOf(" ") + 1, gcMemoryLine.length() - 1);
char gcUnit = gcMemoryLine.charAt(gcMemoryLine.length() - 1);
System.out.println(gcMemory + gcUnit);
long unit = 1;
switch (gcUnit) {
case 'K' : unit = 1024; break;
case 'M' : unit = 1024 * 1024; break;
case 'G' : unit = 1024 * 1024 * 1024; break;
}
long memory = Integer.parseInt(gcMemory) * unit;
if (memLimit != memory) {
throw new Exception("Pysical memory should be " + memLimit + " but was " + memory);
}
}
}
-------------
PR Comment: https://git.openjdk.org/jdk/pull/28006#issuecomment-3462760365
More information about the hotspot-runtime-dev
mailing list