/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* This program exercises the socket import capability by writing
* <key, value> pairs to one or more VoltDB socket importers.
*
* The pairs accumulate in a Queue structure. The program removes pairs
* from the Queue and uses asynchronous database queuries to verify that
* all the pairs written to the socket interface are present and have
* matching values.
*
* The checking proceeds in parallel as the socket writers write to the
* socket importers, and continues on until all pairs have been checked and
* the database has time to complete all socket importer input transactions.
*
* The "perftest" option skips the queuing/checking functions to max out and measure
* import speed.
*
* Opton "partitioned" designates the target table and related SP's are partitioned.
*
* If this option is omitted, the table is replicated.
*/
package socketimporter.client.socketimporter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang3.RandomStringUtils;
import org.voltcore.logging.VoltLogger;
import org.voltcore.utils.Pair;
import org.voltdb.CLIConfig;
import org.voltdb.client.Client;
import org.voltdb.client.ClientFactory;
import org.voltdb.client.ClientStatsContext;
import com.google_voltpatches.common.net.HostAndPort;
public class AsyncBenchmark {
static VoltLogger log = new VoltLogger("Benchmark");
// handy, rather than typing this out several times
static final String HORIZONTAL_RULE =
"----------" + "----------" + "----------" + "----------" +
"----------" + "----------" + "----------" + "----------" + "\n";
// queue structure to hold data as it's written, so we can check it all get's into the database
static Queue<Pair<String,String>> queue = new LinkedBlockingQueue<Pair<String,String>>();
static Queue<Pair<String,String>> dqueue = new LinkedBlockingQueue<Pair<String,String>>();
static boolean importerDone = false;
static DataUtils checkDB = null;
// validated command line configuration
static Config config;
// Timer for periodic stats printing
static Timer timer;
// Benchmark start time
static long benchmarkStartTS;
static final Map<HostAndPort, OutputStream> haplist = new HashMap<HostAndPort, OutputStream>();
static Client client;
// Some thread safe counters for reporting
AtomicLong linesRead = new AtomicLong(0);
AtomicLong rowsAdded = new AtomicLong(0);
static final AtomicLong rowsChecked = new AtomicLong(0);
static final AtomicLong rowsMismatch = new AtomicLong(0);
static final AtomicLong writers = new AtomicLong(0);
static final AtomicLong socketWrites = new AtomicLong(0);
static final AtomicLong socketWriteExceptions = new AtomicLong(0);
static final AtomicLong runCount = new AtomicLong(0);
static final AtomicLong warmupCount = new AtomicLong(0);
final ClientStatsContext periodicStatsContext;
final ClientStatsContext fullStatsContext;
/**
* Uses included {@link CLIConfig} class to
* declaratively state command line options with defaults
* and validation.
*/
static class Config extends CLIConfig {
@Option(desc = "Interval for performance feedback, in seconds.")
long displayinterval = 5;
@Option(desc = "Benchmark duration, in seconds.")
int duration = 300;
@Option(desc = "Warmup duration in seconds.")
int warmup = 20;
@Option(desc = "Comma separated list of the form server[:port] to connect to database for queuries")
String servers = "localhost";
@Option(desc = "Comma separated list of the form server[:port] to connect to socket stream")
String sockservers = "localhost";
@Option(desc = "Report latency for async benchmark run.")
boolean latencyreport = false;
@Option(desc = "Filename to write raw summary statistics to.")
String statsfile = "";
@Option(desc = "Performance test only.")
boolean perftest = false;
@Option(desc = "If true, use the partitioned table for the benchmark; replicated otherwise.")
boolean partitioned = false;
@Override
public void validate() {
if (duration <= 0) exitWithMessageAndUsage("duration must be > 0");
if (warmup < 0) exitWithMessageAndUsage("warmup must be >= 0");
if (displayinterval <= 0) exitWithMessageAndUsage("displayinterval must be > 0");
if (perftest && statsfile.length() == 0) statsfile = "socketimporter.csv";
}
}
/**
* Constructor for benchmark instance.
* Configures VoltDB client and prints configuration.
*
* @param config Parsed & validated CLI options.
*/
public AsyncBenchmark(Config config) {
this.config = config;
//AsyncBenchmark.config = config;
periodicStatsContext = client.createStatsContext();
fullStatsContext = client.createStatsContext();
}
/**
* Connect to a single server with retry. Limited exponential backoff.
* No timeout. This will run until the process is killed if it's not
* able to connect.
*
* @param server hostname:port or just hostname (hostname can be ip).
*/
static OutputStream connectToOneServerWithRetry(String server, int port) {
int sleep = 1000;
while (true) {
try {
Socket pushSocket = new Socket(server, port);
OutputStream out = pushSocket.getOutputStream();
System.out.printf("Connected to VoltDB node at: %s.\n", server);
return out;
}
catch (Exception e) {
System.err.printf("Connection failed - retrying in %d second(s).\n", sleep / 1000);
try { Thread.sleep(sleep); } catch (Exception interruted) {}
if (sleep < 8000) sleep += sleep;
}
}
}
/**
* Connect to a set of servers in parallel. Each will retry until
* connection. This call will block until all have connected.
*
* @param servers A comma separated list of servers using the hostname:port
* syntax (where :port is optional).
* @param port
* @throws InterruptedException if anything bad happens with the threads.
*/
static void connect(String servers) throws InterruptedException {
log.info("Connecting to Socket Streaming Interface...");
String[] serverArray = servers.split(",");
final CountDownLatch connections = new CountDownLatch(serverArray.length);
// use a new thread to connect to each server
for (final String server : serverArray) {
new Thread(new Runnable() {
@Override
public void run() {
// default port; assumed in system test so keep sync'd if it's changed
HostAndPort hap = HostAndPort.fromString(server).withDefaultPort(7001);
OutputStream writer = connectToOneServerWithRetry(hap.getHostText(), hap.getPort());
haplist.put(hap, writer);
connections.countDown();
}
}).start();
}
// block until all have connected
connections.await();
}
/**
* Connect to one or more VoltDB servers.
*
* @param servers A comma separated list of servers using the hostname:port
* syntax (where :port is optional). Assumes 21212 if not specified otherwise.
* @throws InterruptedException if anything bad happens with the threads.
*/
static void dbconnect(String servers) throws InterruptedException, Exception {
log.info("Connecting to VoltDB Interface...");
String[] serverArray = servers.split(",");
client = ClientFactory.createClient();
for (String server : serverArray) {
log.info("..." + server);
client.createConnection(server);
}
}
/**
* Create a Timer task to display performance data on the Vote procedure
* It calls printStatistics() every displayInterval seconds
*/
public static void schedulePeriodicStats() {
timer = new Timer();
TimerTask statsPrinting = new TimerTask() {
@Override
public void run() { printStatistics(); }
};
timer.scheduleAtFixedRate(statsPrinting,
config.displayinterval * 1000,
config.displayinterval * 1000);
}
/**
* Prints a one line update on performance that can be printed
* periodically during a benchmark.
*/
public synchronized static void printStatistics() {
try {
long thrup;
long max_insert_time = checkDB.maxInsertTime();
thrup = (long) (runCount.get() / ((max_insert_time-benchmarkStartTS)/1000.0));
if (thrup > 0) { // first time through, calc can be whacky
log.info(String.format("Import Throughput %d/s, Total Rows %d",
thrup, runCount.get()+warmupCount.get()));
}
} catch (Exception e) {
log.info("Exception in printStatistics" + e);
e.printStackTrace();
}
log.info("Import stats: " + UtilQueries.getImportStats(client));
}
/**
* Prints the results to a csv file for charting
*
* @throws Exception if anything unexpected happens.
*/
public synchronized static void printResults() throws Exception {
FileWriter fw = null;
if ((config.statsfile != null) && (config.statsfile.length() != 0)) {
fw = new FileWriter(config.statsfile);
fw.append(String.format("%s,%d,-1,%d,0,0,0,0,0,0,0,0,0,0\n",
(config.partitioned ? "Partitioned" : "Replicated"),
benchmarkStartTS/1000, // back to seconds
runCount.get()/((checkDB.maxInsertTime()-benchmarkStartTS)/1000))); // throughput -- TPS
fw.close();
}
}
/**
* Core benchmark code.
* Connect. Initialize. Run the loop. Cleanup. Print Results.
*
* @throws Exception if anything unexpected happens.
*/
public void runBenchmark(HostAndPort hap) throws Exception {
System.out.print(HORIZONTAL_RULE);
log.info(" Setup & Initialization");
log.info(HORIZONTAL_RULE);
System.out.print(HORIZONTAL_RULE);
log.info(" Starting Benchmark");
log.info(HORIZONTAL_RULE);
SecureRandom rnd = new SecureRandom();
rnd.setSeed(Thread.currentThread().getId());
log.info("Warming up...");
final long warmupEndTime = System.currentTimeMillis() + (1000l * config.warmup);
while (warmupEndTime > System.currentTimeMillis()) {
String key = Long.toString(rnd.nextLong());
String s;
if (config.perftest) {
String valString = RandomStringUtils.randomAlphanumeric(1024);
s = key + "," + valString + "\n";
} else {
String t = Long.toString(System.currentTimeMillis());
Pair<String,String> p = new Pair<String,String>(key, t);
queue.offer(p);
s = key + "," + t + "\n";
}
writeFully(s, hap, warmupEndTime);
warmupCount.getAndIncrement();
}
benchmarkStartTS = System.currentTimeMillis();
// Run the benchmark loop for the requested duration
// The throughput may be throttled depending on client configuration
// Save the key/value pairs so they can be verified through the database
log.info("\nRunning benchmark...");
final long benchmarkEndTime = System.currentTimeMillis() + (1000l * config.duration);
while (benchmarkEndTime > System.currentTimeMillis()) {
String key = Long.toString(rnd.nextLong());
String s;
if (config.perftest) {
String valString = RandomStringUtils.randomAlphanumeric(16);
s = key + "," + valString + "\n";
} else {
String t = Long.toString(System.currentTimeMillis());
Pair<String,String> p = new Pair<String,String>(key, t);
queue.offer(p);
s = key + "," + t + "\n";
}
writeFully(s, hap, benchmarkEndTime);
runCount.getAndIncrement();
}
haplist.get(hap).flush();
log.info("Benchmark loop complete for this thread.");
if (timer != null) timer.cancel();
}
private void writeFully(String data, HostAndPort hap, long endTime) {
while (System.currentTimeMillis() < endTime) {
try {
OutputStream writer = haplist.get(hap);
writer.write(data.getBytes());
socketWrites.incrementAndGet();
return;
} catch (IOException ex) {
log.info("Exception: " + ex);
OutputStream writer = connectToOneServerWithRetry(hap.getHostText(), hap.getPort());
haplist.put(hap, writer);
socketWriteExceptions.incrementAndGet();
}
}
}
public static class BenchmarkRunner extends Thread {
private final AsyncBenchmark benchmark;
private final CountDownLatch cdl;
private final HostAndPort hap;
public BenchmarkRunner(AsyncBenchmark bm, CountDownLatch c, HostAndPort iidx) {
benchmark = bm;
cdl = c;
hap = iidx;
}
@Override
public void run() {
try {
benchmark.runBenchmark(hap);
writers.incrementAndGet();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
cdl.countDown();
}
}
}
/**
* Main routine creates a benchmark instance and kicks off the run method.
*
* @param args Command line arguments.
* @throws Exception if anything goes wrong.
* @see {@link VoterConfig}
*/
public static void main(String[] args) throws Exception {
VoltLogger log = new VoltLogger("Benchmark.main");
final long WAIT_FOR_A_WHILE = 100 * 1000; // 5 minutes in milliseconds
// create a configuration from the arguments
Config config = new Config();
config.parse(AsyncBenchmark.class.getName(), args);
System.out.print(HORIZONTAL_RULE);
log.info(" Command Line Configuration");
log.info(HORIZONTAL_RULE);
log.info(config.getConfigDumpString());
if(config.latencyreport) {
log.info("NOTICE: Not implemented in this benchmark client.\n");
}
// connect to one or more servers, loop until success
dbconnect(config.servers);
log.info("Setting up DDL");
checkDB = new DataUtils(queue, dqueue, client, config.partitioned);
checkDB.ddlSetup(config.partitioned);
connect(config.sockservers);
CountDownLatch cdl = new CountDownLatch(haplist.size());
for (HostAndPort hap : haplist.keySet()) {
AsyncBenchmark benchmark = new AsyncBenchmark(config);
BenchmarkRunner runner = new BenchmarkRunner(benchmark, cdl, hap);
runner.start();
}
schedulePeriodicStats();
if (!config.perftest) {
// start checking the table that's being populated by the socket injester(s)
while (queue.size() == 0) {
try {
Thread.sleep(1000); // one second.
} catch(InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
log.info("Starting CheckData methods. Queue size: " + queue.size());
checkDB.processQueue();
}
log.info("-- waiting for socket writers.");
// this hangs occasionally, so adding a timeout with a margin
cdl.await(config.duration+config.warmup+1, TimeUnit.SECONDS);
// close socket connections...
for (HostAndPort hap : haplist.keySet()) {
OutputStream writer = haplist.get(hap);
writer.flush();
writer.close();
}
// print the summary results
printResults();
if (!config.perftest) {
log.info("...starting timed check looping... " + queue.size());
final long queueEndTime = System.currentTimeMillis() + WAIT_FOR_A_WHILE;
log.info("Continue checking for " + (queueEndTime-System.currentTimeMillis())/1000 + " seconds.");
while (queueEndTime > System.currentTimeMillis()) {
checkDB.processQueue();
}
}
// final exit criteria -- queue of outstanding importer requests goes to zero
// but with checking for no-progress so we don't get stuck forever.
long outstandingRequests = UtilQueries.getImportOutstandingRequests(client);
long prev_outstandingRequests = outstandingRequests;
int waitloops = 10; // kinda arbitrary but if outstanding requests is not changing for this interval...
while (outstandingRequests != 0 && waitloops > 0) {
log.info("Importer outstanding requests is " + outstandingRequests + ". Waiting for zero.");
outstandingRequests = UtilQueries.getImportOutstandingRequests(client);
if (prev_outstandingRequests == outstandingRequests) {
log.info("Outstanding requests unchanged since last interval.");
waitloops--;
}
prev_outstandingRequests = outstandingRequests;
Thread.sleep(config.displayinterval*1000);
}
client.drain();
client.close();
if (!config.perftest) {
log.info("Queued tuples remaining: " + queue.size());
log.info("Rows checked against database: " + rowsChecked.get());
log.info("Mismatch rows (value imported <> value in DB): " + rowsMismatch.get());
}
log.info("Total rows added by Socket Injester: " + (warmupCount.get()+runCount.get()));
log.info("Socket write count: " + socketWrites.get());
log.info("Socket write exception count: " + socketWriteExceptions.get());
System.exit(0);
}
}