/*
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
licenses@blazegraph.com
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* Created on Apr 16, 2009
*/
package com.bigdata.service.ndx.pipeline;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase2;
import com.bigdata.btree.IndexMetadata;
import com.bigdata.btree.keys.KVO;
import com.bigdata.mdi.IMetadataIndex;
import com.bigdata.mdi.IPartitionMetadata;
import com.bigdata.mdi.MetadataIndex;
import com.bigdata.mdi.PartitionLocator;
import com.bigdata.rawstore.SimpleMemoryRawStore;
import com.bigdata.relation.accesspath.BlockingBuffer;
import com.bigdata.service.Split;
import com.bigdata.service.ndx.AbstractSplitter;
import com.bigdata.service.ndx.ISplitter;
import com.bigdata.util.DaemonThreadFactory;
/**
* Abstract base class for test suites for the {@link AbstractMasterTask} and
* friends using {@link IPartitionMetadata} locators.
* <p>
* Note: There are a bunch of inner classes which have the same names as the
* generic types used by the master and subtask classes. This makes it much
* easier to instantiate these things since all of the generic variety has been
* factored out.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @version $Id$
*/
public class AbstractKeyRangeMasterTestCase extends TestCase2 {
/**
*
*/
public AbstractKeyRangeMasterTestCase() {
}
/**
* @param arg0
*/
public AbstractKeyRangeMasterTestCase(String arg0) {
super(arg0);
}
/**
* Mock stale locator exception.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @version $Id$
*/
static class MockStaleLocatorException extends RuntimeException {
private static final long serialVersionUID = 1L;
public MockStaleLocatorException(L locator) {
super(locator.toString());
}
}
/**
* Mapping from {@link UUID}s onto {@link DS data services}.
*/
protected final Map<UUID, DS> dataServices = new ConcurrentHashMap<UUID, DS>();
/**
* Choose a data service at random.
*/
protected DS getRandomDataService() {
final DS[] a = dataServices.values().toArray(new DS[] {});
final Random r = new Random();
return a[r.nextInt(a.length)];
}
/**
* A mock "data service". This class may be overridden to control the latency
* of the writes.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan
* Thompson</a>
* @version $Id$
*/
static class DS {
/**
* The UUID for this data service.
*/
protected final UUID uuid;
/**
* A collection of index partition identifiers which are no longer
* located on this data service.
* <p>
* Note: Explicit synchronization is required across accesses to
* {@link #staleLocators}, {@link #knownLocators}, and across
* {@link #writeChunk(L, KVO[])}.
*/
private final Set<Integer/* partitionId */> staleLocators = new HashSet<Integer>();
/**
* A collection of index partitions known to exist on this data service.
*/
private final Set<Integer/* partitionId */> knownLocators = new HashSet<Integer>();
public DS(UUID uuid) {
this.uuid = uuid;
}
/**
* Write a chunk onto the data service.
*
* @param locator
* Identifies the index partition to which the write request
* is directed.
* @param chunk
* The chunk to be written.
*/
final public void writeChunk(L locator, KVO<O>[] chunk)
throws MockStaleLocatorException {
synchronized (this) {
if (staleLocators.contains(locator.getPartitionId())) {
throw new MockStaleLocatorException(locator);
}
if (!knownLocators.contains(locator.getPartitionId())) {
throw new RuntimeException("Locator not registered on DS: "
+ locator);
}
}
// Note: outside of synchronized block.
acceptWrite(locator, chunk);
}
/**
* Accept a write (NOP). This may be overridden to impose latency. The
* caller has already determined that the chunk may be written on this
* data service for the given locator.
*
* @param locator
* The locator.
* @param chunk
* The chunk.
*/
protected void acceptWrite(L locator, KVO<O>[] chunk) {
}
/**
* Notify the data service that an index partition is now located on
* that data service.
*
* @param locator
* Identifies the index partition which is located on that
* data service.
*/
protected void notifyLocator(L locator) {
synchronized (this) {
if (!knownLocators.add(locator.getPartitionId())) {
throw new IllegalStateException(
"Already located on this DS: " + locator);
}
}
}
/**
* Notify the data service that an index partition is no longer located
* on that data service.
*
* @param locator
* The locator of the index partition which has been split,
* moved, or joined.
*/
protected void notifyGone(L locator) {
synchronized (this) {
if (!staleLocators.add(locator.getPartitionId())) {
fail("Locator already in stale locators collection? "
+ locator);
}
}
}
}
/**
* The locator maps an unsigned byte[] key-range onto a {@link UUID} which
* identifies a "data service" and an index partition identifier. The index
* partition is presumed to be located on that data service.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan
* Thompson</a>
* @version $Id$
*/
static class L extends PartitionLocator {
/**
* De-serialization ctor.
*/
public L() {
}
/**
* Core ctor impl.
*
* @param partitionId
* @param logicalDataServiceUUID
* @param leftSeparatorKey
* @param rightSeparatorKey
*/
public L(final int partitionId, final UUID logicalDataServiceUUID,
final byte[] leftSeparatorKey, final byte[] rightSeparatorKey) {
super(partitionId, logicalDataServiceUUID, leftSeparatorKey,
rightSeparatorKey);
}
}
static class HS extends MockSubtaskStats {
}
static class O extends Object {
}
static class H extends MockMasterStats<L, HS> {
@Override
protected HS newSubtaskStats(L locator) {
return new HS();
}
}
/**
* Concrete master impl w/o generic types.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @version $Id$
*/
//static
abstract class M extends MockMaster<H, O, KVO<O>, S, L, HS> {
private final ExecutorService executorService;
/** The metadata for the scale-out index. */
final IndexMetadata managedIndexMetadata = new IndexMetadata(
"test-ndx", UUID.randomUUID());
/**
* The metadata index for the scale-out index.
* */
final MetadataIndex mdi = MetadataIndex.create(
new SimpleMemoryRawStore(), UUID.randomUUID(),
managedIndexMetadata);
/**
* Lock used to provide atomic views on MDI for read (finding splits)
* and write (updating the split definitions).
*/
final ReentrantLock mdiLock = new ReentrantLock();
/**
* Splitter based on unsigned byte[] key-ranges using the {@link #mdi}.
*/
final ISplitter splitter = new AbstractSplitter() {
protected IMetadataIndex getMetadataIndex(long ts) {
return mdi;
};
@Override
public LinkedList<Split> splitKeys(final long ts, final int fromIndex,
final int toIndex, final byte[][] keys) {
mdiLock.lock();
try {
return super.splitKeys(ts, fromIndex, toIndex, keys);
} finally {
mdiLock.unlock();
}
}
@Override
public LinkedList<Split> splitKeys(final long ts, final int fromIndex,
final int toIndex, final KVO[] a) {
mdiLock.lock();
try {
return super.splitKeys(ts, fromIndex, toIndex, a);
} finally {
mdiLock.unlock();
}
}
};
public M(final H stats, final BlockingBuffer<KVO<O>[]> buffer,
final ExecutorService executorService, final long sinkIdleTimeout,
final long sinkPollTimeout) {
super(stats, buffer, sinkIdleTimeout, sinkPollTimeout);
this.executorService = executorService;
}
/**
* Overrides the subtask to write data on the "data service". The data
* service may be overridden to control the latency of the operation.
*/
@Override
protected S newSubtask(L locator, BlockingBuffer<KVO<O>[]> out) {
return new S(this, locator, out) {
protected void writeData(KVO<O>[] chunk) {
dataServices.get(locator.getDataServiceUUID())
.writeChunk(locator, chunk);
}
};
}
// /**
// * Handle a stale locator per {@link IndexWriteTask}.
// *
// * @deprecated by the use of the redirect queue.
// */
// @SuppressWarnings("unchecked")
// protected void handleStaleLocator(final S sink, final KVO<O>[] chunk,
// final MockStaleLocatorException cause) throws InterruptedException {
//
// // FIXME remove this method and rewrite the tests.
// throw new UnsupportedOperationException();
//
//// if (sink == null)
//// throw new IllegalArgumentException();
////
//// if (chunk == null)
//// throw new IllegalArgumentException();
////
//// if (cause == null)
//// throw new IllegalArgumentException();
////
//// lock.lockInterruptibly();
//// try {
////
//// stats.redirectCount++;
////
//// /*
//// * Note: We do not need to notify the "client" because the
//// * master is directly accessing the MDI object.
//// */
////// /*
////// * Notify the client so it can refresh the information for this
////// * locator.
////// */
////// ndx.staleLocator(ndx.getTimestamp(), (L) sink.locator, cause);
////
//// /*
//// * Redirect the chunk and anything in the buffer to the appropriate
//// * output sinks.
//// */
//// handleRedirect(sink, chunk);
////
//// /*
//// * Remove the buffer from the map
//// *
//// * Note: This could cause a concurrent modification error if we are
//// * awaiting the various output buffers to be closed. In order to
//// * handle that code that modifies or traverses the [buffers] map
//// * MUST be MUTEX or synchronized.
//// */
//// removeOutputBuffer((L) sink.locator, sink);
////
//// } finally {
////
//// lock.unlock();
////
//// }
//
// }
/**
* Identifies splits using the {@link IMetadataIndex} and assigns those
* splits to the appropriate output sinks.
*/
protected void keyRangePartition(final KVO<O>[] chunk,
final boolean reopen) throws InterruptedException {
/*
* Split the tuples.
*
* Note: The Splitter uses the mdiLock to avoid concurrent
* modification of the MDI while it is querying that index to
* determine how to split the chunk.
*/
final LinkedList<Split> splits = splitter.splitKeys(
0L/* timestampIsIgnored */, 0/* fromIndex */,
chunk.length/* toIndex */, chunk);
/*
* Assign tuples to output buffers based on those splits.
*/
for (Split split : splits) {
addToOutputBuffer((L) split.pmd, chunk, split.fromIndex,
split.toIndex, reopen);
}
}
/**
* This applies {@link #keyRangePartition(KVO[])} to map the chunk
* across the output buffers for the subtasks.
*/
@Override
protected void handleChunk(final KVO<O>[] chunk, final boolean reopen)
throws InterruptedException {
keyRangePartition(chunk, reopen);
}
@Override
protected void submitSubtask(
final FutureTask<? extends AbstractSubtaskStats> subtask) {
executorService.submit(subtask);
}
}
/**
* Concrete subtask impl w/o generic types.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @version $Id$
*/
static class S extends MockSubtask<H, O, KVO<O>, L, S, HS, M> {
public S(final M master, final L locator,
final BlockingBuffer<KVO<O>[]> buffer) {
super(master, locator, buffer);
}
/**
* This method may be overridden to simulate the latency of the
* write operation. The default is a NOP.
*/
protected void writeData(KVO<O>[] chunk) throws Exception {
}
@Override
protected boolean handleChunk(final KVO<O>[] chunk) throws Exception {
final long begin = System.nanoTime();
try {
writeData(chunk);
} catch (MockStaleLocatorException ex) {
log.warn("Stale locator: "+ex);
handleRedirect(chunk, ex);
// done.
return true;
}
final long elapsed = System.nanoTime() - begin;
synchronized (master.stats) {
master.stats.chunksOut.incrementAndGet();
master.stats.elementsOut.addAndGet(chunk.length);
master.stats.elapsedSinkChunkWritingNanos += elapsed;
}
synchronized (stats) {
stats.chunksOut.incrementAndGet();
stats.elementsOut.addAndGet(chunk.length);
stats.elapsedChunkWritingNanos += elapsed;
}
if (log.isInfoEnabled())
log.info("wrote chunk: " + this + ", #elements="
+ chunk.length);
// keep processing.
return false;
}
}
final H masterStats = new H();
final ExecutorService executorService = Executors
.newCachedThreadPool(DaemonThreadFactory.defaultThreadFactory());
protected void tearDown() {
executorService.shutdownNow();
}
/**
* Sleep up to the timeout or until the chunksOut takes on the specified
* value.
*
* @param master
* @param expectedChunksOut
* @param timeout
* @param unit
*
* @throws InterruptedException
* @throws AssertionFailedError
*/
protected void awaitChunksOut(final M master, final int expectedChunksOut,
final long timeout, final TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
while (nanos > 0) {
if (master.stats.chunksOut.get() >= expectedChunksOut) {
return;
}
Thread.sleep(1/* ms */);
}
fail("Timeout");
}
}