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));

Reply via email to