Skip to content

Commit 8e0e374

Browse files
HBASE-29912 Codel lifoThreshold should be applied on soft queue limit instead of hard limit
1 parent 6d36008 commit 8e0e374

3 files changed

Lines changed: 186 additions & 26 deletions

File tree

hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/AdaptiveLifoCoDelCallQueue.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class AdaptiveLifoCoDelCallQueue implements BlockingQueue<CallRunner> {
4444
private LinkedBlockingDeque<CallRunner> queue;
4545

4646
// so we can calculate actual threshold to switch to LIFO under load
47-
private int maxCapacity;
47+
private int currentQueueLimit;
4848

4949
// metrics (shared across all queues)
5050
private LongAdder numGeneralCallsDropped;
@@ -71,27 +71,30 @@ public class AdaptiveLifoCoDelCallQueue implements BlockingQueue<CallRunner> {
7171
private AtomicBoolean isOverloaded = new AtomicBoolean(false);
7272

7373
public AdaptiveLifoCoDelCallQueue(int capacity, int targetDelay, int interval,
74-
double lifoThreshold, LongAdder numGeneralCallsDropped, LongAdder numLifoModeSwitches) {
75-
this.maxCapacity = capacity;
74+
double lifoThreshold, LongAdder numGeneralCallsDropped, LongAdder numLifoModeSwitches,
75+
int currentQueueLimit) {
7676
this.queue = new LinkedBlockingDeque<>(capacity);
7777
this.codelTargetDelay = targetDelay;
7878
this.codelInterval = interval;
7979
this.lifoThreshold = lifoThreshold;
8080
this.numGeneralCallsDropped = numGeneralCallsDropped;
8181
this.numLifoModeSwitches = numLifoModeSwitches;
82+
this.currentQueueLimit = currentQueueLimit;
8283
}
8384

8485
/**
8586
* Update tunables.
8687
* @param newCodelTargetDelay new CoDel target delay
8788
* @param newCodelInterval new CoDel interval
8889
* @param newLifoThreshold new Adaptive Lifo threshold
90+
* @param currentQueueLimit new limit of queue
8991
*/
90-
public void updateTunables(int newCodelTargetDelay, int newCodelInterval,
91-
double newLifoThreshold) {
92+
public void updateTunables(int newCodelTargetDelay, int newCodelInterval, double newLifoThreshold,
93+
int currentQueueLimit) {
9294
this.codelTargetDelay = newCodelTargetDelay;
9395
this.codelInterval = newCodelInterval;
9496
this.lifoThreshold = newLifoThreshold;
97+
this.currentQueueLimit = currentQueueLimit;
9598
}
9699

97100
/**
@@ -104,7 +107,7 @@ public void updateTunables(int newCodelTargetDelay, int newCodelInterval,
104107
public CallRunner take() throws InterruptedException {
105108
CallRunner cr;
106109
while (true) {
107-
if (((double) queue.size() / this.maxCapacity) > lifoThreshold) {
110+
if (((double) queue.size() / this.currentQueueLimit) > lifoThreshold) {
108111
numLifoModeSwitches.increment();
109112
cr = queue.takeLast();
110113
} else {
@@ -124,7 +127,7 @@ public CallRunner poll() {
124127
CallRunner cr;
125128
boolean switched = false;
126129
while (true) {
127-
if (((double) queue.size() / this.maxCapacity) > lifoThreshold) {
130+
if (((double) queue.size() / this.currentQueueLimit) > lifoThreshold) {
128131
// Only count once per switch.
129132
if (!switched) {
130133
switched = true;

hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/RpcExecutor.java

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public abstract class RpcExecutor {
107107
private final Class<? extends BlockingQueue> queueClass;
108108
private final Object[] queueInitArgs;
109109

110+
// this is soft limit of the queue, while initializing we will use hard limit as the size of queue
110111
protected volatile int currentQueueLimit;
111112

112113
private final AtomicInteger activeHandlerCount = new AtomicInteger(0);
@@ -161,11 +162,13 @@ public RpcExecutor(final String name, final int handlerCount, final String callQ
161162
int handlerCountPerQueue = this.handlerCount / this.numCallQueues;
162163
maxQueueLength = handlerCountPerQueue * RpcServer.DEFAULT_MAX_CALLQUEUE_LENGTH_PER_HANDLER;
163164
}
165+
currentQueueLimit = maxQueueLength;
166+
int queueHardLimit = Math.max(maxQueueLength, DEFAULT_CALL_QUEUE_SIZE_HARD_LIMIT);
164167

165168
if (isDeadlineQueueType(callQueueType)) {
166169
this.name += ".Deadline";
167170
this.queueInitArgs =
168-
new Object[] { maxQueueLength, new CallPriorityComparator(conf, priority) };
171+
new Object[] { queueHardLimit, new CallPriorityComparator(conf, priority) };
169172
this.queueClass = BoundedPriorityBlockingQueue.class;
170173
} else if (isCodelQueueType(callQueueType)) {
171174
this.name += ".Codel";
@@ -174,8 +177,8 @@ public RpcExecutor(final String name, final int handlerCount, final String callQ
174177
int codelInterval = conf.getInt(CALL_QUEUE_CODEL_INTERVAL, CALL_QUEUE_CODEL_DEFAULT_INTERVAL);
175178
double codelLifoThreshold =
176179
conf.getDouble(CALL_QUEUE_CODEL_LIFO_THRESHOLD, CALL_QUEUE_CODEL_DEFAULT_LIFO_THRESHOLD);
177-
this.queueInitArgs = new Object[] { maxQueueLength, codelTargetDelay, codelInterval,
178-
codelLifoThreshold, numGeneralCallsDropped, numLifoModeSwitches };
180+
this.queueInitArgs = new Object[] { queueHardLimit, codelTargetDelay, codelInterval,
181+
codelLifoThreshold, numGeneralCallsDropped, numLifoModeSwitches, currentQueueLimit };
179182
this.queueClass = AdaptiveLifoCoDelCallQueue.class;
180183
} else if (isPluggableQueueType(callQueueType)) {
181184
Optional<Class<? extends BlockingQueue<CallRunner>>> pluggableQueueClass =
@@ -185,12 +188,12 @@ public RpcExecutor(final String name, final int handlerCount, final String callQ
185188
throw new PluggableRpcQueueNotFound(
186189
"Pluggable call queue failed to load and selected call" + " queue type required");
187190
} else {
188-
this.queueInitArgs = new Object[] { maxQueueLength, priority, conf };
191+
this.queueInitArgs = new Object[] { queueHardLimit, priority, conf };
189192
this.queueClass = pluggableQueueClass.get();
190193
}
191194
} else {
192195
this.name += ".Fifo";
193-
this.queueInitArgs = new Object[] { maxQueueLength };
196+
this.queueInitArgs = new Object[] { queueHardLimit };
194197
this.queueClass = LinkedBlockingQueue.class;
195198
}
196199

@@ -231,20 +234,7 @@ public Map<String, Long> getCallQueueSizeSummary() {
231234
.collect(Collectors.groupingBy(Pair::getFirst, Collectors.summingLong(Pair::getSecond)));
232235
}
233236

234-
// This method can only be called ONCE per executor instance.
235-
// Before calling: queueInitArgs[0] contains the soft limit (desired queue capacity)
236-
// After calling: queueInitArgs[0] is set to hard limit and currentQueueLimit stores the original
237-
// soft limit.
238-
// Multiple calls would incorrectly use the hard limit as the soft limit.
239-
// As all the queues has same initArgs and queueClass, there should be no need to call this again.
240237
protected void initializeQueues(final int numQueues) {
241-
if (!queues.isEmpty()) {
242-
throw new RuntimeException("Queues are already initialized");
243-
}
244-
if (queueInitArgs.length > 0) {
245-
currentQueueLimit = (int) queueInitArgs[0];
246-
queueInitArgs[0] = Math.max((int) queueInitArgs[0], DEFAULT_CALL_QUEUE_SIZE_HARD_LIMIT);
247-
}
248238
for (int i = 0; i < numQueues; ++i) {
249239
queues.add(ReflectionUtils.newInstance(queueClass, queueInitArgs));
250240
}
@@ -479,8 +469,10 @@ public void onConfigurationChange(Configuration conf) {
479469

480470
for (BlockingQueue<CallRunner> queue : queues) {
481471
if (queue instanceof AdaptiveLifoCoDelCallQueue) {
472+
// current queue Limit for executor is already updated as part of resizeQueues, we need to
473+
// let codel queue also make aware of it
482474
((AdaptiveLifoCoDelCallQueue) queue).updateTunables(codelTargetDelay, codelInterval,
483-
codelLifoThreshold);
475+
codelLifoThreshold, currentQueueLimit);
484476
} else if (queue instanceof ConfigurationObserver) {
485477
((ConfigurationObserver) queue).onConfigurationChange(conf);
486478
}

hbase-server/src/test/java/org/apache/hadoop/hbase/ipc/TestSimpleRpcScheduler.java

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package org.apache.hadoop.hbase.ipc;
1919

20+
import static org.awaitility.Awaitility.await;
2021
import static org.junit.Assert.assertEquals;
2122
import static org.junit.Assert.assertFalse;
2223
import static org.junit.Assert.assertNotEquals;
@@ -34,13 +35,15 @@
3435
import java.lang.reflect.Field;
3536
import java.net.InetSocketAddress;
3637
import java.util.ArrayList;
38+
import java.util.Collections;
3739
import java.util.HashSet;
3840
import java.util.List;
3941
import java.util.Map;
4042
import java.util.Set;
4143
import java.util.concurrent.BlockingQueue;
4244
import java.util.concurrent.CountDownLatch;
4345
import java.util.concurrent.LinkedBlockingQueue;
46+
import java.util.concurrent.TimeUnit;
4447
import org.apache.hadoop.conf.Configuration;
4548
import org.apache.hadoop.hbase.Abortable;
4649
import org.apache.hadoop.hbase.HBaseClassTestRule;
@@ -814,4 +817,166 @@ public void drop() {
814817
}
815818
};
816819
}
820+
821+
/**
822+
* Test LIFO switching behavior through actual RPC calls. This test verifies that when the queue
823+
* fills beyond the LIFO threshold, newer calls are processed before older calls (LIFO mode).
824+
*/
825+
@Test
826+
public void testCoDelLifoWithRpcCalls() throws Exception {
827+
Configuration testConf = HBaseConfiguration.create();
828+
testConf.set(RpcExecutor.CALL_QUEUE_TYPE_CONF_KEY,
829+
RpcExecutor.CALL_QUEUE_TYPE_CODEL_CONF_VALUE);
830+
int maxCallQueueLength = 50;
831+
double codelLifoThreshold = 0.8;
832+
testConf.setInt(RpcScheduler.IPC_SERVER_MAX_CALLQUEUE_LENGTH, maxCallQueueLength);
833+
testConf.setDouble(RpcExecutor.CALL_QUEUE_CODEL_LIFO_THRESHOLD, codelLifoThreshold);
834+
testConf.setInt(RpcExecutor.CALL_QUEUE_CODEL_TARGET_DELAY, 100);
835+
testConf.setInt(RpcExecutor.CALL_QUEUE_CODEL_INTERVAL, 100);
836+
testConf.setInt(HConstants.REGION_SERVER_HANDLER_COUNT, 1); // Single handler to control
837+
// processing
838+
839+
PriorityFunction priority = mock(PriorityFunction.class);
840+
when(priority.getPriority(any(), any(), any())).thenReturn(HConstants.NORMAL_QOS);
841+
SimpleRpcScheduler scheduler =
842+
new SimpleRpcScheduler(testConf, 1, 0, 0, priority, HConstants.QOS_THRESHOLD);
843+
844+
try {
845+
scheduler.init(CONTEXT);
846+
scheduler.start();
847+
848+
// Track completion order
849+
final List<Integer> completedCalls = Collections.synchronizedList(new ArrayList<>());
850+
851+
// Dispatch many slow calls rapidly to fill the queue beyond 80% threshold
852+
// With queue limit of 50, we need > 40 calls to cross 80%
853+
int numCalls = 48;
854+
for (int i = 0; i < numCalls; i++) {
855+
final int callId = i;
856+
CallRunner call = createMockTask(HConstants.NORMAL_QOS);
857+
call.setStatus(new MonitoredRPCHandlerImpl("test"));
858+
doAnswer(invocation -> {
859+
completedCalls.add(callId);
860+
Thread.sleep(100); // Slow processing to allow queue to build up
861+
return null;
862+
}).when(call).run();
863+
scheduler.dispatch(call);
864+
// No delay between dispatches - rapidly fill the queue
865+
}
866+
867+
// Wait for some calls to complete
868+
await().atMost(1, TimeUnit.SECONDS).until(() -> completedCalls.size() >= 2);
869+
870+
// Check that we had LIFO switches
871+
long lifoSwitches = scheduler.getNumLifoModeSwitches();
872+
assertTrue("Should have switched to LIFO mode at least once, but got: " + lifoSwitches,
873+
lifoSwitches > 0);
874+
875+
// Verify LIFO behavior: Among first completed calls, we should see higher call IDs
876+
// (indicating later dispatched calls completed first)
877+
int maxCallIdCompleted = -1;
878+
for (int i = 0; i < 5 && i < completedCalls.size(); i++) {
879+
maxCallIdCompleted = Math.max(maxCallIdCompleted, completedCalls.get(i));
880+
}
881+
// At least one of the early completed calls should have a high ID (>20)
882+
// indicating LIFO processing
883+
assertTrue(
884+
"Expected LIFO behavior: early completed calls should include call arrived after threshold "
885+
+ "maxCallIdCompleted: " + maxCallIdCompleted,
886+
maxCallIdCompleted > maxCallQueueLength * codelLifoThreshold);
887+
888+
} finally {
889+
scheduler.stop();
890+
}
891+
}
892+
893+
/**
894+
* Test that CoDel queue returns to FIFO mode after draining below threshold.
895+
*/
896+
@Test
897+
public void testCoDelQueueDrainAndFifoReturn() throws Exception {
898+
Configuration testConf = HBaseConfiguration.create();
899+
testConf.set(RpcExecutor.CALL_QUEUE_TYPE_CONF_KEY,
900+
RpcExecutor.CALL_QUEUE_TYPE_CODEL_CONF_VALUE);
901+
testConf.setInt(RpcScheduler.IPC_SERVER_MAX_CALLQUEUE_LENGTH, 50);
902+
testConf.setDouble(RpcExecutor.CALL_QUEUE_CODEL_LIFO_THRESHOLD, 0.8);
903+
testConf.setInt(HConstants.REGION_SERVER_HANDLER_COUNT, 2);
904+
905+
PriorityFunction priority = mock(PriorityFunction.class);
906+
when(priority.getPriority(any(), any(), any())).thenReturn(HConstants.NORMAL_QOS);
907+
SimpleRpcScheduler scheduler =
908+
new SimpleRpcScheduler(testConf, 2, 0, 0, priority, HConstants.QOS_THRESHOLD);
909+
910+
try {
911+
scheduler.init(CONTEXT);
912+
scheduler.start();
913+
914+
final List<Integer> completedCalls = Collections.synchronizedList(new ArrayList<>());
915+
916+
// Phase 1: Fill queue rapidly to trigger LIFO (>40 calls for 80% of 50)
917+
for (int i = 0; i < 48; i++) {
918+
final int callId = i;
919+
CallRunner call = createMockTask(HConstants.NORMAL_QOS);
920+
call.setStatus(new MonitoredRPCHandlerImpl("test"));
921+
doAnswer(invocation -> {
922+
completedCalls.add(callId);
923+
Thread.sleep(80);
924+
return null;
925+
}).when(call).run();
926+
scheduler.dispatch(call);
927+
}
928+
929+
// Wait for calls to complete
930+
await().atMost(1, TimeUnit.SECONDS).until(() -> completedCalls.size() >= 2);
931+
long lifoSwitchesPhase1 = scheduler.getNumLifoModeSwitches();
932+
assertTrue("Should have entered LIFO mode", lifoSwitchesPhase1 > 0);
933+
934+
// Phase 2: Let queue drain
935+
await().atMost(2, TimeUnit.SECONDS).until(() -> scheduler.getGeneralQueueLength() <= 0);
936+
int queueLength = scheduler.getGeneralQueueLength();
937+
assertTrue("Queue should have drained significantly, but got: " + queueLength,
938+
queueLength < 25);
939+
940+
// Phase 3: Send new calls - should process in FIFO order
941+
completedCalls.clear();
942+
for (int i = 100; i < 105; i++) {
943+
final int callId = i;
944+
CallRunner call = createMockTask(HConstants.NORMAL_QOS);
945+
call.setStatus(new MonitoredRPCHandlerImpl("test"));
946+
doAnswer(invocation -> {
947+
completedCalls.add(callId);
948+
Thread.sleep(80);
949+
return null;
950+
}).when(call).run();
951+
scheduler.dispatch(call);
952+
}
953+
954+
// Wait for these calls to complete
955+
await().atMost(2, TimeUnit.SECONDS).until(() -> completedCalls.size() >= 2);
956+
957+
// Verify FIFO behavior: calls should complete in order (100, 101, 102, 103, 104)
958+
assertTrue(
959+
"Should have completed test calls, but count of completed calls: " + completedCalls.size(),
960+
completedCalls.size() >= 2);
961+
// Check that calls completed roughly in order (allowing some variance due to threading with 2
962+
// handlers)
963+
// With 2 handlers, we expect general FIFO order but some interleaving is possible
964+
// Just verify the general trend is increasing
965+
int violations = 0;
966+
for (int i = 0; i < completedCalls.size() - 1; i++) {
967+
int current = completedCalls.get(i);
968+
int next = completedCalls.get(i + 1);
969+
if (next < current) {
970+
violations++;
971+
}
972+
}
973+
// Allow at most 1 violation due to concurrent execution by 2 handlers
974+
assertTrue("Calls should complete in approximate FIFO order, violations: " + violations
975+
+ ", order: " + completedCalls, violations <= 1);
976+
977+
} finally {
978+
scheduler.stop();
979+
}
980+
}
981+
817982
}

0 commit comments

Comments
 (0)