package org.radargun.stages.cache;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import org.radargun.DistStageAck;
import org.radargun.StageResult;
import org.radargun.config.Property;
import org.radargun.config.Stage;
import org.radargun.reporting.Report;
import org.radargun.stages.AbstractDistStage;
import org.radargun.state.SlaveState;
import org.radargun.stats.BasicOperationStats;
import org.radargun.stats.BasicStatistics;
import org.radargun.stats.Request;
import org.radargun.stats.Statistics;
import org.radargun.traits.BasicOperations;
import org.radargun.traits.BulkOperations;
import org.radargun.traits.CacheInformation;
import org.radargun.traits.InjectTrait;
import org.radargun.utils.TimeService;
import org.radargun.utils.Utils;
/**
* <p>
* Generates random data to fill the cache. The seed used to instantiate the java.util.Random object
* can be specified, so that the same data is generated between runs. To specify generating a fixed
* amount of data in the cache, specify the valueSize and valueCount parameters. The number of
* values will be divided by the return value of <code>getActiveSlaveCount()</code>. To generate
* data based on the amount of available RAM, specify the valueSize and ramPercentage parameters.
* The amount of free memory on each node will be calculated and then used to determine the number
* of values that are written by the node.
* </p>
*
* <p>
* To add a precise amount of data to the cache, you need to be aware of the storage overhead. For a
* byte array, each value needs an additional 152 bytes. When <code>stringData</code> is enabled,
* the values will require 2 * <code>valueSize</code> bytes + the additional 152 bytes. Keep these
* values in mind when calculating the <code>valueCount</code>.
* </p>
*
* @author Alan Field <afield@redhat.com>
*/
@Stage(doc = "Generates random data to fill the cache.")
public class RandomDataStage extends AbstractDistStage {
public static final String RANDOMDATA_TOTALBYTES_KEY = "randomDataTotalBytes";
public static final String RANDOMDATA_CLUSTER_WORDCOUNT_KEY = "randomDataClusterWordcount";
@Property(doc = "The seed to use for the java.util.Random object. "
+ "The default is the return value of Calendar.getInstance().getWeekYear().")
public long randomSeed = Calendar.getInstance().getWeekYear();
@Property(doc = "The size of the values to put into the cache. The default size is 1MB (1024 * 1024).")
public int valueSize = 1024 * 1024;
@Property(doc = "The number of values of valueSize to write to the cache. "
+ "Either valueCount or ramPercentageDataSize should be specified, but not both.")
public long valueCount = -1;
@Property(doc = "A double that represents the percentage of the total Java heap "
+ "used to determine the amount of data to put into the cache. "
+ "Either valueCount or ramPercentageDataSize should be specified, but not both.")
public double ramPercentage = -1;
@Property(doc = "The name of the bucket where keys are written. The default is null.")
public String bucket = null;
@Property(doc = "If true, then String objects with printable characters are written to the cache."
+ "The default is false")
public boolean stringData = false;
@Property(doc = "If true, then the time for each put operation is written to the logs. The default is false.")
public boolean printWriteStatistics = false;
@Property(doc = "If true, then the random word generator selects a word from a pre-defined list. "
+ "The default is false.")
public boolean limitWordCount = false;
@Property(doc = "The maximum number of words to generate in the pre-defined list of words used with limitWordCount."
+ "The default is 100.")
public int maxWordCount = 100;
@Property(doc = "The maximum number of characters allowed in a word. The default is 20.")
public int maxWordLength = 20;
@Property(doc = "If false, then each node in the cluster generates a list of maxWordCount words. "
+ "If true, then each node in the cluster shares the same list of words. The default is false.")
public boolean shareWords = false;
/*
* From http://infinispan.blogspot.com/2013/01/infinispan-memory-overhead.html and
* http://infinispan.blogspot.com/2013/07/lower-memory-overhead-in-infinispan.html
*/
@Property(doc = "The bytes used over the size of the key and value when "
+ "putting to the cache. By default the stage retrieves the value from cache wrapper automatically.")
public int valueByteOverhead = -1;
@Property(doc = "The number of bytes to write to the cache when the valueByteOverhead, "
+ "stringData, and valueSize are taken into account. The code assumes this is an "
+ "even multiple of valueSize plus valueByteOverhead. If stringData is true, then "
+ "the code assumes this is an even multiple of (2 * valueSize) plus valueByteOverhead.")
public long targetMemoryUse = -1;
@Property(doc = "The number of times to retry a put if it fails. Default is 10.")
public int putRetryCount = 10;
@Property(doc = "The maximum number of seconds to sleep before retrying a failed put command. The default is 5.")
public int maxSleepInterval = 5;
@Property(doc = "Size of batch to be loaded into cache (using putAll). If <= 0, put() operation is used sequentially. Default is 0.")
public int batchSize = 0;
@Property(doc = "Controls whether batch insertion is performed in asychronous way. Default is false (prefer synchronous operations).")
public boolean useAsyncBatchLoading = false;
@Property(name = "statistics", doc = "Type of gathered statistics. Default are the 'default' statistics " +
"(fixed size memory footprint for each operation).", complexConverter = Statistics.Converter.class)
private Statistics statisticsPrototype = new BasicStatistics(new BasicOperationStats());
@InjectTrait(dependency = InjectTrait.Dependency.MANDATORY)
protected BasicOperations basicOperations;
@InjectTrait(dependency = InjectTrait.Dependency.OPTIONAL)
protected BulkOperations bulkOperations;
@InjectTrait
protected CacheInformation cacheInformation;
private Random random;
private String[][] words = null;
private Runtime runtime = null;
private int newlinePunctuationModulo = 10;
private long nodePutCount;
private long countOfWordsInData = 0;
private HashMap<String, Integer> wordCount = new HashMap<String, Integer>();
/**
*
* Fills a multi-dimensional array with randomly generated words. The first dimension of the
* array is based on the length of the word in characters, and runs from 1 to maxWordLength.
* Dividing the wordCount by maxWordLength determines how many words of each length are
* generated.
*
* @param wordCount
* the total number of words to generate
* @param maxWordLength
* the maximum size in characters for a word
*/
private void fillWordArray(int wordCount, int maxWordLength) {
int wordsPerLength = wordCount / maxWordLength;
words = new String[maxWordLength][wordsPerLength];
for (int i = 1; i <= maxWordLength; i++) {
for (int j = 0; j < wordsPerLength; j++) {
/*
* Intern the string to reduce memory usage since these words will be used multiple
* times
*/
words[i - 1][j] = new String(generateRandomUniqueWord(i, false)).intern();
}
}
if (log.isTraceEnabled()) {
log.tracef("Slave %d words array = %s", slaveState.getSlaveIndex(), Arrays.deepToString(words));
}
}
@Override
public void initOnSlave(SlaveState slaveState) {
super.initOnSlave(slaveState);
if (shareWords && limitWordCount) {
random = new Random(randomSeed);
} else {
/*
* Add the slaveIndex to the seed to guarantee that each node generates a different word
* list
*/
random = new Random(randomSeed + slaveState.getSlaveIndex());
}
fillWordArray(maxWordCount, maxWordLength);
}
@Override
public DistStageAck executeOnSlave() {
random = new Random(randomSeed + slaveState.getSlaveIndex());
if (ramPercentage > 0 && valueCount > 0) {
return errorResponse("Either valueCount or ramPercentageDataSize should be specified, but not both");
}
if (shareWords && !limitWordCount) {
return errorResponse("The shareWords property can only be true when limitWordCount is also true.");
}
if (limitWordCount && !stringData) {
return errorResponse("The limitWordCount property can only be true when stringData is also true.");
}
if (valueByteOverhead == -1 && cacheInformation == null) {
return errorResponse("The valueByteOverhead property must be supplied for this cache.");
}
if (batchSize > 0 && bulkOperations == null) {
return errorResponse("Batch loading was enabled, however service doesn't provide " + BulkOperations.class + " implementation");
}
/*
* If valueByteOverhead is not specified, then try to retrieve the byte overhead from the
* CacheWrapper
*/
if (valueByteOverhead == -1 && cacheInformation != null) {
valueByteOverhead = cacheInformation.getCache(null).getEntryOverhead();
}
runtime = Runtime.getRuntime();
int valueSizeWithOverhead = valueByteOverhead;
/*
* String data is twice the size of a byte array
*/
if (stringData) {
valueSizeWithOverhead += (valueSize * 2);
} else {
valueSizeWithOverhead += valueSize;
}
if (ramPercentage > 0) {
System.gc();
targetMemoryUse = (long) (runtime.maxMemory() * ramPercentage);
log.trace("targetMemoryUse: " + Utils.kbString(targetMemoryUse));
nodePutCount = (long) Math.ceil(targetMemoryUse / valueSizeWithOverhead);
} else {
long totalPutCount = valueCount;
if (targetMemoryUse > 0) {
if (targetMemoryUse % valueSizeWithOverhead != 0) {
log.warn("The supplied value for targetMemoryUse (" + targetMemoryUse
+ ") is not evenly divisible by the value size plus byte overhead (" + valueSizeWithOverhead + ")");
}
totalPutCount = targetMemoryUse / valueSizeWithOverhead;
}
nodePutCount = (long) Math.ceil(totalPutCount / slaveState.getClusterSize());
/*
* Add one to the nodeCount on each slave with an index less than the remainder so that the
* correct number of values are written to the cache
*/
if ((totalPutCount % slaveState.getClusterSize() != 0)
&& slaveState.getSlaveIndex() < (totalPutCount % slaveState.getClusterSize())) {
nodePutCount++;
}
}
long putCount = nodePutCount;
boolean useBatchLoading = batchSize > 0;
long bytesWritten = 0;
int numOperationsToPerform = 0;
BulkOperations.Cache<String, Object> bulkOperationsCache = null;
BasicOperations.Cache<String, Object> basicOperationsCache = null;
if (useBatchLoading) {
bulkOperationsCache = bulkOperations.getCache(bucket, useAsyncBatchLoading);
numOperationsToPerform = batchSize;
} else {
basicOperationsCache = basicOperations.getCache(bucket);
numOperationsToPerform = 1;
}
try {
byte[] buffer = new byte[valueSize];
Statistics stats = statisticsPrototype.copy();
stats.begin();
int counter = 0;
Map map = new HashMap(numOperationsToPerform);
while (putCount > 0) {
String key = Integer.toString(slaveState.getSlaveIndex()) + "-" + putCount + ":" + TimeService.nanoTime();
String cacheData = null;
if (stringData) {
cacheData = generateRandomStringData(valueSize);
if (useBatchLoading) {
map.put(key, cacheData);
}
} else {
random.nextBytes(buffer);
if (useBatchLoading) {
map.put(key, buffer);
}
}
// putCount == 1 -> last iteration
if (counter == numOperationsToPerform - 1 || putCount == 1) {
Request request;
boolean success = false;
for (int i = 0; i < putRetryCount; ++i) {
try {
if (stringData) {
if (putCount % 5000 == 0) {
log.info(i + ": Writing string length " + valueSize + " to cache key: " + key);
}
request = stats.startRequest();
if (batchSize > 0) {
bulkOperationsCache.putAll(map);
} else {
basicOperationsCache.put(key, cacheData);
}
} else {
if (putCount % 5000 == 0) {
log.info(i + ": Writing " + valueSize + " bytes to cache key: " + key);
}
request = stats.startRequest();
if (batchSize > 0) {
bulkOperationsCache.putAll(map);
} else {
basicOperationsCache.put(key, buffer);
}
}
request.succeeded(BasicOperations.PUT);
if (printWriteStatistics) {
log.info("Put on slave" + slaveState.getSlaveIndex() + " took "
+ Utils.prettyPrintTime(request.duration(), TimeUnit.NANOSECONDS));
}
success = true;
counter = -1;
map.clear();
break;
} catch (Exception e) {
// If the put fails, sleep to see if staggering the put will succeed
Thread.sleep(maxSleepInterval * 1000);
}
}
if (!success) {
return errorResponse("Failed to insert entry into cache",
new RuntimeException(String.format("Failed to insert entry %d times.", putRetryCount)));
}
}
if (stringData) {
bytesWritten += (valueSize * 2);
} else {
bytesWritten += valueSize;
}
putCount--;
counter++;
}
stats.end();
System.gc();
log.info("Memory - free: " + Utils.kbString(runtime.freeMemory()) + " - max: "
+ Utils.kbString(runtime.maxMemory()) + "- total: " + Utils.kbString(runtime.totalMemory()));
log.debug("nodePutCount = " + nodePutCount + "; bytesWritten = " + bytesWritten + "; targetMemoryUse = "
+ targetMemoryUse + "; countOfWordsInData = " + countOfWordsInData);
return new DataInsertAck(slaveState, nodePutCount, cacheInformation.getCache(null).getLocallyStoredSize(),
bytesWritten, targetMemoryUse, countOfWordsInData, wordCount, stats);
} catch (Exception e) {
return errorResponse("An exception occurred", e);
} finally {
// Log the word counts for this node
if (stringData && !wordCount.isEmpty() && log.isDebugEnabled()) {
log.debug("Word counts for node" + slaveState.getSlaveIndex());
log.debug("--------------------");
for (Map.Entry<String, Integer> entry : wordCount.entrySet()) {
log.debug("key: " + entry.getKey() + "; value: " + entry.getValue());
}
log.debug("--------------------");
}
}
}
private String generateRandomStringData(int dataSize) {
/*
* Generate a random string of "words" using random single and multi-byte characters that are
* separated by punctuation marks and whitespace.
*/
String punctuationChars = "!,.;?";
int wordLength = maxWordLength;
CharBuffer data = CharBuffer.allocate(dataSize);
while (data.remaining() > 0) {
String word;
if (limitWordCount) {
word = pickRandomWord(wordLength);
} else {
word = generateRandomUniqueWord(wordLength, true);
}
data = data.put(word);
countOfWordsInData++;
if (wordCount.containsKey(word)) {
wordCount.put(word, wordCount.get(word) + 1);
} else {
wordCount.put(word, 1);
}
if (data.remaining() >= 2 && random.nextInt() % newlinePunctuationModulo == 0) {
data.put(punctuationChars.charAt(random.nextInt(punctuationChars.length() - 1)));
data.put('\n');
} else {
if (data.remaining() >= 1) {
data.put(' ');
}
}
if (data.remaining() < wordLength) {
wordLength = data.remaining();
}
}
return data.rewind().toString();
}
/**
*
* Randomly selects a random length word based on the words array defined above
*
* @param maxLength
* the maximum length of the word
* @return the word as a String whose length may be less than <code>maxLength</code>
*/
private String pickRandomWord(int maxLength) {
String word = "";
String[] pickWords = {};
int pick = 0;
int wordLength = maxLength;
// Random.nextInt(0) generates an error
if (maxLength - 1 > 0) {
wordLength = random.nextInt(maxLength - 1) + 1;
}
pickWords = words[wordLength - 1];
if (pickWords.length - 1 > 0) {
pick = random.nextInt(pickWords.length - 1);
}
word = pickWords[pick];
return word;
}
/**
*
* Generates a random length "word" by randomly selecting single and multi-byte characters
*
* @param maxLength
* the maximum length of the word
* @param randomLength
* if <code>true</code>, use a random length with a max value of <code>maxLength</code>
* @return the word as a String whose length may be less than <code>maxLength</code>
*/
private String generateRandomUniqueWord(int maxLength, boolean randomLength) {
String singleByteChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
String multiByteChars = "ÅÄÇÉÑÖÕÜàäâáãçëèêéîïìíñôöòóüûùúÿ";
StringBuilder data = new StringBuilder();
int wordLength = maxLength;
if (randomLength && maxLength - 1 > 0) {
wordLength = random.nextInt(maxLength - 1) + 1;
}
for (int i = wordLength; i > 0; i--) {
// If wordLength == 1, then only use singleByteChars
if (wordLength > 1 && random.nextBoolean()) {
data.append(multiByteChars.charAt(random.nextInt(multiByteChars.length() - 1)));
} else {
data.append(singleByteChars.charAt(random.nextInt(singleByteChars.length() - 1)));
}
}
return data.toString();
}
@Override
public StageResult processAckOnMaster(List<DistStageAck> acks) {
StageResult result = super.processAckOnMaster(acks);
if (result.isError())
return result;
log.info("--------------------");
if (ramPercentage > 0) {
if (stringData) {
log.info("Filled cache with String objects totaling " + Math.round(ramPercentage * 100)
+ "% of the Java heap");
} else {
log.info("Filled cache with byte arrays totaling " + Math.round(ramPercentage * 100) + "% of the Java heap");
}
}
if (ramPercentage < 0 && targetMemoryUse > 0) {
if (stringData) {
log.info("Filled cache with String objects totaling " + Utils.kbString(targetMemoryUse));
} else {
log.info("Filled cache with byte arrays totaling " + Utils.kbString(targetMemoryUse));
}
}
if (valueCount > 0) {
if (stringData) {
log.info("Filled cache with " + Utils.kbString((valueSize * 2) * valueCount) + " of " + valueSize
+ " character String objects");
} else {
log.info("Filled cache with " + Utils.kbString(valueSize * valueCount) + " of " + Utils.kbString(valueSize)
+ " byte arrays");
}
}
Report report = masterState.getReport();
Report.Test test = report.createTest("Random_Data_Stage", null, true);
int testIteration = test.getIterations().size();
Map<Integer, Report.SlaveResult> nodeKeyCountsResult = new HashMap<Integer, Report.SlaveResult>();
Map<Integer, Report.SlaveResult> nodeTargetMemoryUseResult = new HashMap<Integer, Report.SlaveResult>();
Map<Integer, Report.SlaveResult> nodeCountOfWordsInDataResult = new HashMap<Integer, Report.SlaveResult>();
Map<Integer, Report.SlaveResult> nodeBytesWritten = new HashMap<Integer, Report.SlaveResult>();
long totalValues = 0;
long totalBytes = 0;
long totalNodeWordCount = 0;
Map<String, Integer> clusterWordCount = new TreeMap<String, Integer>();
for (DataInsertAck ack : instancesOf(acks, DataInsertAck.class)) {
if (ack.wordCount != null) {
for (Map.Entry<String, Integer> entry : ack.wordCount.entrySet()) {
if (clusterWordCount.containsKey(entry.getKey())) {
clusterWordCount.put(entry.getKey(), clusterWordCount.get(entry.getKey()) + entry.getValue());
} else {
clusterWordCount.put(entry.getKey(), entry.getValue());
}
}
}
nodeKeyCountsResult.put(ack.getSlaveIndex(), new Report.SlaveResult(Long.toString(ack.nodeKeyCount), false));
nodeBytesWritten.put(ack.getSlaveIndex(), new Report.SlaveResult(Long.toString(ack.bytesWritten), false));
test.addStatistics(testIteration, ack.getSlaveIndex(), Collections.singletonList(ack.nodePutStats));
totalValues += ack.nodePutCount;
totalBytes += ack.bytesWritten;
String logInfo = "Slave " + ack.getSlaveIndex() + " wrote " + ack.nodePutCount
+ " values to the cache with a total size of " + Utils.kbString(ack.bytesWritten);
if (ramPercentage > 0) {
logInfo += "; targetMemoryUse = " + Utils.kbString(ack.targetMemoryUse);
nodeTargetMemoryUseResult.put(ack.getSlaveIndex(), new Report.SlaveResult(Long.toString(ack.targetMemoryUse), false));
}
if (stringData) {
logInfo += "; countOfWordsInData = " + ack.countOfWordsInData;
nodeCountOfWordsInDataResult.put(ack.getSlaveIndex(), new Report.SlaveResult(Long.toString(ack.countOfWordsInData), false));
}
log.info(logInfo);
}
log.info("The cache contains " + totalValues + " values with a total size of " + Utils.kbString(totalBytes));
if (limitWordCount) {
int totalWordCount = maxWordCount;
if (!shareWords) {
totalWordCount = maxWordCount * slaveState.getClusterSize();
}
log.info(totalWordCount + " words were generated with a maximum length of " + maxWordLength + " characters");
}
if (!clusterWordCount.isEmpty() && log.isDebugEnabled()) {
log.debug("--------------------");
log.debug("Cluster wide word count:");
for (String key : clusterWordCount.keySet()) {
log.debug("word: " + key + "; count: " + clusterWordCount.get(key));
}
//TODO Will this take too much memory?
// masterState.put(RANDOMDATA_CLUSTER_WORDCOUNT_KEY, clusterWordCount);
}
log.info("--------------------");
masterState.put(RANDOMDATA_TOTALBYTES_KEY, totalBytes);
test.addResult(
testIteration,
new Report.TestResult("Kilobytes written per node", nodeBytesWritten, Utils.kbString(totalBytes), false));
test.addResult(testIteration, new Report.TestResult("Key count per node", nodeKeyCountsResult, "", false));
if (!nodeTargetMemoryUseResult.isEmpty()) {
test.addResult(testIteration, new Report.TestResult("Target memory use per node", nodeTargetMemoryUseResult,
Utils.kbString(totalBytes), false));
}
if (!nodeCountOfWordsInDataResult.isEmpty()) {
test.addResult(testIteration, new Report.TestResult("Count of words in data per node",
nodeCountOfWordsInDataResult, Long.toString(totalNodeWordCount), false));
}
return StageResult.SUCCESS;
}
private static class DataInsertAck extends DistStageAck {
final long nodePutCount;
final long nodeKeyCount;
final long bytesWritten;
final long targetMemoryUse;
final long countOfWordsInData;
final Map<String, Integer> wordCount;
final Statistics nodePutStats;
private DataInsertAck(SlaveState slaveState, long nodePutCount, long nodeKeyCount, long bytesWritten,
long targetMemoryUse, long countOfWordsInData, Map<String, Integer> wordCount, Statistics nodePutStats) {
super(slaveState);
this.nodePutCount = nodePutCount;
this.nodeKeyCount = nodeKeyCount;
this.bytesWritten = bytesWritten;
this.targetMemoryUse = targetMemoryUse;
this.countOfWordsInData = countOfWordsInData;
this.wordCount = wordCount;
this.nodePutStats = nodePutStats;
}
}
}