This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 65730ff88c fix(concurrent): count the -Xmx headroom as available
memory in MemoryLimitCalculator (#7809)
65730ff88c is described below
commit 65730ff88c05f9fdec1ea93a00ab04d66ca1c2b4
Author: Manas Choksi <[email protected]>
AuthorDate: Mon Oct 19 12:49:50 2026 +0530
fix(concurrent): count the -Xmx headroom as available memory in
MemoryLimitCalculator (#7809)
MemoryLimitCalculator.maxAvailable() sampled only Runtime.freeMemory(), the
free
part of the currently committed heap, so MemorySafeLinkedBlockingQueue and
the
admission paths it backs (the shared HierarchicalWheelTimer queue and the
work
queues) rejected tasks whenever the committed heap had less free space than
the
configured floor, even though the JVM could still commit heap up to -Xmx. On
small-Xms deployments the wheel timer queue (Constants.THE_256_MB) then
silently
dropped recurring tasks such as heartbeat registrations (issue repro:
-Xms256m -Xmx768m, 156MiB free committed vs 668MiB headroom to -Xmx ->
rejected).
refresh() now derives the limit from maxMemory() - (totalMemory() -
freeMemory()),
which is identical to freeMemory() when the committed heap already equals
-Xmx
(pinned-heap deployments keep their exact behavior) and only admits more
when the
heap can actually grow. The apache/dubbo twin of this class still samples
freeMemory(), so this intentionally improves on it.
The MemorySafeLinkedBlockingQueue tests now use a maxMemory()+1 floor
guarded by
an assumption instead of the Integer.MAX_VALUE floor, which could no longer
force
rejection on heaps above 2GiB once the headroom is counted, and adds
regression
tests for the headroom accounting.
Fixes #7803
Signed-off-by: choksi2212 <[email protected]>
Co-authored-by: choksi2212 <[email protected]>
---
.../common/concurrent/MemoryLimitCalculator.java | 15 +++++++---
.../concurrent/MemoryLimitCalculatorTest.java | 9 ++++++
.../MemorySafeLinkedBlockingQueueTest.java | 35 ++++++++++++++++++----
3 files changed, 50 insertions(+), 9 deletions(-)
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/concurrent/MemoryLimitCalculator.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/concurrent/MemoryLimitCalculator.java
index 8688c274c1..65c6856020 100644
---
a/shenyu-common/src/main/java/org/apache/shenyu/common/concurrent/MemoryLimitCalculator.java
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/concurrent/MemoryLimitCalculator.java
@@ -23,9 +23,10 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
- * {@link java.lang.Runtime#freeMemory()} technology is used to calculate the
- * memory limit by using the percentage of the current maximum available
memory,
- * which can be used with {@link
org.apache.shenyu.common.concurrent.MemoryLimiter}.
+ * The memory limit is calculated from the amount of memory the JVM can still
commit
+ * up to {@link java.lang.Runtime#maxMemory()} (the {@code -Xmx} headroom),
not just
+ * from the free part of the current committed heap, and it can be used with
+ * {@link org.apache.shenyu.common.concurrent.MemoryLimiter}.
*
* @see org.apache.shenyu.common.concurrent.MemoryLimiter
*/
@@ -54,7 +55,13 @@ public class MemoryLimitCalculator {
}
private static void refresh() {
- maxAvailable = Runtime.getRuntime().freeMemory();
+ final Runtime runtime = Runtime.getRuntime();
+ final long used = runtime.totalMemory() - runtime.freeMemory();
+ // The JVM can grow the committed heap up to maxMemory(), so the
memory that is
+ // genuinely still available is the headroom to -Xmx, not only the
free part of
+ // the currently committed heap. When the committed heap already
equals the
+ // maximum (-Xms == -Xmx), this is identical to freeMemory().
+ maxAvailable = runtime.maxMemory() - used;
}
/**
diff --git
a/shenyu-common/src/test/java/org/apache/shenyu/common/concurrent/MemoryLimitCalculatorTest.java
b/shenyu-common/src/test/java/org/apache/shenyu/common/concurrent/MemoryLimitCalculatorTest.java
index 2dbfe509e1..d08ad4ffe5 100644
---
a/shenyu-common/src/test/java/org/apache/shenyu/common/concurrent/MemoryLimitCalculatorTest.java
+++
b/shenyu-common/src/test/java/org/apache/shenyu/common/concurrent/MemoryLimitCalculatorTest.java
@@ -20,6 +20,7 @@ package org.apache.shenyu.common.concurrent;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
@@ -47,4 +48,12 @@ public class MemoryLimitCalculatorTest {
public void testDefaultCalculate() {
assertEquals((long) (MemoryLimitCalculator.maxAvailable() * 0.8),
MemoryLimitCalculator.defaultLimit());
}
+
+ @Test
+ public void testMaxAvailableCountsMemoryTheHeapCanStillCommit() {
+ // The available memory must account for the part of the heap the JVM
can
+ // still commit up to -Xmx, not only the free part of the committed
heap.
+ final Runtime runtime = Runtime.getRuntime();
+ assertTrue(MemoryLimitCalculator.maxAvailable() >= runtime.maxMemory()
- runtime.totalMemory());
+ }
}
diff --git
a/shenyu-common/src/test/java/org/apache/shenyu/common/concurrent/MemorySafeLinkedBlockingQueueTest.java
b/shenyu-common/src/test/java/org/apache/shenyu/common/concurrent/MemorySafeLinkedBlockingQueueTest.java
index afbfa63c32..31f77494c3 100644
---
a/shenyu-common/src/test/java/org/apache/shenyu/common/concurrent/MemorySafeLinkedBlockingQueueTest.java
+++
b/shenyu-common/src/test/java/org/apache/shenyu/common/concurrent/MemorySafeLinkedBlockingQueueTest.java
@@ -27,11 +27,16 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class MemorySafeLinkedBlockingQueueTest {
@Test
- public void test() throws Exception {
- MemorySafeLinkedBlockingQueue<Runnable> queue = new
MemorySafeLinkedBlockingQueue<>(Integer.MAX_VALUE);
+ public void test() {
+ // A floor above every byte the JVM can ever commit rejects
deterministically.
+ // Integer.MAX_VALUE is the largest floor the constructor accepts, so
skip
+ // on heaps that are at least that large.
+ assumeTrue(Runtime.getRuntime().maxMemory() < Integer.MAX_VALUE);
+ MemorySafeLinkedBlockingQueue<Runnable> queue = new
MemorySafeLinkedBlockingQueue<>((int) (Runtime.getRuntime().maxMemory() + 1));
// all memory is reserved for JVM, so it will fail here
assertThat(queue.offer(() -> {
}), is(false));
@@ -43,8 +48,27 @@ public class MemorySafeLinkedBlockingQueueTest {
}
@Test
- public void testCustomReject() throws Exception {
- MemorySafeLinkedBlockingQueue<Runnable> queue = new
MemorySafeLinkedBlockingQueue<>(Integer.MAX_VALUE);
+ public void testAdmissionCountsMemoryTheHeapCanStillCommit() {
+ final Runtime runtime = Runtime.getRuntime();
+ final long committed = runtime.totalMemory();
+ final long free = runtime.freeMemory();
+ final long max = runtime.maxMemory();
+ // Only meaningful when the committed heap can still grow toward -Xmx.
+ assumeTrue(max - committed > 32L * 1024 * 1024);
+ final long headroom = max - (committed - free);
+ // A floor that cannot be met from the free committed heap alone, but
that
+ // still leaves more memory available than the headroom to -Xmx.
+ final long floor = (free + headroom) / 2;
+ assumeTrue(floor > free && floor < headroom && floor <
Integer.MAX_VALUE);
+ MemorySafeLinkedBlockingQueue<Runnable> queue = new
MemorySafeLinkedBlockingQueue<>((int) floor);
+ assertThat(queue.offer(() -> {
+ }), is(true));
+ }
+
+ @Test
+ public void testCustomReject() {
+ assumeTrue(Runtime.getRuntime().maxMemory() < Integer.MAX_VALUE);
+ MemorySafeLinkedBlockingQueue<Runnable> queue = new
MemorySafeLinkedBlockingQueue<>((int) (Runtime.getRuntime().maxMemory() + 1));
queue.setRejector(new AbortPolicy<>());
assertThrows(RejectException.class, () -> queue.offer(() -> {
}));
@@ -54,8 +78,9 @@ public class MemorySafeLinkedBlockingQueueTest {
public void testDiscardOldestDoesNotReenterMemoryCheck() {
Runnable oldest = () -> { };
Runnable replacement = () -> { };
+ assumeTrue(Runtime.getRuntime().maxMemory() < Integer.MAX_VALUE);
MemorySafeLinkedBlockingQueue<Runnable> queue =
- new
MemorySafeLinkedBlockingQueue<>(Collections.singletonList(oldest),
Integer.MAX_VALUE);
+ new
MemorySafeLinkedBlockingQueue<>(Collections.singletonList(oldest), (int)
(Runtime.getRuntime().maxMemory() + 1));
queue.setRejector(new DiscardOldestPolicy<>());
assertThat(queue.offer(replacement), is(false));