From 86f8e1b82133f667d28c48e7410d9fb83b1d56b9 Mon Sep 17 00:00:00 2001 From: Severin Gehwolf Date: Fri, 7 Nov 2025 11:59:02 +0100 Subject: [PATCH 1/2] 8370966: Create regression test for hierarchical memory limit fix in JDK-8370572 --- .../containers/docker/TestJFRWithJMX.java | 8 +- .../jtreg/containers/docker/TestJcmd.java | 2 +- .../docker/TestMemoryInvisibleParent.java | 117 ++++++++++++++++++ .../docker/TestMemoryWithSubgroups.java | 15 +-- .../jtreg/containers/docker/TestPids.java | 2 +- .../TestDockerMemoryMetricsSubgroup.java | 15 +-- .../containers/docker/DockerRunOptions.java | 6 + .../containers/docker/DockerTestUtils.java | 31 +++++ 8 files changed, 159 insertions(+), 37 deletions(-) create mode 100644 test/hotspot/jtreg/containers/docker/TestMemoryInvisibleParent.java diff --git a/test/hotspot/jtreg/containers/docker/TestJFRWithJMX.java b/test/hotspot/jtreg/containers/docker/TestJFRWithJMX.java index a9de46e00b0a4..efe1fa4ffbcc9 100644 --- a/test/hotspot/jtreg/containers/docker/TestJFRWithJMX.java +++ b/test/hotspot/jtreg/containers/docker/TestJFRWithJMX.java @@ -77,7 +77,7 @@ public static void main(String[] args) throws Exception { throw new SkippedException("Docker is not supported on this host"); } - if (isPodman() & !Platform.isRoot()) { + if (DockerTestUtils.isPodman() & !Platform.isRoot()) { throw new SkippedException("test cannot be run under rootless podman configuration"); } @@ -222,10 +222,4 @@ static File transferRecording(FlightRecorderMXBean bean, long streamId) throws E } } - static boolean isPodman() { - String[] parts = Container.ENGINE_COMMAND - .toLowerCase() - .split(File.pathSeparator); - return "podman".equals(parts[parts.length - 1]); - } } diff --git a/test/hotspot/jtreg/containers/docker/TestJcmd.java b/test/hotspot/jtreg/containers/docker/TestJcmd.java index 4b604096b00b6..8c210544bb68c 100644 --- a/test/hotspot/jtreg/containers/docker/TestJcmd.java +++ b/test/hotspot/jtreg/containers/docker/TestJcmd.java @@ -57,7 +57,7 @@ public class TestJcmd { private static final String IMAGE_NAME = Common.imageName("jcmd"); private static final int TIME_TO_RUN_CONTAINER_PROCESS = (int) (10 * Utils.TIMEOUT_FACTOR); // seconds private static final String CONTAINER_NAME = "test-container"; - private static final boolean IS_PODMAN = Container.ENGINE_COMMAND.contains("podman"); + private static final boolean IS_PODMAN = DockerTestUtils.isPodman(); private static final String ROOT_UID = "0"; diff --git a/test/hotspot/jtreg/containers/docker/TestMemoryInvisibleParent.java b/test/hotspot/jtreg/containers/docker/TestMemoryInvisibleParent.java new file mode 100644 index 0000000000000..9f1fe315e866c --- /dev/null +++ b/test/hotspot/jtreg/containers/docker/TestMemoryInvisibleParent.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2025, IBM + * 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.Container; +import jdk.test.lib.containers.docker.Common; +import jdk.test.lib.containers.docker.DockerTestUtils; +import jdk.test.lib.containers.docker.ContainerRuntimeVersionTestUtils; +import jdk.test.lib.containers.docker.DockerRunOptions; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.internal.platform.Metrics; + +import java.nio.file.Path; +import java.nio.file.Files; +import java.util.ArrayList; + +import jtreg.SkippedException; + +/* + * @test + * @bug 8370966 + * @requires os.family == "linux" + * @requires !vm.asan + * @modules java.base/jdk.internal.platform + * @library /test/lib + * @run main TestMemoryInvisibleParent + */ +public class TestMemoryInvisibleParent { + private static final String UNLIMITED = "-1"; + private static final String imageName = Common.imageName("invisible-parent"); + + 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; + } + + ContainerRuntimeVersionTestUtils.checkContainerVersionSupported(); + + if (DockerTestUtils.isRootless()) { + throw new SkippedException("Test skipped in rootless mode"); + } + DockerTestUtils.buildJdkContainerImage(imageName); + + if ("cgroupv1".equals(metrics.getProvider())) { + try { + testMemoryLimitHiddenParent("104857600", "104857600"); + testMemoryLimitHiddenParent("209715200", "209715200"); + } finally { + DockerTestUtils.removeDockerImage(imageName); + } + } else { + throw new SkippedException("cgroup v1 - only test! This is " + metrics.getProvider()); + } + } + + private static void testMemoryLimitHiddenParent(String valueToSet, String expectedValue) + throws Exception { + + Common.logNewTestCase("Cgroup V1 hidden parent memory limit: " + valueToSet); + + try { + String cgroupParent = setParentWithLimit(valueToSet); + DockerRunOptions opts = new DockerRunOptions(imageName, "/jdk/bin/java", "-version", "-Xlog:os+container=trace"); + opts.appendTestJavaOptions = false; + if (DockerTestUtils.isPodman()) { + // Podman needs to run this test with engine option --cgroup-manager=cgroupfs + opts.addEngineOpts("--cgroup-manager", "cgroupfs"); + } + opts.addDockerOpts("--cgroup-parent=/" + cgroupParent); + Common.run(opts) + .shouldContain("Hierarchical Memory Limit is: " + expectedValue); + } finally { + // Reset the parent memory limit to unlimited (-1) + setParentWithLimit(UNLIMITED); + } + } + + private static String setParentWithLimit(String memLimit) throws Exception { + String cgroupParent = "hidden-parent-" + TestMemoryInvisibleParent.class.getSimpleName() + Runtime.version().feature(); + Path sysFsMemory = Path.of("/", "sys", "fs", "cgroup", "memory"); + Path cgroupParentPath = sysFsMemory.resolve(cgroupParent); + ProcessBuilder pb = new ProcessBuilder("mkdir", "-p", cgroupParentPath.toString()); + OutputAnalyzer out = new OutputAnalyzer(pb.start()) + .shouldHaveExitValue(0); + Path memoryLimitsFile = cgroupParentPath.resolve("memory.limit_in_bytes"); + Files.writeString(memoryLimitsFile, memLimit); + System.out.println("Cgroup parent is: /" + cgroupParentPath.getFileName() + + " at " + sysFsMemory.toString()); + return cgroupParent; + } + +} diff --git a/test/hotspot/jtreg/containers/docker/TestMemoryWithSubgroups.java b/test/hotspot/jtreg/containers/docker/TestMemoryWithSubgroups.java index bd1713e578c4a..3b901765ee9ba 100644 --- a/test/hotspot/jtreg/containers/docker/TestMemoryWithSubgroups.java +++ b/test/hotspot/jtreg/containers/docker/TestMemoryWithSubgroups.java @@ -46,19 +46,6 @@ public class TestMemoryWithSubgroups { private static final String imageName = Common.imageName("subgroup"); - static String getEngineInfo(String format) throws Exception { - return DockerTestUtils.execute(Container.ENGINE_COMMAND, "info", "-f", format) - .getStdout(); - } - - static boolean isRootless() throws Exception { - // Docker and Podman have different INFO structures. - // The node path for Podman is .Host.Security.Rootless, that also holds for - // Podman emulating Docker CLI. The node path for Docker is .SecurityOptions. - return (getEngineInfo("{{.Host.Security.Rootless}}").contains("true") || - getEngineInfo("{{.SecurityOptions}}").contains("name=rootless")); - } - public static void main(String[] args) throws Exception { Metrics metrics = Metrics.systemMetrics(); if (metrics == null) { @@ -72,7 +59,7 @@ public static void main(String[] args) throws Exception { ContainerRuntimeVersionTestUtils.checkContainerVersionSupported(); - if (isRootless()) { + if (DockerTestUtils.isRootless()) { throw new SkippedException("Test skipped in rootless mode"); } Common.prepareWhiteBox(); diff --git a/test/hotspot/jtreg/containers/docker/TestPids.java b/test/hotspot/jtreg/containers/docker/TestPids.java index 2e9c97110b2a4..62bd70dc61fb4 100644 --- a/test/hotspot/jtreg/containers/docker/TestPids.java +++ b/test/hotspot/jtreg/containers/docker/TestPids.java @@ -47,7 +47,7 @@ public class TestPids { private static final String imageName = Common.imageName("pids"); - private static final boolean IS_PODMAN = Container.ENGINE_COMMAND.contains("podman"); + private static final boolean IS_PODMAN = DockerTestUtils.isPodman(); private static final int UNLIMITED_PIDS_PODMAN = 0; private static final int UNLIMITED_PIDS_DOCKER = -1; diff --git a/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetricsSubgroup.java b/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetricsSubgroup.java index 53f11f22d79ba..f9dd405891c61 100644 --- a/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetricsSubgroup.java +++ b/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetricsSubgroup.java @@ -52,19 +52,6 @@ public class TestDockerMemoryMetricsSubgroup { DockerfileConfig.getBaseImageName() + ":" + DockerfileConfig.getBaseImageVersion(); - static String getEngineInfo(String format) throws Exception { - return DockerTestUtils.execute(Container.ENGINE_COMMAND, "info", "-f", format) - .getStdout(); - } - - static boolean isRootless() throws Exception { - // Docker and Podman have different INFO structures. - // The node path for Podman is .Host.Security.Rootless, that also holds for - // Podman emulating Docker CLI. The node path for Docker is .SecurityOptions. - return (getEngineInfo("{{.Host.Security.Rootless}}").contains("true") || - getEngineInfo("{{.SecurityOptions}}").contains("name=rootless")); - } - public static void main(String[] args) throws Exception { Metrics metrics = Metrics.systemMetrics(); if (metrics == null) { @@ -78,7 +65,7 @@ public static void main(String[] args) throws Exception { ContainerRuntimeVersionTestUtils.checkContainerVersionSupported(); - if (isRootless()) { + if (DockerTestUtils.isRootless()) { throw new SkippedException("Test skipped in rootless mode"); } diff --git a/test/lib/jdk/test/lib/containers/docker/DockerRunOptions.java b/test/lib/jdk/test/lib/containers/docker/DockerRunOptions.java index 1942ae734d9cd..b9ea60c9fb75e 100644 --- a/test/lib/jdk/test/lib/containers/docker/DockerRunOptions.java +++ b/test/lib/jdk/test/lib/containers/docker/DockerRunOptions.java @@ -31,6 +31,7 @@ // in test environment. public class DockerRunOptions { public String imageNameAndTag; + public ArrayList engineOpts = new ArrayList<>(); public ArrayList dockerOpts = new ArrayList<>(); public String command; // normally a full path to java public ArrayList javaOpts = new ArrayList<>(); @@ -70,6 +71,11 @@ public final DockerRunOptions addDockerOpts(String... opts) { return this; } + public final DockerRunOptions addEngineOpts(String... opts) { + Collections.addAll(engineOpts, opts); + return this; + } + public final DockerRunOptions addJavaOpts(String... opts) { Collections.addAll(javaOpts, opts); return this; diff --git a/test/lib/jdk/test/lib/containers/docker/DockerTestUtils.java b/test/lib/jdk/test/lib/containers/docker/DockerTestUtils.java index f2f744240f033..422671d65b70f 100644 --- a/test/lib/jdk/test/lib/containers/docker/DockerTestUtils.java +++ b/test/lib/jdk/test/lib/containers/docker/DockerTestUtils.java @@ -83,6 +83,14 @@ public static boolean isDockerEngineAvailable() throws Exception { return isDockerEngineAvailable; } + /** + * Checks if the actual engine command is podman. + * + * @return {@code true} if engine is podman. {@code false} otherwise. + */ + public static boolean isPodman() { + return Container.ENGINE_COMMAND.contains("podman"); + } /** * Convenience method, will check if docker engine is available and usable; @@ -121,6 +129,26 @@ private static boolean isDockerEngineAvailableCheck() throws Exception { return true; } + private static String getEngineInfo(String format) throws Exception { + return execute(Container.ENGINE_COMMAND, "info", "-f", format).getStdout(); + } + + /** + * Determine if the engine is running in root-less mode. + * + * @return {@code true} when running root-less (podman or docker). {@code false} + * otherwise. + * + * @throws Exception + */ + public static boolean isRootless() throws Exception { + // Docker and Podman have different INFO structures. + // The node path for Podman is .Host.Security.Rootless, that also holds for + // Podman emulating Docker CLI. The node path for Docker is .SecurityOptions. + return (getEngineInfo("{{.Host.Security.Rootless}}").contains("true") || + getEngineInfo("{{.SecurityOptions}}").contains("name=rootless")); + } + /** * Build a container image that contains JDK under test. * The jdk will be placed under the "/jdk/" folder inside the image/container file system. @@ -202,6 +230,9 @@ private static void buildImage(String imageName, Path buildDir) throws Exception */ public static List buildJavaCommand(DockerRunOptions opts) throws Exception { List cmd = buildContainerCommand(); + if (!opts.engineOpts.isEmpty()) { + cmd.addAll(opts.engineOpts); + } cmd.add("run"); if (opts.tty) cmd.add("--tty=true"); From faeff2a1246e61ea6ab3414061159352b84fa68d Mon Sep 17 00:00:00 2001 From: Severin Gehwolf Date: Mon, 10 Nov 2025 18:36:55 +0100 Subject: [PATCH 2/2] Indenting and copyright --- .../jtreg/containers/docker/TestMemoryInvisibleParent.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hotspot/jtreg/containers/docker/TestMemoryInvisibleParent.java b/test/hotspot/jtreg/containers/docker/TestMemoryInvisibleParent.java index 9f1fe315e866c..208dd603615f9 100644 --- a/test/hotspot/jtreg/containers/docker/TestMemoryInvisibleParent.java +++ b/test/hotspot/jtreg/containers/docker/TestMemoryInvisibleParent.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025, IBM + * Copyright (C) 2025, IBM Corporation. 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 @@ -93,7 +93,7 @@ private static void testMemoryLimitHiddenParent(String valueToSet, String expect } opts.addDockerOpts("--cgroup-parent=/" + cgroupParent); Common.run(opts) - .shouldContain("Hierarchical Memory Limit is: " + expectedValue); + .shouldContain("Hierarchical Memory Limit is: " + expectedValue); } finally { // Reset the parent memory limit to unlimited (-1) setParentWithLimit(UNLIMITED);