Skip to content

Commit 7d75839

Browse files
mergify[bot]andsel
andauthored
[Backport 8.x] Limit memory consumption in test on overflow (#17373) (#17411)
Updates only test code to be able to run a test that consumes big memory if: - the physical memory is bigger than the requested Java heap - JDK version is greater than or equal to 21. The reason to limit the JDK version is that on 16GB machine the G1GC is more efficient than the one on previous JDKs and so let complete the test with 10GB heap, while in JDK 17 it consistently fails with OOM error. (cherry picked from commit 075fdb4) Co-authored-by: Andrea Selva <[email protected]>
1 parent ee216ea commit 7d75839

File tree

3 files changed

+62
-10
lines changed

3 files changed

+62
-10
lines changed

logstash-core/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ tasks.register("javaTests", Test) {
124124
exclude '/org/logstash/plugins/factory/PluginFactoryExtTest.class'
125125
exclude '/org/logstash/execution/ObservedExecutionTest.class'
126126

127-
maxHeapSize = "12g"
127+
// 10GB is needed by the BufferedTokenizerExtWithSizeLimitTest.givenTooLongInputExtractDoesntOverflow test
128+
maxHeapSize = "10g"
128129

129130
jacoco {
130131
enabled = true

logstash-core/src/main/java/org/logstash/util/JavaVersion.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class JavaVersion implements Comparable<JavaVersion> {
3232
public static final JavaVersion CURRENT = parse(System.getProperty("java.specification.version"));
3333
public static final JavaVersion JAVA_11 = parse("11");
3434
public static final JavaVersion JAVA_17 = parse("17");
35+
public static final JavaVersion JAVA_21 = parse("21");
3536

3637
private final List<Integer> version;
3738

logstash-core/src/test/java/org/logstash/common/BufferedTokenizerExtWithSizeLimitTest.java

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.logstash.common;
2121

22+
import org.hamcrest.Matchers;
2223
import org.jruby.RubyArray;
2324
import org.jruby.RubyString;
2425
import org.jruby.runtime.ThreadContext;
@@ -27,12 +28,21 @@
2728
import org.junit.Test;
2829
import org.logstash.RubyTestBase;
2930
import org.logstash.RubyUtil;
31+
import org.logstash.util.JavaVersion;
3032

33+
import javax.management.Attribute;
34+
import javax.management.InstanceNotFoundException;
35+
import javax.management.ReflectionException;
36+
import java.lang.management.ManagementFactory;
37+
import java.lang.management.OperatingSystemMXBean;
3138
import java.util.List;
3239

3340
import static org.hamcrest.MatcherAssert.assertThat;
3441
import static org.hamcrest.Matchers.containsString;
35-
import static org.junit.Assert.*;
42+
import static org.junit.Assert.assertEquals;
43+
import static org.junit.Assert.assertThrows;
44+
import static org.junit.Assume.assumeThat;
45+
import static org.junit.Assume.assumeTrue;
3646
import static org.logstash.RubyUtil.RUBY;
3747

3848
@SuppressWarnings("unchecked")
@@ -115,27 +125,67 @@ public void giveMultipleSegmentsThatGeneratesMultipleBufferFullErrorsThenIsAbleT
115125
}
116126

117127
@Test
118-
public void givenMaliciousInputExtractDoesntOverflow() {
128+
public void givenTooLongInputExtractDoesntOverflow() {
129+
// This test has proven to go OOM on JDK 11 and JDK 17, also if physical memory is 16GB.
130+
// JDK 21 successfully executes the test due to internal changes or efficiency of G1GC (the default GC).
131+
// Tested also others GC on JDK 11 without any success:
132+
// - ZGC
133+
// - Parallel GC
134+
// - CMS
135+
// remove this code when the minimal JDK version for Logstash is JDK 21 or greater.
136+
137+
assumeThat("Expect at least JDK 21", JavaVersion.CURRENT, Matchers.greaterThanOrEqualTo(JavaVersion.JAVA_21));
138+
long expectedNeedHeapMemory = 10L * GB;
139+
// To understand the motivation of 10GB heap please read https://github.com/elastic/logstash/pull/17373#issuecomment-2750378212
140+
assumeTrue("Skip the test because VM hasn't enough physical memory", hasEnoughPhysicalMemory(expectedNeedHeapMemory));
141+
119142
assertEquals("Xmx must equals to what's defined in the Gradle's javaTests task",
120-
12L * GB, Runtime.getRuntime().maxMemory());
143+
expectedNeedHeapMemory, Runtime.getRuntime().maxMemory());
121144

122145
// re-init the tokenizer with big sizeLimit
123146
initSUTWithSizeLimit((int) (2L * GB) - 3);
124147
// Integer.MAX_VALUE is 2 * GB
125-
String bigFirstPiece = generateString("a", Integer.MAX_VALUE - 1024);
126-
sut.extract(context, RubyUtil.RUBY.newString(bigFirstPiece));
148+
RubyString bigFirstPiece = generateString("a", Integer.MAX_VALUE - 1024);
149+
sut.extract(context, bigFirstPiece);
127150

128151
// add another small fragment to trigger int overflow
129152
// sizeLimit is (2^32-1)-3 first segment length is (2^32-1) - 1024 second is 1024 +2
130-
// so the combined length of first and second is > sizeLimit and should throw an expection
153+
// so the combined length of first and second is > sizeLimit and should throw an exception
131154
// but because of overflow it's negative and happens to be < sizeLimit
132155
Exception thrownException = assertThrows(IllegalStateException.class, () -> {
133-
sut.extract(context, RubyUtil.RUBY.newString(generateString("a", 1024 + 2)));
156+
sut.extract(context, generateString("a", 1024 + 2));
134157
});
135158
assertThat(thrownException.getMessage(), containsString("input buffer full"));
136159
}
137160

138-
private String generateString(String fill, int size) {
139-
return fill.repeat(size);
161+
private RubyString generateString(String fill, int size) {
162+
return RubyUtil.RUBY.newString(fill.repeat(size));
163+
}
164+
165+
private boolean hasEnoughPhysicalMemory(long requiredPhysicalMemory) {
166+
long physicalMemory;
167+
try {
168+
physicalMemory = readPhysicalMemorySize();
169+
} catch (InstanceNotFoundException | ReflectionException e) {
170+
System.out.println("Can't read attribute JMX OS bean");
171+
return false;
172+
} catch (IllegalStateException e) {
173+
System.out.println(e.getMessage());
174+
return false;
175+
}
176+
System.out.println("Physical memory on the VM is: " + physicalMemory + " bytes");
177+
return physicalMemory > requiredPhysicalMemory;
178+
}
179+
180+
private long readPhysicalMemorySize() throws ReflectionException, InstanceNotFoundException {
181+
OperatingSystemMXBean op = ManagementFactory.getOperatingSystemMXBean();
182+
183+
List<Attribute> attributes = ManagementFactory.getPlatformMBeanServer()
184+
.getAttributes(op.getObjectName(), new String[]{"TotalPhysicalMemorySize"} ).asList();
185+
if (attributes.isEmpty()) {
186+
throw new IllegalStateException("Attribute TotalPhysicalMemorySize is not available from JMX OS bean");
187+
}
188+
Attribute a = attributes.get(0);
189+
return (long) (Long) a.getValue();
140190
}
141191
}

0 commit comments

Comments
 (0)