This is an automated email from the ASF dual-hosted git repository. mapohl pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/flink.git
commit ac910542ccb78108cb130fe2368e97557afd9cc6 Author: Matthias Pohl <matthias.p...@aiven.io> AuthorDate: Thu Feb 15 15:19:15 2024 +0100 [FLINK-22765][test] Hardens ExceptionUtilsITCase#testIsMetaspaceOutOfMemoryError in JDK21 This test started to fail quite regularly in JDK21. The problem was that the low heap size could have caused an OutOfMemoryError to appear when compiling the dummy classes. An OOM in the compilation phase results in a different error message being printed to stdout that wasn't captured by the test. The solution is to pre-compile the classes upfront (with the normal heap size). The test main method will only load the classes. No compilation is necessary. --- .../apache/flink/testutils/ClassLoaderUtils.java | 41 +++++++++- .../flink/runtime/util/ExceptionUtilsITCase.java | 87 +++++++++++++++------- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/flink-core/src/test/java/org/apache/flink/testutils/ClassLoaderUtils.java b/flink-core/src/test/java/org/apache/flink/testutils/ClassLoaderUtils.java index 2a3f320e107..c4ca8a2b529 100644 --- a/flink-core/src/test/java/org/apache/flink/testutils/ClassLoaderUtils.java +++ b/flink-core/src/test/java/org/apache/flink/testutils/ClassLoaderUtils.java @@ -18,6 +18,8 @@ package org.apache.flink.testutils; +import org.apache.flink.util.Preconditions; + import javax.annotation.Nullable; import javax.tools.JavaCompiler; import javax.tools.ToolProvider; @@ -40,6 +42,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; /** Utilities to create class loaders. */ @@ -142,6 +145,15 @@ public class ClassLoaderUtils { return this; } + public ClassLoaderBuilder addClass(String className) { + Preconditions.checkState( + new File(root, className + ".java").exists(), + "The class %s was added without any source code being present.", + className); + + return addClass(className, null); + } + public ClassLoaderBuilder addClass(String className, String source) { String oldValue = classes.putIfAbsent(className, source); @@ -158,7 +170,7 @@ public class ClassLoaderUtils { return this; } - public URLClassLoader build() throws IOException { + public void generateSourcesAndCompile() throws IOException { for (Map.Entry<String, String> classInfo : classes.entrySet()) { writeAndCompile(root, createFileName(classInfo.getKey()), classInfo.getValue()); } @@ -171,10 +183,37 @@ public class ClassLoaderUtils { for (Map.Entry<String, String> resource : resources.entrySet()) { writeSourceFile(root, resource.getKey(), resource.getValue()); } + } + + public URLClassLoader buildWithoutCompilation() throws MalformedURLException { + final int generatedSourceClassesCount = + Objects.requireNonNull( + root.listFiles( + (dir, name) -> { + if (!name.endsWith(".java")) { + return false; + } + final String derivedClassName = + name.substring(0, name.lastIndexOf('.')); + return classes.containsKey(derivedClassName); + })) + .length; + Preconditions.checkState( + generatedSourceClassesCount == classes.size(), + "The generated Java sources in %s (%s) do not match the classes in this %s (%s).", + root.getAbsolutePath(), + generatedSourceClassesCount, + ClassLoaderBuilder.class.getSimpleName(), + classes.size()); return createClassLoader(root, parent); } + public URLClassLoader build() throws IOException { + generateSourcesAndCompile(); + return buildWithoutCompilation(); + } + private String createFileName(String className) { return className + ".java"; } diff --git a/flink-tests/src/test/java/org/apache/flink/runtime/util/ExceptionUtilsITCase.java b/flink-tests/src/test/java/org/apache/flink/runtime/util/ExceptionUtilsITCase.java index ef151d100b4..e383f7667be 100644 --- a/flink-tests/src/test/java/org/apache/flink/runtime/util/ExceptionUtilsITCase.java +++ b/flink-tests/src/test/java/org/apache/flink/runtime/util/ExceptionUtilsITCase.java @@ -35,11 +35,10 @@ import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.text.IsEmptyString.isEmptyString; import static org.junit.Assert.assertThat; /** Tests for {@link ExceptionUtils} which require to spawn JVM process and set JVM memory args. */ @@ -55,26 +54,63 @@ public class ExceptionUtilsITCase extends TestLogger { @Test public void testIsDirectOutOfMemoryError() throws IOException, InterruptedException { String className = DummyDirectAllocatingProgram.class.getName(); - RunResult result = run(className, Collections.emptyList(), DIRECT_MEMORY_SIZE, -1); + RunResult result = run(className, DIRECT_MEMORY_SIZE, -1); assertThat(result.getErrorOut() + "|" + result.getStandardOut(), is("|")); } @Test public void testIsMetaspaceOutOfMemoryError() throws IOException, InterruptedException { String className = DummyClassLoadingProgram.class.getName(); + final File compiledClassesFolder = TEMPORARY_FOLDER.getRoot(); + final int classCount = 10; + + // compile the classes first + final String sourcePattern = + "public class %s { @Override public String toString() { return \"dummy\"; } }"; + final ClassLoaderBuilder classLoaderBuilder = + ClassLoaderUtils.withRoot(compiledClassesFolder); + for (int i = 0; i < classCount; i++) { + final String dummyClassName = "DummyClass" + i; + final String source = String.format(sourcePattern, dummyClassName); + classLoaderBuilder.addClass(dummyClassName, source); + } + classLoaderBuilder.generateSourcesAndCompile(); + // load only one class and record required Metaspace RunResult normalOut = - run(className, getDummyClassLoadingProgramArgs(1), -1, INITIAL_BIG_METASPACE_SIZE); - long okMetaspace = Long.parseLong(normalOut.getStandardOut()); + run( + className, + -1, + INITIAL_BIG_METASPACE_SIZE, + 1, + compiledClassesFolder.getAbsolutePath()); + + // multiply the Metaspace size to stabilize the test - relying solely on the Metaspace size + // of the initial run might cause OOMs to appear in the main thread (due to JVM-specific + // artifacts being loaded) + long okMetaspace = 3 * Long.parseLong(normalOut.getStandardOut()); + assertThat("No error is expected here.", normalOut.getErrorOut(), is("")); + // load more classes to cause 'OutOfMemoryError: Metaspace' - RunResult oomOut = run(className, getDummyClassLoadingProgramArgs(1000), -1, okMetaspace); - // 'OutOfMemoryError: Metaspace' errors are caught, hence no output means we produced the - // expected exception. - assertThat(oomOut.getErrorOut() + "|" + oomOut.getStandardOut(), is("|")); + RunResult oomOut = + run( + className, + -1, + okMetaspace, + classCount, + compiledClassesFolder.getAbsolutePath()); + assertThat( + "OutOfMemoryError: Metaspace errors are caught and don't generate any output.", + oomOut.getStandardOut(), + isEmptyString()); + assertThat("Nothing should have been printed to stderr.", oomOut.getErrorOut(), is("")); } private static RunResult run( - String className, Iterable<String> args, long directMemorySize, long metaspaceSize) + String className, + long directMemorySize, + long metaspaceSize, + Object... mainClassParameters) throws InterruptedException, IOException { TestProcessBuilder taskManagerProcessBuilder = new TestProcessBuilder(className); if (directMemorySize > 0) { @@ -86,9 +122,11 @@ public class ExceptionUtilsITCase extends TestLogger { taskManagerProcessBuilder.addJvmArg( String.format("-XX:MaxMetaspaceSize=%d", metaspaceSize)); } - for (String arg : args) { - taskManagerProcessBuilder.addMainClassArg(arg); + + for (Object parameterValue : mainClassParameters) { + taskManagerProcessBuilder.addMainClassArg(String.valueOf(parameterValue)); } + // JAVA_TOOL_OPTIONS is configured on CI which would affect the process output taskManagerProcessBuilder.withCleanEnvironment(); TestProcess p = taskManagerProcessBuilder.start(); @@ -115,12 +153,6 @@ public class ExceptionUtilsITCase extends TestLogger { } } - private static Collection<String> getDummyClassLoadingProgramArgs(int numberOfLoadedClasses) { - return Arrays.asList( - Integer.toString(numberOfLoadedClasses), - TEMPORARY_FOLDER.getRoot().getAbsolutePath()); - } - /** Dummy java program to generate Direct OOM. */ public static class DummyDirectAllocatingProgram { private DummyDirectAllocatingProgram() {} @@ -158,8 +190,10 @@ public class ExceptionUtilsITCase extends TestLogger { for (int index = 0; index < numberOfLoadedClasses; index++) { classes.add(loadDummyClass(index, args[1])); } - String out = classes.size() > 1 ? "Exception is not thrown, metaspace usage: " : ""; - output(out + getMetaspaceUsage()); + + if (classes.size() == 1) { + output(String.valueOf(getMetaspaceUsage())); + } } catch (Throwable t) { if (ExceptionUtils.isMetaspaceOutOfMemoryError(t)) { return; @@ -170,14 +204,11 @@ public class ExceptionUtilsITCase extends TestLogger { private static Class<?> loadDummyClass(int index, String folderToSaveSource) throws ClassNotFoundException, IOException { - String className = "DummyClass" + index; - String sourcePattern = - "public class %s { @Override public String toString() { return \"%s\"; } }"; - ClassLoaderBuilder classLoaderBuilder = - ClassLoaderUtils.withRoot(new File(folderToSaveSource)); - classLoaderBuilder.addClass( - className, String.format(sourcePattern, className, "dummy")); - ClassLoader classLoader = classLoaderBuilder.build(); + final String className = "DummyClass" + index; + final ClassLoader classLoader = + ClassLoaderUtils.withRoot(new File(folderToSaveSource)) + .addClass(className) + .buildWithoutCompilation(); return Class.forName(className, true, classLoader); }