/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* Licensed 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.
*/
package com.github.ambry.store;
import com.codahale.metrics.MetricRegistry;
import com.github.ambry.clustermap.ClusterAgentsFactory;
import com.github.ambry.clustermap.ClusterMap;
import com.github.ambry.commons.BlobId;
import com.github.ambry.commons.BlobIdFactory;
import com.github.ambry.config.ClusterMapConfig;
import com.github.ambry.config.StoreConfig;
import com.github.ambry.config.VerifiableProperties;
import com.github.ambry.tools.util.ToolUtils;
import com.github.ambry.utils.SystemTime;
import com.github.ambry.utils.Throttler;
import com.github.ambry.utils.Utils;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
/**
* Reads from a file and populates the indexes and issues
* random read requests and tracks performance . This test reads 10000 ids
* at a time from the index write performance log and does random reads from that list.
* After 2 minutes it replaces the input with the next 10000 set.
*/
public class IndexReadPerformance {
static class IndexPayload {
private BlobIndexMetrics index;
private HashSet<String> ids;
public IndexPayload(BlobIndexMetrics index, HashSet<String> ids) {
this.index = index;
this.ids = ids;
}
public BlobIndexMetrics getIndex() {
return index;
}
public HashSet<String> getIds() {
return ids;
}
}
public static void main(String args[]) {
try {
OptionParser parser = new OptionParser();
ArgumentAcceptingOptionSpec<String> logToReadOpt =
parser.accepts("logToRead", "The log that needs to be replayed for traffic")
.withRequiredArg()
.describedAs("log_to_read")
.ofType(String.class);
ArgumentAcceptingOptionSpec<String> hardwareLayoutOpt =
parser.accepts("hardwareLayout", "The path of the hardware layout file")
.withRequiredArg()
.describedAs("hardware_layout")
.ofType(String.class);
ArgumentAcceptingOptionSpec<String> partitionLayoutOpt =
parser.accepts("partitionLayout", "The path of the partition layout file")
.withRequiredArg()
.describedAs("partition_layout")
.ofType(String.class);
ArgumentAcceptingOptionSpec<Integer> numberOfReadersOpt =
parser.accepts("numberOfReaders", "The number of readers that read to a random index concurrently")
.withRequiredArg()
.describedAs("The number of readers")
.ofType(Integer.class)
.defaultsTo(4);
ArgumentAcceptingOptionSpec<Integer> readsPerSecondOpt =
parser.accepts("readsPerSecond", "The rate at which reads need to be performed")
.withRequiredArg()
.describedAs("The number of reads per second")
.ofType(Integer.class)
.defaultsTo(1000);
ArgumentAcceptingOptionSpec<Boolean> verboseLoggingOpt =
parser.accepts("enableVerboseLogging", "Enables verbose logging")
.withOptionalArg()
.describedAs("Enable verbose logging")
.ofType(Boolean.class)
.defaultsTo(false);
OptionSet options = parser.parse(args);
ArrayList<OptionSpec> listOpt = new ArrayList<>();
listOpt.add(logToReadOpt);
listOpt.add(hardwareLayoutOpt);
listOpt.add(partitionLayoutOpt);
ToolUtils.ensureOrExit(listOpt, options, parser);
String logToRead = options.valueOf(logToReadOpt);
int numberOfReaders = options.valueOf(numberOfReadersOpt);
int readsPerSecond = options.valueOf(readsPerSecondOpt);
boolean enableVerboseLogging = options.has(verboseLoggingOpt);
if (enableVerboseLogging) {
System.out.println("Enabled verbose logging");
}
String hardwareLayoutPath = options.valueOf(hardwareLayoutOpt);
String partitionLayoutPath = options.valueOf(partitionLayoutOpt);
ClusterMapConfig clusterMapConfig = new ClusterMapConfig(new VerifiableProperties(new Properties()));
ClusterMap map =
((ClusterAgentsFactory) Utils.getObj(clusterMapConfig.clusterMapClusterAgentsFactory, clusterMapConfig,
hardwareLayoutPath, partitionLayoutPath)).getClusterMap();
StoreKeyFactory factory = new BlobIdFactory(map);
// Read the log and get the index directories and create the indexes
final BufferedReader br = new BufferedReader(new FileReader(logToRead));
final HashMap<String, IndexPayload> hashes = new HashMap<String, IndexPayload>();
String line;
StoreMetrics metrics = new StoreMetrics(System.getProperty("user.dir"), new MetricRegistry());
ScheduledExecutorService s = Utils.newScheduler(numberOfReaders, "index", true);
Log log = new Log(System.getProperty("user.dir"), 1000, 1000, metrics);
Properties props = new Properties();
props.setProperty("store.index.memory.size.bytes", "1048576");
StoreConfig config = new StoreConfig(new VerifiableProperties(props));
final AtomicLong totalTimeTaken = new AtomicLong(0);
final AtomicLong totalReads = new AtomicLong(0);
final CountDownLatch latch = new CountDownLatch(numberOfReaders);
final AtomicBoolean shutdown = new AtomicBoolean(false);
// attach shutdown handler to catch control-c
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
try {
System.out.println("Shutdown invoked");
shutdown.set(true);
latch.await();
System.out.println("Total reads : " + totalReads.get() + " Total time taken : " + totalTimeTaken.get()
+ " Nano Seconds Average time taken per read "
+ ((double) totalReads.get() / totalTimeTaken.get()) / SystemTime.NsPerSec + " Seconds");
} catch (Exception e) {
System.out.println("Error while shutting down " + e);
}
}
});
ScheduledExecutorService scheduleReadLog = Utils.newScheduler(1, true);
while ((line = br.readLine()) != null) {
if (line.startsWith("logdir")) {
String[] logdirs = line.split("-");
BlobIndexMetrics metricIndex =
new BlobIndexMetrics(logdirs[1], s, log, enableVerboseLogging, totalReads, totalTimeTaken, totalReads,
config, null, factory);
hashes.put(logdirs[1], new IndexPayload(metricIndex, new HashSet<String>()));
} else {
break;
}
}
// read next batch of ids after 2 minutes
scheduleReadLog.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
populateIds(br, hashes);
}
}, 0, 120, TimeUnit.SECONDS);
Throttler throttler = new Throttler(readsPerSecond, 100, true, SystemTime.getInstance());
Thread[] threadIndexPerf = new Thread[numberOfReaders];
for (int i = 0; i < numberOfReaders; i++) {
threadIndexPerf[i] = new Thread(new IndexReadPerfRun(hashes, throttler, shutdown, latch, map));
threadIndexPerf[i].start();
}
for (int i = 0; i < numberOfReaders; i++) {
threadIndexPerf[i].join();
}
br.close();
} catch (Exception e) {
System.out.println("Exiting process with exception " + e);
}
}
public static void populateIds(BufferedReader reader, HashMap<String, IndexPayload> hashes) {
try {
// read the first 10000 or less blob ids from the log
synchronized (hashes) {
System.out.println("Reading ids");
String line;
int count = 0;
for (Map.Entry<String, IndexPayload> payload : hashes.entrySet()) {
payload.getValue().getIds().clear();
}
while ((line = reader.readLine()) != null) {
if (line.startsWith("blobid")) {
String[] ids = line.split("-");
HashSet<String> idsForIndex = hashes.get(ids[1]).getIds();
idsForIndex.add(ids[2] + "-" + ids[3] + "-" + ids[4] + "-" + ids[5] + "-" + ids[6]);
System.out.println("Reading id " + ids[2]);
}
if (count == 10000) {
break;
}
count++;
}
}
} catch (Exception e) {
System.out.print("Error when reading from log " + e);
}
}
public static class IndexReadPerfRun implements Runnable {
private HashMap<String, IndexPayload> hashes;
private Throttler throttler;
private AtomicBoolean isShutdown;
private CountDownLatch latch;
private ClusterMap map;
public IndexReadPerfRun(HashMap<String, IndexPayload> hashes, Throttler throttler, AtomicBoolean isShutdown,
CountDownLatch latch, ClusterMap map) {
this.hashes = hashes;
this.throttler = throttler;
this.isShutdown = isShutdown;
this.latch = latch;
this.map = map;
}
public void run() {
try {
System.out.println("Starting index read performance");
System.out.flush();
while (!isShutdown.get()) {
synchronized (hashes) {
// choose a random index
int indexToUse = new Random().nextInt(hashes.size());
IndexPayload index = (IndexPayload) hashes.values().toArray()[indexToUse];
if (index.getIds().size() == 0) {
continue;
}
int idToUse = new Random().nextInt(index.getIds().size());
String idToLookup = (String) index.getIds().toArray()[idToUse];
if (index.getIndex().findKey(new BlobId(idToLookup, map)) == null) {
System.out.println("Error id not found in index " + idToLookup);
} else {
System.out.println("found id " + idToLookup);
}
throttler.maybeThrottle(1);
}
Thread.sleep(1000);
}
} catch (Exception e) {
System.out.println("Exiting index read perf thread " + e.getCause());
} finally {
latch.countDown();
}
}
}
}