This is an automated email from the ASF dual-hosted git repository.

apurtell pushed a commit to branch branch-2.5
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2.5 by this push:
     new 7ebb557178d HBASE-29912 Codel lifoThreshold should be applied on soft 
queue limit instead of hard limit (#8346)
7ebb557178d is described below

commit 7ebb557178d4eab7001906063c1a476a991c45cb
Author: Umesh <[email protected]>
AuthorDate: Fri Jun 19 04:33:29 2026 +0530

    HBASE-29912 Codel lifoThreshold should be applied on soft queue limit 
instead of hard limit (#8346)
    
    Signed-off-by: Andrew Purtell <[email protected]>
---
 .../hbase/ipc/AdaptiveLifoCoDelCallQueue.java      |  17 +-
 .../org/apache/hadoop/hbase/ipc/RpcExecutor.java   |  43 ++--
 .../hadoop/hbase/ipc/TestSimpleRpcScheduler.java   | 252 +++++++++++++++++----
 3 files changed, 246 insertions(+), 66 deletions(-)

diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/AdaptiveLifoCoDelCallQueue.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/AdaptiveLifoCoDelCallQueue.java
index dee517c2e0a..89fde2e1c14 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/AdaptiveLifoCoDelCallQueue.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/AdaptiveLifoCoDelCallQueue.java
@@ -44,7 +44,7 @@ public class AdaptiveLifoCoDelCallQueue implements 
BlockingQueue<CallRunner> {
   private LinkedBlockingDeque<CallRunner> queue;
 
   // so we can calculate actual threshold to switch to LIFO under load
-  private int maxCapacity;
+  private volatile int softLimit;
 
   // metrics (shared across all queues)
   private LongAdder numGeneralCallsDropped;
@@ -71,14 +71,15 @@ public class AdaptiveLifoCoDelCallQueue implements 
BlockingQueue<CallRunner> {
   private AtomicBoolean isOverloaded = new AtomicBoolean(false);
 
   public AdaptiveLifoCoDelCallQueue(int capacity, int targetDelay, int 
interval,
-    double lifoThreshold, LongAdder numGeneralCallsDropped, LongAdder 
numLifoModeSwitches) {
-    this.maxCapacity = capacity;
+    double lifoThreshold, LongAdder numGeneralCallsDropped, LongAdder 
numLifoModeSwitches,
+    int currentQueueLimit) {
     this.queue = new LinkedBlockingDeque<>(capacity);
     this.codelTargetDelay = targetDelay;
     this.codelInterval = interval;
     this.lifoThreshold = lifoThreshold;
     this.numGeneralCallsDropped = numGeneralCallsDropped;
     this.numLifoModeSwitches = numLifoModeSwitches;
+    this.softLimit = currentQueueLimit;
   }
 
   /**
@@ -86,12 +87,14 @@ public class AdaptiveLifoCoDelCallQueue implements 
BlockingQueue<CallRunner> {
    * @param newCodelTargetDelay new CoDel target delay
    * @param newCodelInterval    new CoDel interval
    * @param newLifoThreshold    new Adaptive Lifo threshold
+   * @param currentQueueLimit   new soft limit of queue
    */
-  public void updateTunables(int newCodelTargetDelay, int newCodelInterval,
-    double newLifoThreshold) {
+  public void updateTunables(int newCodelTargetDelay, int newCodelInterval, 
double newLifoThreshold,
+    int currentQueueLimit) {
     this.codelTargetDelay = newCodelTargetDelay;
     this.codelInterval = newCodelInterval;
     this.lifoThreshold = newLifoThreshold;
+    this.softLimit = currentQueueLimit;
   }
 
   /**
@@ -104,7 +107,7 @@ public class AdaptiveLifoCoDelCallQueue implements 
BlockingQueue<CallRunner> {
   public CallRunner take() throws InterruptedException {
     CallRunner cr;
     while (true) {
-      if (((double) queue.size() / this.maxCapacity) > lifoThreshold) {
+      if (((double) queue.size() / this.softLimit) > lifoThreshold) {
         numLifoModeSwitches.increment();
         cr = queue.takeLast();
       } else {
@@ -124,7 +127,7 @@ public class AdaptiveLifoCoDelCallQueue implements 
BlockingQueue<CallRunner> {
     CallRunner cr;
     boolean switched = false;
     while (true) {
-      if (((double) queue.size() / this.maxCapacity) > lifoThreshold) {
+      if (((double) queue.size() / this.softLimit) > lifoThreshold) {
         // Only count once per switch.
         if (!switched) {
           switched = true;
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcExecutor.java 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcExecutor.java
index c4abb048f75..ff99f253b8e 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcExecutor.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcExecutor.java
@@ -107,7 +107,11 @@ public abstract class RpcExecutor {
   private final Class<? extends BlockingQueue> queueClass;
   private final Object[] queueInitArgs;
 
+  // this is soft limit of the queue, not size/capacity.
   protected volatile int currentQueueLimit;
+  // While initializing we will use hard limit as the capacity of queue, it 
will let us dynamically
+  // change the queue limit
+  protected final int queueHardLimit;
 
   private final AtomicInteger activeHandlerCount = new AtomicInteger(0);
   private final List<RpcHandler> handlers;
@@ -161,11 +165,13 @@ public abstract class RpcExecutor {
       int handlerCountPerQueue = this.handlerCount / this.numCallQueues;
       maxQueueLength = handlerCountPerQueue * 
RpcServer.DEFAULT_MAX_CALLQUEUE_LENGTH_PER_HANDLER;
     }
+    currentQueueLimit = maxQueueLength;
+    queueHardLimit = Math.max(maxQueueLength, 
DEFAULT_CALL_QUEUE_SIZE_HARD_LIMIT);
 
     if (isDeadlineQueueType(callQueueType)) {
       this.name += ".Deadline";
       this.queueInitArgs =
-        new Object[] { maxQueueLength, new CallPriorityComparator(conf, 
priority) };
+        new Object[] { queueHardLimit, new CallPriorityComparator(conf, 
priority) };
       this.queueClass = BoundedPriorityBlockingQueue.class;
     } else if (isCodelQueueType(callQueueType)) {
       this.name += ".Codel";
@@ -174,8 +180,8 @@ public abstract class RpcExecutor {
       int codelInterval = conf.getInt(CALL_QUEUE_CODEL_INTERVAL, 
CALL_QUEUE_CODEL_DEFAULT_INTERVAL);
       double codelLifoThreshold =
         conf.getDouble(CALL_QUEUE_CODEL_LIFO_THRESHOLD, 
CALL_QUEUE_CODEL_DEFAULT_LIFO_THRESHOLD);
-      this.queueInitArgs = new Object[] { maxQueueLength, codelTargetDelay, 
codelInterval,
-        codelLifoThreshold, numGeneralCallsDropped, numLifoModeSwitches };
+      this.queueInitArgs = new Object[] { queueHardLimit, codelTargetDelay, 
codelInterval,
+        codelLifoThreshold, numGeneralCallsDropped, numLifoModeSwitches, 
currentQueueLimit };
       this.queueClass = AdaptiveLifoCoDelCallQueue.class;
     } else if (isPluggableQueueType(callQueueType)) {
       Optional<Class<? extends BlockingQueue<CallRunner>>> pluggableQueueClass 
=
@@ -185,12 +191,12 @@ public abstract class RpcExecutor {
         throw new PluggableRpcQueueNotFound(
           "Pluggable call queue failed to load and selected call" + " queue 
type required");
       } else {
-        this.queueInitArgs = new Object[] { maxQueueLength, priority, conf };
+        this.queueInitArgs = new Object[] { queueHardLimit, priority, conf };
         this.queueClass = pluggableQueueClass.get();
       }
     } else {
       this.name += ".Fifo";
-      this.queueInitArgs = new Object[] { maxQueueLength };
+      this.queueInitArgs = new Object[] { queueHardLimit };
       this.queueClass = LinkedBlockingQueue.class;
     }
 
@@ -231,20 +237,7 @@ public abstract class RpcExecutor {
       .collect(Collectors.groupingBy(Pair::getFirst, 
Collectors.summingLong(Pair::getSecond)));
   }
 
-  // This method can only be called ONCE per executor instance.
-  // Before calling: queueInitArgs[0] contains the soft limit (desired queue 
capacity)
-  // After calling: queueInitArgs[0] is set to hard limit and 
currentQueueLimit stores the original
-  // soft limit.
-  // Multiple calls would incorrectly use the hard limit as the soft limit.
-  // As all the queues has same initArgs and queueClass, there should be no 
need to call this again.
   protected void initializeQueues(final int numQueues) {
-    if (!queues.isEmpty()) {
-      throw new RuntimeException("Queues are already initialized");
-    }
-    if (queueInitArgs.length > 0) {
-      currentQueueLimit = (int) queueInitArgs[0];
-      queueInitArgs[0] = Math.max((int) queueInitArgs[0], 
DEFAULT_CALL_QUEUE_SIZE_HARD_LIMIT);
-    }
     for (int i = 0; i < numQueues; ++i) {
       queues.add(ReflectionUtils.newInstance(queueClass, queueInitArgs));
     }
@@ -464,7 +457,15 @@ public abstract class RpcExecutor {
       }
     }
     final int queueLimit = currentQueueLimit;
-    currentQueueLimit = conf.getInt(configKey, queueLimit);
+    int newQueueLimit = conf.getInt(configKey, queueLimit);
+    if (newQueueLimit > queueHardLimit) {
+      LOG.warn(
+        "Requested soft limit {} exceeds queue hard limit/capacity {}. "
+          + "A region server restart is required to grow the underlying 
queue.",
+        newQueueLimit, queueHardLimit);
+      newQueueLimit = currentQueueLimit;
+    }
+    currentQueueLimit = newQueueLimit;
   }
 
   public void onConfigurationChange(Configuration conf) {
@@ -477,8 +478,10 @@ public abstract class RpcExecutor {
 
     for (BlockingQueue<CallRunner> queue : queues) {
       if (queue instanceof AdaptiveLifoCoDelCallQueue) {
+        // current queue Limit for executor is already updated as part of 
resizeQueues, we need to
+        // let codel queue also make aware of it
         ((AdaptiveLifoCoDelCallQueue) queue).updateTunables(codelTargetDelay, 
codelInterval,
-          codelLifoThreshold);
+          codelLifoThreshold, currentQueueLimit);
       } else if (queue instanceof ConfigurationObserver) {
         ((ConfigurationObserver) queue).onConfigurationChange(conf);
       }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/ipc/TestSimpleRpcScheduler.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/ipc/TestSimpleRpcScheduler.java
index 74d4eb2b864..767351497b5 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/ipc/TestSimpleRpcScheduler.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/ipc/TestSimpleRpcScheduler.java
@@ -17,6 +17,7 @@
  */
 package org.apache.hadoop.hbase.ipc;
 
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -34,13 +35,14 @@ import java.io.IOException;
 import java.lang.reflect.Field;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.Abortable;
 import org.apache.hadoop.hbase.HBaseConfiguration;
@@ -87,6 +89,7 @@ public class TestSimpleRpcScheduler {
   };
   private Configuration conf;
   private String testMethodName;
+  private final AtomicInteger requestProcessed = new AtomicInteger(0);
 
   @BeforeEach
   public void setUp(TestInfo testInfo) {
@@ -226,6 +229,7 @@ public class TestSimpleRpcScheduler {
     RequestHeader header = 
RequestHeader.newBuilder().setPriority(priority).build();
     when(task.getRpcCall()).thenReturn(call);
     when(call.getHeader()).thenReturn(header);
+    
when(call.getReceiveTime()).thenReturn(EnvironmentEdgeManager.currentTime());
     return task;
   }
 
@@ -567,66 +571,84 @@ public class TestSimpleRpcScheduler {
     }
   }
 
-  private static final class CoDelEnvironmentEdge implements EnvironmentEdge {
+  private final class CoDelEnvironmentEdge implements EnvironmentEdge {
 
-    private final BlockingQueue<Long> timeQ = new LinkedBlockingQueue<>();
+    private long perRequestOffset;
+    private long gapInRequests;
+    private long testStartTime = 0;
 
-    private long offset;
+    private int requestCount = 0;
 
-    private final Set<String> threadNamePrefixs = new HashSet<>();
+    private final Set<String> threadNamePrefixes = new HashSet<>();
+    private final String testThread = Thread.currentThread().getName();
 
     @Override
     public long currentTime() {
-      for (String threadNamePrefix : threadNamePrefixs) {
-        String threadName = Thread.currentThread().getName();
+      String threadName = Thread.currentThread().getName();
+      if (threadName.equals(testThread)) {
+        // test thread will create a request and add that to scheduler
+        // Replicating a constant rate of request arrival.
+        long requestStartTime = testStartTime + requestCount * gapInRequests;
+        requestCount++;
+        return requestStartTime;
+      }
+      // handler thread will pick the request from queue, this time will 
depend on processing time
+      // of handler
+      for (String threadNamePrefix : threadNamePrefixes) {
         if (threadName.startsWith(threadNamePrefix)) {
-          if (timeQ != null) {
-            Long qTime = timeQ.poll();
-            if (qTime != null) {
-              return qTime.longValue() + offset;
-            }
+          long requestPickTime;
+          if (gapInRequests >= perRequestOffset) {
+            // it means handler can complete the processing of last request 
and will be free when it
+            // will come to serve next request. We don't need it in fast path 
but just in case for
+            // other tests
+            requestPickTime = testStartTime + requestProcessed.get() * 
gapInRequests;
+          } else {
+            // this means handler will still be busy processing the last 
requests when new request
+            // will come in queue
+            requestPickTime = testStartTime + requestProcessed.get() * 
perRequestOffset;
           }
+          return requestPickTime;
         }
       }
       return System.currentTimeMillis();
     }
+
+    public void startTestFor(int arrivalRatePerSecond, long 
requestProcessingTime) {
+      this.perRequestOffset = requestProcessingTime;
+      this.gapInRequests = 1000L / arrivalRatePerSecond;
+      this.requestCount = 0;
+      requestProcessed.set(0);
+      this.testStartTime = System.currentTimeMillis();
+    }
   }
 
-  // FIX. I don't get this test (St.Ack). When I time this test, the minDelay 
is > 2 * codel delay
-  // from the get go. So we are always overloaded. The test below would seem 
to complete the
-  // queuing of all the CallRunners inside the codel check interval. I don't 
think we are skipping
-  // codel checking. Second, I think this test has been broken since 
HBASE-16089 Add on FastPath for
-  // CoDel went in. The thread name we were looking for was the name BEFORE we 
updated: i.e.
-  // "RpcServer.CodelBQ.default.handler". But same patch changed the name of 
the codel fastpath
-  // thread to: new FastPathBalancedQueueRpcExecutor("CodelFPBQ.default", 
handlerCount,
-  // numCallQueues... Codel is hard to test. This test is going to be flakey 
given it all
-  // timer-based. Disabling for now till chat with authors.
+  // This test is fixing the processing time and arrival rate of the request 
and checking if CoDel
+  // can utilize the maximum capacity of system
   @Test
   public void testCoDelScheduling() throws Exception {
     CoDelEnvironmentEdge envEdge = new CoDelEnvironmentEdge();
-    envEdge.threadNamePrefixs.add("RpcServer.default.FPBQ.Codel.handler");
+    envEdge.threadNamePrefixes.add("RpcServer.default.FPBQ.Codel.handler");
     Configuration schedConf = HBaseConfiguration.create();
     schedConf.setInt(RpcScheduler.IPC_SERVER_MAX_CALLQUEUE_LENGTH, 250);
     schedConf.set(RpcExecutor.CALL_QUEUE_TYPE_CONF_KEY,
       RpcExecutor.CALL_QUEUE_TYPE_CODEL_CONF_VALUE);
+    schedConf.setInt(RpcExecutor.CALL_QUEUE_CODEL_TARGET_DELAY, 5);
     PriorityFunction priority = mock(PriorityFunction.class);
     when(priority.getPriority(any(), any(), 
any())).thenReturn(HConstants.NORMAL_QOS);
     SimpleRpcScheduler scheduler =
       new SimpleRpcScheduler(schedConf, 1, 1, 1, priority, 
HConstants.QOS_THRESHOLD);
+
     try {
-      // Loading mocked call runner can take a good amount of time the first 
time through
-      // (haven't looked why). Load it for first time here outside of the 
timed loop.
-      getMockedCallRunner(EnvironmentEdgeManager.currentTime(), 2);
       scheduler.start();
       EnvironmentEdgeManager.injectEdge(envEdge);
-      envEdge.offset = 5;
-      // Calls faster than min delay
-      // LOG.info("Start");
+
+      // incoming traffic < capacity
+      envEdge.startTestFor(20, 40);
       for (int i = 0; i < 100; i++) {
         long time = EnvironmentEdgeManager.currentTime();
-        envEdge.timeQ.put(time);
         CallRunner cr = getMockedCallRunner(time, 2);
         scheduler.dispatch(cr);
+        Thread.sleep(50);
       }
       // LOG.info("Loop done");
       // make sure fast calls are handled
@@ -635,13 +657,15 @@ public class TestSimpleRpcScheduler {
       assertEquals(0, scheduler.getNumGeneralCallsDropped(),
         "None of these calls should have been discarded");
 
-      envEdge.offset = 151;
-      // calls slower than min delay, but not individually slow enough to be 
dropped
+      // incoming traffic = capacity
+      envEdge.startTestFor(20, 50);
       for (int i = 0; i < 20; i++) {
         long time = EnvironmentEdgeManager.currentTime();
-        envEdge.timeQ.put(time);
         CallRunner cr = getMockedCallRunner(time, 2);
         scheduler.dispatch(cr);
+        // We have mocked the arrival time and the time request get picked 
from queue but we need to
+        // also make sure that queue fill at the arrival rate
+        Thread.sleep(50);
       }
 
       // make sure somewhat slow calls are handled
@@ -650,21 +674,28 @@ public class TestSimpleRpcScheduler {
       assertEquals(0, scheduler.getNumGeneralCallsDropped(),
         "None of these calls should have been discarded");
 
-      envEdge.offset = 2000;
+      // incoming traffic > capacity
+      envEdge.startTestFor(20, 60);
       // now slow calls and the ones to be dropped
       for (int i = 0; i < 60; i++) {
         long time = EnvironmentEdgeManager.currentTime();
-        envEdge.timeQ.put(time);
         CallRunner cr = getMockedCallRunner(time, 100);
         scheduler.dispatch(cr);
+        Thread.sleep(50);
       }
 
       // make sure somewhat slow calls are handled
       waitUntilQueueEmpty(scheduler);
       Thread.sleep(100);
-      assertTrue(scheduler.getNumGeneralCallsDropped() > 12,
-        "There should have been at least 12 calls dropped however there were "
+      // 60 calls will take 3 second with 20 rps. And in 3 second with 60 
processing time we can
+      // only serve 50 request
+      assertTrue(requestProcessed.get() >= 48,
+        "Number of processed requests should be greater then 90% of capacity"
+          + requestProcessed.get());
+      assertTrue(scheduler.getNumGeneralCallsDropped() >= 9,
+        "There should have been at least 9 calls dropped however there were "
           + scheduler.getNumGeneralCallsDropped());
+
     } finally {
       scheduler.stop();
     }
@@ -779,9 +810,8 @@ public class TestSimpleRpcScheduler {
           return;
         }
         try {
-          LOG.warn("Sleeping for " + sleepTime);
           Thread.sleep(sleepTime);
-          LOG.warn("Done Sleeping for " + sleepTime);
+          requestProcessed.incrementAndGet();
         } catch (InterruptedException e) {
         }
       }
@@ -796,4 +826,148 @@ public class TestSimpleRpcScheduler {
       }
     };
   }
+
+  /**
+   * Test LIFO switching behavior through actual RPC calls. This test verifies 
that when the queue
+   * fills beyond the LIFO threshold, newer calls are processed before older 
calls (LIFO mode).
+   */
+  @Test
+  public void testCoDelLifoWithRpcCalls() throws Exception {
+    Configuration testConf = HBaseConfiguration.create();
+    testConf.set(RpcExecutor.CALL_QUEUE_TYPE_CONF_KEY,
+      RpcExecutor.CALL_QUEUE_TYPE_CODEL_CONF_VALUE);
+    int maxCallQueueLength = 50;
+    double coDelLifoThreshold = 0.8;
+    testConf.setInt(RpcScheduler.IPC_SERVER_MAX_CALLQUEUE_LENGTH, 
maxCallQueueLength);
+    testConf.setDouble(RpcExecutor.CALL_QUEUE_CODEL_LIFO_THRESHOLD, 
coDelLifoThreshold);
+    testConf.setInt(RpcExecutor.CALL_QUEUE_CODEL_TARGET_DELAY, 100);
+    testConf.setInt(RpcExecutor.CALL_QUEUE_CODEL_INTERVAL, 100);
+    testConf.setInt(HConstants.REGION_SERVER_HANDLER_COUNT, 1); // Single 
handler to control
+                                                                // processing
+
+    PriorityFunction priority = mock(PriorityFunction.class);
+    when(priority.getPriority(any(), any(), 
any())).thenReturn(HConstants.NORMAL_QOS);
+    SimpleRpcScheduler scheduler =
+      new SimpleRpcScheduler(testConf, 1, 0, 0, priority, 
HConstants.QOS_THRESHOLD);
+
+    try {
+      scheduler.init(CONTEXT);
+      scheduler.start();
+
+      // Track completion order
+      final List<Integer> completedCalls = Collections.synchronizedList(new 
ArrayList<>());
+
+      // Dispatch many slow calls rapidly to fill the queue beyond 80% 
threshold
+      // With queue limit of 50, we need > 40 calls to cross 80%
+      int numCalls = 48;
+      for (int i = 0; i < numCalls; i++) {
+        final int callId = i;
+        CallRunner call = createMockTask(HConstants.NORMAL_QOS);
+        call.setStatus(new MonitoredRPCHandlerImpl("test"));
+        doAnswer(invocation -> {
+          completedCalls.add(callId);
+          Thread.sleep(100); // Slow processing to allow queue to build up
+          return null;
+        }).when(call).run();
+        scheduler.dispatch(call);
+        // No delay between dispatches - rapidly fill the queue
+      }
+
+      // Wait for some calls to complete
+      await().atMost(2, TimeUnit.SECONDS).until(() -> completedCalls.size() >= 
3);
+
+      // Check that we had LIFO switches
+      long lifoSwitches = scheduler.getNumLifoModeSwitches();
+      assertTrue(lifoSwitches > 0,
+        "Should have switched to LIFO mode at least once, but got: " + 
lifoSwitches);
+
+      // Verify LIFO behavior: Among first completed calls, we should see 
higher call IDs
+      // (indicating later dispatched calls completed first)
+      int maxCallIdCompleted = -1;
+      for (int i = 0; i < completedCalls.size(); i++) {
+        maxCallIdCompleted = Math.max(maxCallIdCompleted, 
completedCalls.get(i));
+      }
+      // At least one of the early completed calls should have a high ID (>20)
+      // indicating LIFO processing
+      assertTrue(maxCallIdCompleted > maxCallQueueLength * coDelLifoThreshold,
+        "Expected LIFO behavior: early completed calls should include call 
arrived after threshold "
+          + "maxCallIdCompleted: " + maxCallIdCompleted);
+
+    } finally {
+      scheduler.stop();
+    }
+  }
+
+  /**
+   * Test that CoDel queue returns to FIFO mode after draining below threshold.
+   */
+  @Test
+  public void testCoDelQueueDrainAndFifoReturn() throws Exception {
+    Configuration testConf = HBaseConfiguration.create();
+    testConf.set(RpcExecutor.CALL_QUEUE_TYPE_CONF_KEY,
+      RpcExecutor.CALL_QUEUE_TYPE_CODEL_CONF_VALUE);
+    testConf.setLong(RpcExecutor.CALL_QUEUE_CODEL_TARGET_DELAY, 100);
+    testConf.setInt(RpcScheduler.IPC_SERVER_MAX_CALLQUEUE_LENGTH, 50);
+    testConf.setDouble(RpcExecutor.CALL_QUEUE_CODEL_LIFO_THRESHOLD, 0.8);
+    testConf.setInt(HConstants.REGION_SERVER_HANDLER_COUNT, 2);
+
+    PriorityFunction priority = mock(PriorityFunction.class);
+    when(priority.getPriority(any(), any(), 
any())).thenReturn(HConstants.NORMAL_QOS);
+    SimpleRpcScheduler scheduler =
+      new SimpleRpcScheduler(testConf, 2, 0, 0, priority, 
HConstants.QOS_THRESHOLD);
+
+    try {
+      scheduler.init(CONTEXT);
+      scheduler.start();
+
+      final List<Integer> completedCalls = Collections.synchronizedList(new 
ArrayList<>());
+
+      // Fill queue rapidly to trigger LIFO (>40 calls for 80% of 50)
+      for (int i = 0; i < 48; i++) {
+        final int callId = i;
+        CallRunner call = createMockTask(HConstants.NORMAL_QOS);
+        call.setStatus(new MonitoredRPCHandlerImpl("test"));
+        doAnswer(invocation -> {
+          completedCalls.add(callId);
+          Thread.sleep(80);
+          return null;
+        }).when(call).run();
+        scheduler.dispatch(call);
+      }
+
+      // Wait for calls to complete
+      await().atMost(1, TimeUnit.SECONDS).until(() -> completedCalls.size() >= 
3);
+      assertTrue(scheduler.getNumLifoModeSwitches() > 0, "Should have entered 
LIFO mode");
+
+      await().atMost(2, TimeUnit.SECONDS).until(
+        () -> scheduler.getGeneralQueueLength() == 0 && 
scheduler.getActiveRpcHandlerCount() == 0);
+
+      long pastNumLifoModeSwitches = scheduler.getNumLifoModeSwitches();
+      // Send new calls - should process in FIFO order
+      completedCalls.clear();
+      for (int i = 100; i < 105; i++) {
+        final int callId = i;
+        CallRunner call = createMockTask(HConstants.NORMAL_QOS);
+        call.setStatus(new MonitoredRPCHandlerImpl("test"));
+        doAnswer(invocation -> {
+          completedCalls.add(callId);
+          Thread.sleep(50);
+          return null;
+        }).when(call).run();
+        scheduler.dispatch(call);
+      }
+
+      // Wait for these calls to complete
+      await().atMost(2, TimeUnit.SECONDS).until(() -> completedCalls.size() >= 
2);
+
+      long newLifoSwitch = scheduler.getNumLifoModeSwitches() - 
pastNumLifoModeSwitches;
+      // Allow at most 1 violation due to concurrent execution by 2 handlers
+      assertEquals(0, newLifoSwitch,
+        "Queue should not switch to LIFO last 5 calls but number of LIFO 
switch are : "
+          + newLifoSwitch);
+    } finally {
+      scheduler.stop();
+    }
+  }
+
 }

Reply via email to