package com.bigdata.service;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.log4j.Logger;
/**
* A round robin implementation that may be used when there are no scores
* available. Services are selected using a round robin policy. The class will
* notice service joins and service leaves and will incorporate the new set of
* services into its decision making while preserving a (mostly) round robin
* behavior.
* <p>
* Note: This has internal state in order to provide a round-robin policy.
* Therefore the load balancer MUST use a single instance of this class for
* stateful behavior to be observed.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @version $Id$
*/
abstract public class AbstractRoundRobinServiceLoadHelper implements
IServiceLoadHelper {
protected static final Logger log = Logger
.getLogger(AbstractRoundRobinServiceLoadHelper.class);
protected static final boolean INFO = log.isInfoEnabled();
// protected static final boolean DEBUG = log.isDebugEnabled();
/**
* Used to provide a round robin behavior.
*/
private final AtomicInteger roundRobinIndex = new AtomicInteger();
protected AbstractRoundRobinServiceLoadHelper() {
}
/**
* Await the availability of at least the specified #of {@link IDataService}s.
*
* @param minCount
* The minimum #of data services.
* @param timeout
* The timeout (ms).
*
* @return An array #of the {@link UUID}s of the {@link IDataService}s
* that have been discovered. Note that at least <i>minDataServices</i>
* elements will be present in this array but that ALL discovered
* data services may be reported.
*
* @see AbstractScaleOutFederation#awaitServices(int, long)
*/
abstract protected UUID[] awaitServices(int minCount, long timeout)
throws InterruptedException, TimeoutException;
/**
* Issues {@link UUID}s using a round-robin over those that are joined. For
* this purpose, the joined {@link DataService}s are appended to an ordered
* set. The index of the most recently assigned service is maintained in a
* counter. Services that leave are removed from the set, but we do not
* bother to adjust the counter. We always return the {@link UUID} of the
* service at index MOD N, where N is the #of services that are joined at
* the time that this method looks at the set. We then post-increment the
* counter.
* <p>
* The round-robin allocate strategy is a good choice where there is little
* real data on the services, or when there is a set of services whose
* scores place them into an equivalence class such that we have no
* principled reason for preferring one service over another among those in
* the equivalence class.
*
* @throws TimeoutException
* @throws InterruptedException
*
* @todo develop the concept of equivalence classes further. divide the
* joined services into a set of equivalence classes. parameterize
* this method to accept an equivalence class. always apply this
* method when drawing from an equivalence class.
*
* @see TestLoadBalancerRoundRobin
*/
public UUID[] getUnderUtilizedDataServices(final int minCount,
final int maxCount, final UUID exclude)
throws InterruptedException, TimeoutException {
/*
* Note: In order to reduce the state that we track I've coded this to
* build the UUID[] array on demand from the joined services and then
* sort the UUIDs. This gives us a single additional item of state over
* that already recorded by the load balancer - the counter itself. This
* is just a pragmatic hack to get a round-robin behavior into place.
*/
if (INFO)
log.info("minCount=" + minCount + ", maxCount=" + maxCount
+ ", exclude=" + exclude);
/*
* Note: I've reduced this to only demand a single data service. If more
* are available then they are returned. If none are available then we
* can not proceed. If only one is available, then it will be assigned
* for each of the requested minCount UUIDs.
*/
final long timeout = 10000;// ms @todo config
UUID[] a = awaitServices(1/* minCount */, timeout);
if (a.length == 1 && exclude != null && exclude.equals(a[0])) {
/*
* Make sure that we have at least one unexcluded service.'
*
* @todo unit test for this bit.
*/
a = awaitServices(2 /* minCount */, timeout);
}
/*
* Note: We sort the UUIDs in order to provide a (mostly) stable
* ordering. The set of UUIDs CAN change through service joins and
* service leaves. The [roundRobinIndex] maintains the position of the
* last UUID returned from those available and we will start serving
* again from that position.
*/
Arrays.sort(a);
final int n; // = Math.min(maxCount, a.length);
if (minCount == 0 && maxCount == 0) {
/*
* When there are no constraints we want to make any recommendations
* that we can.
*/
n = a.length;
} else if (a.length > minCount) {
/*
* No more than the maxCount (note that maxCount may be zero in
* which case we want to have no more than the minCount).
*/
n = Math.min(a.length, Math.max(minCount, maxCount));
} else {
// no less than the minCount.
n = minCount;
}
if (INFO) {
log.info("Have " + a.length + " data services and will make " + n
+ " assignments.");
}
final UUID[] b = new UUID[ n ];
int i = 0;
int nexcluded = 0; // set to ONE iff we see the excluded service.
while (i + nexcluded < b.length) {
final UUID uuid = a[roundRobinIndex.getAndIncrement() % a.length];
assert uuid != null;
if (exclude != null && uuid.equals(exclude)) {
nexcluded = 1;
continue;
}
b[i++] = uuid;
}
final UUID[] c;
if (nexcluded == 0) {
c = b;
} else {
// make dense.
c = new UUID[n - 1];
System.arraycopy(b, 0, c, 0, n - 1);
}
if(INFO) {
log.info("Assigned UUIDs: "+Arrays.toString(c));
}
return c;
}
}