HADOOP-10282. Create a FairCallQueue: a multi-level call queue which schedules incoming calls and multiplexes outgoing calls. (Contributed by Chris Li)
git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@1619938 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
10d267975c
commit
4b3a6b8722
|
@ -560,6 +560,10 @@ Release 2.6.0 - UNRELEASED
|
||||||
HADOOP-10224. JavaKeyStoreProvider has to protect against corrupting
|
HADOOP-10224. JavaKeyStoreProvider has to protect against corrupting
|
||||||
underlying store. (asuresh via tucu)
|
underlying store. (asuresh via tucu)
|
||||||
|
|
||||||
|
HADOOP-10282. Create a FairCallQueue: a multi-level call queue which
|
||||||
|
schedules incoming calls and multiplexes outgoing calls. (Chris Li via
|
||||||
|
Arpit Agarwal)
|
||||||
|
|
||||||
BUG FIXES
|
BUG FIXES
|
||||||
|
|
||||||
HADOOP-10781. Unportable getgrouplist() usage breaks FreeBSD (Dmitry
|
HADOOP-10781. Unportable getgrouplist() usage breaks FreeBSD (Dmitry
|
||||||
|
|
|
@ -0,0 +1,449 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hadoop.ipc;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.AbstractQueue;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.concurrent.locks.Condition;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.apache.commons.lang.NotImplementedException;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.metrics2.util.MBeans;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A queue with multiple levels for each priority.
|
||||||
|
*/
|
||||||
|
public class FairCallQueue<E extends Schedulable> extends AbstractQueue<E>
|
||||||
|
implements BlockingQueue<E> {
|
||||||
|
// Configuration Keys
|
||||||
|
public static final int IPC_CALLQUEUE_PRIORITY_LEVELS_DEFAULT = 4;
|
||||||
|
public static final String IPC_CALLQUEUE_PRIORITY_LEVELS_KEY =
|
||||||
|
"faircallqueue.priority-levels";
|
||||||
|
|
||||||
|
public static final Log LOG = LogFactory.getLog(FairCallQueue.class);
|
||||||
|
|
||||||
|
/* The queues */
|
||||||
|
private final ArrayList<BlockingQueue<E>> queues;
|
||||||
|
|
||||||
|
/* Read locks */
|
||||||
|
private final ReentrantLock takeLock = new ReentrantLock();
|
||||||
|
private final Condition notEmpty = takeLock.newCondition();
|
||||||
|
private void signalNotEmpty() {
|
||||||
|
takeLock.lock();
|
||||||
|
try {
|
||||||
|
notEmpty.signal();
|
||||||
|
} finally {
|
||||||
|
takeLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scheduler picks which queue to place in */
|
||||||
|
private RpcScheduler scheduler;
|
||||||
|
|
||||||
|
/* Multiplexer picks which queue to draw from */
|
||||||
|
private RpcMultiplexer multiplexer;
|
||||||
|
|
||||||
|
/* Statistic tracking */
|
||||||
|
private final ArrayList<AtomicLong> overflowedCalls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a FairCallQueue.
|
||||||
|
* @param capacity the maximum size of each sub-queue
|
||||||
|
* @param ns the prefix to use for configuration
|
||||||
|
* @param conf the configuration to read from
|
||||||
|
* Notes: the FairCallQueue has no fixed capacity. Rather, it has a minimum
|
||||||
|
* capacity of `capacity` and a maximum capacity of `capacity * number_queues`
|
||||||
|
*/
|
||||||
|
public FairCallQueue(int capacity, String ns, Configuration conf) {
|
||||||
|
int numQueues = parseNumQueues(ns, conf);
|
||||||
|
LOG.info("FairCallQueue is in use with " + numQueues + " queues.");
|
||||||
|
|
||||||
|
this.queues = new ArrayList<BlockingQueue<E>>(numQueues);
|
||||||
|
this.overflowedCalls = new ArrayList<AtomicLong>(numQueues);
|
||||||
|
|
||||||
|
for(int i=0; i < numQueues; i++) {
|
||||||
|
this.queues.add(new LinkedBlockingQueue<E>(capacity));
|
||||||
|
this.overflowedCalls.add(new AtomicLong(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduler = new DecayRpcScheduler(numQueues, ns, conf);
|
||||||
|
this.multiplexer = new WeightedRoundRobinMultiplexer(numQueues, ns, conf);
|
||||||
|
|
||||||
|
// Make this the active source of metrics
|
||||||
|
MetricsProxy mp = MetricsProxy.getInstance(ns);
|
||||||
|
mp.setDelegate(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the number of queues from the configuration.
|
||||||
|
* This will affect the FairCallQueue's overall capacity.
|
||||||
|
* @throws IllegalArgumentException on invalid queue count
|
||||||
|
*/
|
||||||
|
private static int parseNumQueues(String ns, Configuration conf) {
|
||||||
|
int retval = conf.getInt(ns + "." + IPC_CALLQUEUE_PRIORITY_LEVELS_KEY,
|
||||||
|
IPC_CALLQUEUE_PRIORITY_LEVELS_DEFAULT);
|
||||||
|
if(retval < 1) {
|
||||||
|
throw new IllegalArgumentException("numQueues must be at least 1");
|
||||||
|
}
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first non-empty queue with equal or lesser priority
|
||||||
|
* than <i>startIdx</i>. Wraps around, searching a maximum of N
|
||||||
|
* queues, where N is this.queues.size().
|
||||||
|
*
|
||||||
|
* @param startIdx the queue number to start searching at
|
||||||
|
* @return the first non-empty queue with less priority, or null if
|
||||||
|
* everything was empty
|
||||||
|
*/
|
||||||
|
private BlockingQueue<E> getFirstNonEmptyQueue(int startIdx) {
|
||||||
|
final int numQueues = this.queues.size();
|
||||||
|
for(int i=0; i < numQueues; i++) {
|
||||||
|
int idx = (i + startIdx) % numQueues; // offset and wrap around
|
||||||
|
BlockingQueue<E> queue = this.queues.get(idx);
|
||||||
|
if (queue.size() != 0) {
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All queues were empty
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AbstractQueue and BlockingQueue methods */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Put and offer follow the same pattern:
|
||||||
|
* 1. Get a priorityLevel from the scheduler
|
||||||
|
* 2. Get the nth sub-queue matching this priorityLevel
|
||||||
|
* 3. delegate the call to this sub-queue.
|
||||||
|
*
|
||||||
|
* But differ in how they handle overflow:
|
||||||
|
* - Put will move on to the next queue until it lands on the last queue
|
||||||
|
* - Offer does not attempt other queues on overflow
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void put(E e) throws InterruptedException {
|
||||||
|
int priorityLevel = scheduler.getPriorityLevel(e);
|
||||||
|
|
||||||
|
final int numLevels = this.queues.size();
|
||||||
|
while (true) {
|
||||||
|
BlockingQueue<E> q = this.queues.get(priorityLevel);
|
||||||
|
boolean res = q.offer(e);
|
||||||
|
if (!res) {
|
||||||
|
// Update stats
|
||||||
|
this.overflowedCalls.get(priorityLevel).getAndIncrement();
|
||||||
|
|
||||||
|
// If we failed to insert, try again on the next level
|
||||||
|
priorityLevel++;
|
||||||
|
|
||||||
|
if (priorityLevel == numLevels) {
|
||||||
|
// That was the last one, we will block on put in the last queue
|
||||||
|
// Delete this line to drop the call
|
||||||
|
this.queues.get(priorityLevel-1).put(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
signalNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean offer(E e, long timeout, TimeUnit unit)
|
||||||
|
throws InterruptedException {
|
||||||
|
int priorityLevel = scheduler.getPriorityLevel(e);
|
||||||
|
BlockingQueue<E> q = this.queues.get(priorityLevel);
|
||||||
|
boolean ret = q.offer(e, timeout, unit);
|
||||||
|
|
||||||
|
signalNotEmpty();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean offer(E e) {
|
||||||
|
int priorityLevel = scheduler.getPriorityLevel(e);
|
||||||
|
BlockingQueue<E> q = this.queues.get(priorityLevel);
|
||||||
|
boolean ret = q.offer(e);
|
||||||
|
|
||||||
|
signalNotEmpty();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public E take() throws InterruptedException {
|
||||||
|
int startIdx = this.multiplexer.getAndAdvanceCurrentIndex();
|
||||||
|
|
||||||
|
takeLock.lockInterruptibly();
|
||||||
|
try {
|
||||||
|
// Wait while queue is empty
|
||||||
|
for (;;) {
|
||||||
|
BlockingQueue<E> q = this.getFirstNonEmptyQueue(startIdx);
|
||||||
|
if (q != null) {
|
||||||
|
// Got queue, so return if we can poll out an object
|
||||||
|
E e = q.poll();
|
||||||
|
if (e != null) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notEmpty.await();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
takeLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public E poll(long timeout, TimeUnit unit)
|
||||||
|
throws InterruptedException {
|
||||||
|
|
||||||
|
int startIdx = this.multiplexer.getAndAdvanceCurrentIndex();
|
||||||
|
|
||||||
|
long nanos = unit.toNanos(timeout);
|
||||||
|
takeLock.lockInterruptibly();
|
||||||
|
try {
|
||||||
|
for (;;) {
|
||||||
|
BlockingQueue<E> q = this.getFirstNonEmptyQueue(startIdx);
|
||||||
|
if (q != null) {
|
||||||
|
E e = q.poll();
|
||||||
|
if (e != null) {
|
||||||
|
// Escape condition: there might be something available
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nanos <= 0) {
|
||||||
|
// Wait has elapsed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Now wait on the condition for a bit. If we get
|
||||||
|
// spuriously awoken we'll re-loop
|
||||||
|
nanos = notEmpty.awaitNanos(nanos);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
notEmpty.signal(); // propagate to a non-interrupted thread
|
||||||
|
throw ie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
takeLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* poll() provides no strict consistency: it is possible for poll to return
|
||||||
|
* null even though an element is in the queue.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public E poll() {
|
||||||
|
int startIdx = this.multiplexer.getAndAdvanceCurrentIndex();
|
||||||
|
|
||||||
|
BlockingQueue<E> q = this.getFirstNonEmptyQueue(startIdx);
|
||||||
|
if (q == null) {
|
||||||
|
return null; // everything is empty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to the sub-queue's poll, which could still return null
|
||||||
|
return q.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peek, like poll, provides no strict consistency.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public E peek() {
|
||||||
|
BlockingQueue<E> q = this.getFirstNonEmptyQueue(0);
|
||||||
|
if (q == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return q.peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size returns the sum of all sub-queue sizes, so it may be greater than
|
||||||
|
* capacity.
|
||||||
|
* Note: size provides no strict consistency, and should not be used to
|
||||||
|
* control queue IO.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
int size = 0;
|
||||||
|
for (BlockingQueue q : this.queues) {
|
||||||
|
size += q.size();
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator is not implemented, as it is not needed.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Iterator<E> iterator() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* drainTo defers to each sub-queue. Note that draining from a FairCallQueue
|
||||||
|
* to another FairCallQueue will likely fail, since the incoming calls
|
||||||
|
* may be scheduled differently in the new FairCallQueue. Nonetheless this
|
||||||
|
* method is provided for completeness.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int drainTo(Collection<? super E> c, int maxElements) {
|
||||||
|
int sum = 0;
|
||||||
|
for (BlockingQueue<E> q : this.queues) {
|
||||||
|
sum += q.drainTo(c, maxElements);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int drainTo(Collection<? super E> c) {
|
||||||
|
int sum = 0;
|
||||||
|
for (BlockingQueue<E> q : this.queues) {
|
||||||
|
sum += q.drainTo(c);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns maximum remaining capacity. This does not reflect how much you can
|
||||||
|
* ideally fit in this FairCallQueue, as that would depend on the scheduler's
|
||||||
|
* decisions.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int remainingCapacity() {
|
||||||
|
int sum = 0;
|
||||||
|
for (BlockingQueue q : this.queues) {
|
||||||
|
sum += q.remainingCapacity();
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetricsProxy is a singleton because we may init multiple
|
||||||
|
* FairCallQueues, but the metrics system cannot unregister beans cleanly.
|
||||||
|
*/
|
||||||
|
private static final class MetricsProxy implements FairCallQueueMXBean {
|
||||||
|
// One singleton per namespace
|
||||||
|
private static final HashMap<String, MetricsProxy> INSTANCES =
|
||||||
|
new HashMap<String, MetricsProxy>();
|
||||||
|
|
||||||
|
// Weakref for delegate, so we don't retain it forever if it can be GC'd
|
||||||
|
private WeakReference<FairCallQueue> delegate;
|
||||||
|
|
||||||
|
// Keep track of how many objects we registered
|
||||||
|
private int revisionNumber = 0;
|
||||||
|
|
||||||
|
private MetricsProxy(String namespace) {
|
||||||
|
MBeans.register(namespace, "FairCallQueue", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized MetricsProxy getInstance(String namespace) {
|
||||||
|
MetricsProxy mp = INSTANCES.get(namespace);
|
||||||
|
if (mp == null) {
|
||||||
|
// We must create one
|
||||||
|
mp = new MetricsProxy(namespace);
|
||||||
|
INSTANCES.put(namespace, mp);
|
||||||
|
}
|
||||||
|
return mp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDelegate(FairCallQueue obj) {
|
||||||
|
this.delegate = new WeakReference<FairCallQueue>(obj);
|
||||||
|
this.revisionNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getQueueSizes() {
|
||||||
|
FairCallQueue obj = this.delegate.get();
|
||||||
|
if (obj == null) {
|
||||||
|
return new int[]{};
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj.getQueueSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long[] getOverflowedCalls() {
|
||||||
|
FairCallQueue obj = this.delegate.get();
|
||||||
|
if (obj == null) {
|
||||||
|
return new long[]{};
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj.getOverflowedCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int getRevision() {
|
||||||
|
return revisionNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FairCallQueueMXBean
|
||||||
|
public int[] getQueueSizes() {
|
||||||
|
int numQueues = queues.size();
|
||||||
|
int[] sizes = new int[numQueues];
|
||||||
|
for (int i=0; i < numQueues; i++) {
|
||||||
|
sizes[i] = queues.get(i).size();
|
||||||
|
}
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long[] getOverflowedCalls() {
|
||||||
|
int numQueues = queues.size();
|
||||||
|
long[] calls = new long[numQueues];
|
||||||
|
for (int i=0; i < numQueues; i++) {
|
||||||
|
calls[i] = overflowedCalls.get(i).get();
|
||||||
|
}
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For testing
|
||||||
|
@VisibleForTesting
|
||||||
|
public void setScheduler(RpcScheduler newScheduler) {
|
||||||
|
this.scheduler = newScheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public void setMultiplexer(RpcMultiplexer newMux) {
|
||||||
|
this.multiplexer = newMux;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hadoop.ipc;
|
||||||
|
|
||||||
|
public interface FairCallQueueMXBean {
|
||||||
|
// Get the size of each subqueue, the index corrosponding to the priority
|
||||||
|
// level.
|
||||||
|
int[] getQueueSizes();
|
||||||
|
long[] getOverflowedCalls();
|
||||||
|
int getRevision();
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hadoop.ipc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement this interface to make a pluggable multiplexer in the
|
||||||
|
* FairCallQueue.
|
||||||
|
*/
|
||||||
|
public interface RpcMultiplexer {
|
||||||
|
/**
|
||||||
|
* Should get current index and optionally perform whatever is needed
|
||||||
|
* to prepare the next index.
|
||||||
|
* @return current index
|
||||||
|
*/
|
||||||
|
int getAndAdvanceCurrentIndex();
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ import org.apache.hadoop.conf.Configuration;
|
||||||
* There may be more reads than the minimum due to race conditions. This is
|
* There may be more reads than the minimum due to race conditions. This is
|
||||||
* allowed by design for performance reasons.
|
* allowed by design for performance reasons.
|
||||||
*/
|
*/
|
||||||
public class WeightedRoundRobinMultiplexer {
|
public class WeightedRoundRobinMultiplexer implements RpcMultiplexer {
|
||||||
// Config keys
|
// Config keys
|
||||||
public static final String IPC_CALLQUEUE_WRRMUX_WEIGHTS_KEY =
|
public static final String IPC_CALLQUEUE_WRRMUX_WEIGHTS_KEY =
|
||||||
"faircallqueue.multiplexer.weights";
|
"faircallqueue.multiplexer.weights";
|
||||||
|
|
|
@ -0,0 +1,392 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hadoop.ipc;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
|
import org.apache.hadoop.security.UserGroupInformation;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.mockito.Matchers;
|
||||||
|
|
||||||
|
import static org.apache.hadoop.ipc.FairCallQueue.IPC_CALLQUEUE_PRIORITY_LEVELS_KEY;
|
||||||
|
|
||||||
|
public class TestFairCallQueue extends TestCase {
|
||||||
|
private FairCallQueue<Schedulable> fcq;
|
||||||
|
|
||||||
|
private Schedulable mockCall(String id) {
|
||||||
|
Schedulable mockCall = mock(Schedulable.class);
|
||||||
|
UserGroupInformation ugi = mock(UserGroupInformation.class);
|
||||||
|
|
||||||
|
when(ugi.getUserName()).thenReturn(id);
|
||||||
|
when(mockCall.getUserGroupInformation()).thenReturn(ugi);
|
||||||
|
|
||||||
|
return mockCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A scheduler which always schedules into priority zero
|
||||||
|
private RpcScheduler alwaysZeroScheduler;
|
||||||
|
{
|
||||||
|
RpcScheduler sched = mock(RpcScheduler.class);
|
||||||
|
when(sched.getPriorityLevel(Matchers.<Schedulable>any())).thenReturn(0); // always queue 0
|
||||||
|
alwaysZeroScheduler = sched;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUp() {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
conf.setInt("ns." + IPC_CALLQUEUE_PRIORITY_LEVELS_KEY, 2);
|
||||||
|
|
||||||
|
fcq = new FairCallQueue<Schedulable>(5, "ns", conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Ensure that FairCallQueue properly implements BlockingQueue
|
||||||
|
//
|
||||||
|
public void testPollReturnsNullWhenEmpty() {
|
||||||
|
assertNull(fcq.poll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPollReturnsTopCallWhenNotEmpty() {
|
||||||
|
Schedulable call = mockCall("c");
|
||||||
|
assertTrue(fcq.offer(call));
|
||||||
|
|
||||||
|
assertEquals(call, fcq.poll());
|
||||||
|
|
||||||
|
// Poll took it out so the fcq is empty
|
||||||
|
assertEquals(0, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOfferSucceeds() {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
// We can fit 10 calls
|
||||||
|
assertTrue(fcq.offer(mockCall("c")));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(5, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOfferFailsWhenFull() {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
for (int i = 0; i < 5; i++) { assertTrue(fcq.offer(mockCall("c"))); }
|
||||||
|
|
||||||
|
assertFalse(fcq.offer(mockCall("c"))); // It's full
|
||||||
|
|
||||||
|
assertEquals(5, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOfferSucceedsWhenScheduledLowPriority() {
|
||||||
|
// Scheduler will schedule into queue 0 x 5, then queue 1
|
||||||
|
RpcScheduler sched = mock(RpcScheduler.class);
|
||||||
|
when(sched.getPriorityLevel(Matchers.<Schedulable>any())).thenReturn(0, 0, 0, 0, 0, 1, 0);
|
||||||
|
fcq.setScheduler(sched);
|
||||||
|
for (int i = 0; i < 5; i++) { assertTrue(fcq.offer(mockCall("c"))); }
|
||||||
|
|
||||||
|
assertTrue(fcq.offer(mockCall("c")));
|
||||||
|
|
||||||
|
assertEquals(6, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPeekNullWhenEmpty() {
|
||||||
|
assertNull(fcq.peek());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPeekNonDestructive() {
|
||||||
|
Schedulable call = mockCall("c");
|
||||||
|
assertTrue(fcq.offer(call));
|
||||||
|
|
||||||
|
assertEquals(call, fcq.peek());
|
||||||
|
assertEquals(call, fcq.peek()); // Non-destructive
|
||||||
|
assertEquals(1, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPeekPointsAtHead() {
|
||||||
|
Schedulable call = mockCall("c");
|
||||||
|
Schedulable next = mockCall("b");
|
||||||
|
fcq.offer(call);
|
||||||
|
fcq.offer(next);
|
||||||
|
|
||||||
|
assertEquals(call, fcq.peek()); // Peek points at the head
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPollTimeout() throws InterruptedException {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
|
||||||
|
assertNull(fcq.poll(10, TimeUnit.MILLISECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPollSuccess() throws InterruptedException {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
|
||||||
|
Schedulable call = mockCall("c");
|
||||||
|
assertTrue(fcq.offer(call));
|
||||||
|
|
||||||
|
assertEquals(call, fcq.poll(10, TimeUnit.MILLISECONDS));
|
||||||
|
|
||||||
|
assertEquals(0, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOfferTimeout() throws InterruptedException {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
assertTrue(fcq.offer(mockCall("c"), 10, TimeUnit.MILLISECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(fcq.offer(mockCall("e"), 10, TimeUnit.MILLISECONDS)); // It's full
|
||||||
|
|
||||||
|
assertEquals(5, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDrainTo() {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
conf.setInt("ns." + IPC_CALLQUEUE_PRIORITY_LEVELS_KEY, 2);
|
||||||
|
FairCallQueue<Schedulable> fcq2 = new FairCallQueue<Schedulable>(10, "ns", conf);
|
||||||
|
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
fcq2.setScheduler(alwaysZeroScheduler);
|
||||||
|
|
||||||
|
// Start with 3 in fcq, to be drained
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
fcq.offer(mockCall("c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fcq.drainTo(fcq2);
|
||||||
|
|
||||||
|
assertEquals(0, fcq.size());
|
||||||
|
assertEquals(3, fcq2.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDrainToWithLimit() {
|
||||||
|
Configuration conf = new Configuration();
|
||||||
|
conf.setInt("ns." + IPC_CALLQUEUE_PRIORITY_LEVELS_KEY, 2);
|
||||||
|
FairCallQueue<Schedulable> fcq2 = new FairCallQueue<Schedulable>(10, "ns", conf);
|
||||||
|
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
fcq2.setScheduler(alwaysZeroScheduler);
|
||||||
|
|
||||||
|
// Start with 3 in fcq, to be drained
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
fcq.offer(mockCall("c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fcq.drainTo(fcq2, 2);
|
||||||
|
|
||||||
|
assertEquals(1, fcq.size());
|
||||||
|
assertEquals(2, fcq2.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testInitialRemainingCapacity() {
|
||||||
|
assertEquals(10, fcq.remainingCapacity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testFirstQueueFullRemainingCapacity() {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
while (fcq.offer(mockCall("c"))) ; // Queue 0 will fill up first, then queue 1
|
||||||
|
|
||||||
|
assertEquals(5, fcq.remainingCapacity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testAllQueuesFullRemainingCapacity() {
|
||||||
|
RpcScheduler sched = mock(RpcScheduler.class);
|
||||||
|
when(sched.getPriorityLevel(Matchers.<Schedulable>any())).thenReturn(0, 0, 0, 0, 0, 1, 1, 1, 1, 1);
|
||||||
|
fcq.setScheduler(sched);
|
||||||
|
while (fcq.offer(mockCall("c"))) ;
|
||||||
|
|
||||||
|
assertEquals(0, fcq.remainingCapacity());
|
||||||
|
assertEquals(10, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testQueuesPartialFilledRemainingCapacity() {
|
||||||
|
RpcScheduler sched = mock(RpcScheduler.class);
|
||||||
|
when(sched.getPriorityLevel(Matchers.<Schedulable>any())).thenReturn(0, 1, 0, 1, 0);
|
||||||
|
fcq.setScheduler(sched);
|
||||||
|
for (int i = 0; i < 5; i++) { fcq.offer(mockCall("c")); }
|
||||||
|
|
||||||
|
assertEquals(5, fcq.remainingCapacity());
|
||||||
|
assertEquals(5, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Putter produces FakeCalls
|
||||||
|
*/
|
||||||
|
public class Putter implements Runnable {
|
||||||
|
private final BlockingQueue<Schedulable> cq;
|
||||||
|
|
||||||
|
public final String tag;
|
||||||
|
public volatile int callsAdded = 0; // How many calls we added, accurate unless interrupted
|
||||||
|
private final int maxCalls;
|
||||||
|
|
||||||
|
public Putter(BlockingQueue<Schedulable> aCq, int maxCalls, String tag) {
|
||||||
|
this.maxCalls = maxCalls;
|
||||||
|
this.cq = aCq;
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTag() {
|
||||||
|
if (this.tag != null) return this.tag;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// Fill up to max (which is infinite if maxCalls < 0)
|
||||||
|
while (callsAdded < maxCalls || maxCalls < 0) {
|
||||||
|
cq.put(mockCall(getTag()));
|
||||||
|
callsAdded++;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taker consumes FakeCalls
|
||||||
|
*/
|
||||||
|
public class Taker implements Runnable {
|
||||||
|
private final BlockingQueue<Schedulable> cq;
|
||||||
|
|
||||||
|
public final String tag; // if >= 0 means we will only take the matching tag, and put back
|
||||||
|
// anything else
|
||||||
|
public volatile int callsTaken = 0; // total calls taken, accurate if we aren't interrupted
|
||||||
|
public volatile Schedulable lastResult = null; // the last thing we took
|
||||||
|
private final int maxCalls; // maximum calls to take
|
||||||
|
|
||||||
|
private IdentityProvider uip;
|
||||||
|
|
||||||
|
public Taker(BlockingQueue<Schedulable> aCq, int maxCalls, String tag) {
|
||||||
|
this.maxCalls = maxCalls;
|
||||||
|
this.cq = aCq;
|
||||||
|
this.tag = tag;
|
||||||
|
this.uip = new UserIdentityProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// Take while we don't exceed maxCalls, or if maxCalls is undefined (< 0)
|
||||||
|
while (callsTaken < maxCalls || maxCalls < 0) {
|
||||||
|
Schedulable res = cq.take();
|
||||||
|
String identity = uip.makeIdentity(res);
|
||||||
|
|
||||||
|
if (tag != null && this.tag.equals(identity)) {
|
||||||
|
// This call does not match our tag, we should put it back and try again
|
||||||
|
cq.put(res);
|
||||||
|
} else {
|
||||||
|
callsTaken++;
|
||||||
|
lastResult = res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert we can take exactly the numberOfTakes
|
||||||
|
public void assertCanTake(BlockingQueue<Schedulable> cq, int numberOfTakes,
|
||||||
|
int takeAttempts) throws InterruptedException {
|
||||||
|
|
||||||
|
Taker taker = new Taker(cq, takeAttempts, "default");
|
||||||
|
Thread t = new Thread(taker);
|
||||||
|
t.start();
|
||||||
|
t.join(100);
|
||||||
|
|
||||||
|
assertEquals(numberOfTakes, taker.callsTaken);
|
||||||
|
t.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert we can put exactly the numberOfPuts
|
||||||
|
public void assertCanPut(BlockingQueue<Schedulable> cq, int numberOfPuts,
|
||||||
|
int putAttempts) throws InterruptedException {
|
||||||
|
|
||||||
|
Putter putter = new Putter(cq, putAttempts, null);
|
||||||
|
Thread t = new Thread(putter);
|
||||||
|
t.start();
|
||||||
|
t.join(100);
|
||||||
|
|
||||||
|
assertEquals(numberOfPuts, putter.callsAdded);
|
||||||
|
t.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure put will overflow into lower queues when the top is full
|
||||||
|
public void testPutOverflows() throws InterruptedException {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
|
||||||
|
// We can fit more than 5, even though the scheduler suggests the top queue
|
||||||
|
assertCanPut(fcq, 8, 8);
|
||||||
|
assertEquals(8, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPutBlocksWhenAllFull() throws InterruptedException {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
|
||||||
|
assertCanPut(fcq, 10, 10); // Fill up
|
||||||
|
assertEquals(10, fcq.size());
|
||||||
|
|
||||||
|
// Put more which causes overflow
|
||||||
|
assertCanPut(fcq, 0, 1); // Will block
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTakeBlocksWhenEmpty() throws InterruptedException {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
assertCanTake(fcq, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTakeRemovesCall() throws InterruptedException {
|
||||||
|
fcq.setScheduler(alwaysZeroScheduler);
|
||||||
|
Schedulable call = mockCall("c");
|
||||||
|
fcq.offer(call);
|
||||||
|
|
||||||
|
assertEquals(call, fcq.take());
|
||||||
|
assertEquals(0, fcq.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTakeTriesNextQueue() throws InterruptedException {
|
||||||
|
// Make a FCQ filled with calls in q 1 but empty in q 0
|
||||||
|
RpcScheduler q1Scheduler = mock(RpcScheduler.class);
|
||||||
|
when(q1Scheduler.getPriorityLevel(Matchers.<Schedulable>any())).thenReturn(1);
|
||||||
|
fcq.setScheduler(q1Scheduler);
|
||||||
|
|
||||||
|
// A mux which only draws from q 0
|
||||||
|
RpcMultiplexer q0mux = mock(RpcMultiplexer.class);
|
||||||
|
when(q0mux.getAndAdvanceCurrentIndex()).thenReturn(0);
|
||||||
|
fcq.setMultiplexer(q0mux);
|
||||||
|
|
||||||
|
Schedulable call = mockCall("c");
|
||||||
|
fcq.put(call);
|
||||||
|
|
||||||
|
// Take from q1 even though mux said q0, since q0 empty
|
||||||
|
assertEquals(call, fcq.take());
|
||||||
|
assertEquals(0, fcq.size());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue