diff --git a/bin/bindings.properties b/bin/bindings.properties index d75730c1..580ae742 100644 --- a/bin/bindings.properties +++ b/bin/bindings.properties @@ -1,65 +1,66 @@ # # Copyright (c) 2012 - 2016 YCSB contributors. 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. See the License for the specific language governing # permissions and limitations under the License. See accompanying # LICENSE file. # #DATABASE BINDINGS # # Available bindings should be listed here in the form of # name:class # # - the name must start in column 0. # - the name is also the directory where the class can be found. # - if the directory contains multiple versions with different classes, # use a dash with the version. (e.g. cassandra-7, cassandra-cql) # accumulo:com.yahoo.ycsb.db.accumulo.AccumuloClient aerospike:com.yahoo.ycsb.db.AerospikeClient asynchbase:com.yahoo.ycsb.db.AsyncHBaseClient arangodb:com.yahoo.ycsb.db.ArangoDBClient azuretablestorage:com.yahoo.ycsb.db.azuretablestorage.AzureClient basic:com.yahoo.ycsb.BasicDB cassandra-cql:com.yahoo.ycsb.db.CassandraCQLClient cassandra2-cql:com.yahoo.ycsb.db.CassandraCQLClient couchbase:com.yahoo.ycsb.db.CouchbaseClient couchbase2:com.yahoo.ycsb.db.couchbase2.Couchbase2Client dynamodb:com.yahoo.ycsb.db.DynamoDBClient elasticsearch:com.yahoo.ycsb.db.ElasticsearchClient geode:com.yahoo.ycsb.db.GeodeClient googlebigtable:com.yahoo.ycsb.db.GoogleBigtableClient googledatastore:com.yahoo.ycsb.db.GoogleDatastoreClient hbase094:com.yahoo.ycsb.db.HBaseClient hbase098:com.yahoo.ycsb.db.HBaseClient hbase10:com.yahoo.ycsb.db.HBaseClient10 hypertable:com.yahoo.ycsb.db.HypertableClient infinispan-cs:com.yahoo.ycsb.db.InfinispanRemoteClient infinispan:com.yahoo.ycsb.db.InfinispanClient jdbc:com.yahoo.ycsb.db.JdbcDBClient kudu:com.yahoo.ycsb.db.KuduYCSBClient mapkeeper:com.yahoo.ycsb.db.MapKeeperClient memcached:com.yahoo.ycsb.db.MemcachedClient mongodb:com.yahoo.ycsb.db.MongoDbClient mongodb-async:com.yahoo.ycsb.db.AsyncMongoDbClient nosqldb:com.yahoo.ycsb.db.NoSqlDbClient orientdb:com.yahoo.ycsb.db.OrientDBClient rados:com.yahoo.ycsb.db.RadosClient redis:com.yahoo.ycsb.db.RedisClient +rest:com.yahoo.ycsb.webservice.rest.RestClient riak:com.yahoo.ycsb.db.riak.RiakKVClient s3:com.yahoo.ycsb.db.S3Client solr:com.yahoo.ycsb.db.solr.SolrClient solr6:com.yahoo.ycsb.db.solr6.SolrClient tarantool:com.yahoo.ycsb.db.TarantoolClient voldemort:com.yahoo.ycsb.db.VoldemortClient diff --git a/bin/ycsb b/bin/ycsb index e0f7092e..72e218a5 100755 --- a/bin/ycsb +++ b/bin/ycsb @@ -1,298 +1,299 @@ #!/usr/bin/env python # # Copyright (c) 2012 - 2015 YCSB contributors. 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. See the License for the specific language governing # permissions and limitations under the License. See accompanying # LICENSE file. # import errno import fnmatch import io import os import shlex import sys import subprocess try: mod = __import__('argparse') import argparse except ImportError: print >> sys.stderr, '[ERROR] argparse not found. Try installing it via "pip".' exit(1) BASE_URL = "https://github.com/brianfrankcooper/YCSB/tree/master/" COMMANDS = { "shell" : { "command" : "", "description" : "Interactive mode", "main" : "com.yahoo.ycsb.CommandLine", }, "load" : { "command" : "-load", "description" : "Execute the load phase", "main" : "com.yahoo.ycsb.Client", }, "run" : { "command" : "-t", "description" : "Execute the transaction phase", "main" : "com.yahoo.ycsb.Client", }, } DATABASES = { "accumulo" : "com.yahoo.ycsb.db.accumulo.AccumuloClient", "aerospike" : "com.yahoo.ycsb.db.AerospikeClient", "arangodb" : "com.yahoo.ycsb.db.ArangoDBClient", "asynchbase" : "com.yahoo.ycsb.db.AsyncHBaseClient", "basic" : "com.yahoo.ycsb.BasicDB", "cassandra-cql": "com.yahoo.ycsb.db.CassandraCQLClient", "cassandra2-cql": "com.yahoo.ycsb.db.CassandraCQLClient", "couchbase" : "com.yahoo.ycsb.db.CouchbaseClient", "couchbase2" : "com.yahoo.ycsb.db.couchbase2.Couchbase2Client", "dynamodb" : "com.yahoo.ycsb.db.DynamoDBClient", "elasticsearch": "com.yahoo.ycsb.db.ElasticsearchClient", "geode" : "com.yahoo.ycsb.db.GeodeClient", "googlebigtable" : "com.yahoo.ycsb.db.GoogleBigtableClient", "googledatastore" : "com.yahoo.ycsb.db.GoogleDatastoreClient", "hbase094" : "com.yahoo.ycsb.db.HBaseClient", "hbase098" : "com.yahoo.ycsb.db.HBaseClient", "hbase10" : "com.yahoo.ycsb.db.HBaseClient10", "hypertable" : "com.yahoo.ycsb.db.HypertableClient", "infinispan-cs": "com.yahoo.ycsb.db.InfinispanRemoteClient", "infinispan" : "com.yahoo.ycsb.db.InfinispanClient", "jdbc" : "com.yahoo.ycsb.db.JdbcDBClient", "kudu" : "com.yahoo.ycsb.db.KuduYCSBClient", "mapkeeper" : "com.yahoo.ycsb.db.MapKeeperClient", "memcached" : "com.yahoo.ycsb.db.MemcachedClient", "mongodb" : "com.yahoo.ycsb.db.MongoDbClient", "mongodb-async": "com.yahoo.ycsb.db.AsyncMongoDbClient", "nosqldb" : "com.yahoo.ycsb.db.NoSqlDbClient", "orientdb" : "com.yahoo.ycsb.db.OrientDBClient", "rados" : "com.yahoo.ycsb.db.RadosClient", "redis" : "com.yahoo.ycsb.db.RedisClient", + "rest" : "com.yahoo.ycsb.webservice.rest.RestClient", "riak" : "com.yahoo.ycsb.db.riak.RiakKVClient", "s3" : "com.yahoo.ycsb.db.S3Client", "solr" : "com.yahoo.ycsb.db.solr.SolrClient", "solr6" : "com.yahoo.ycsb.db.solr6.SolrClient", "tarantool" : "com.yahoo.ycsb.db.TarantoolClient", "voldemort" : "com.yahoo.ycsb.db.VoldemortClient" } OPTIONS = { "-P file" : "Specify workload file", "-p key=value" : "Override workload property", "-s" : "Print status to stderr", "-target n" : "Target ops/sec (default: unthrottled)", "-threads n" : "Number of client threads (default: 1)", "-cp path" : "Additional Java classpath entries", "-jvm-args args" : "Additional arguments to the JVM", } def usage(): output = io.BytesIO() print >> output, "%s command database [options]" % sys.argv[0] print >> output, "\nCommands:" for command in sorted(COMMANDS.keys()): print >> output, " %s %s" % (command.ljust(14), COMMANDS[command]["description"]) print >> output, "\nDatabases:" for db in sorted(DATABASES.keys()): print >> output, " %s %s" % (db.ljust(14), BASE_URL + db.split("-")[0]) print >> output, "\nOptions:" for option in sorted(OPTIONS.keys()): print >> output, " %s %s" % (option.ljust(14), OPTIONS[option]) print >> output, """\nWorkload Files: There are various predefined workloads under workloads/ directory. See https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties for the list of workload properties.""" return output.getvalue() # Python 2.6 doesn't have check_output. Add the method as it is in Python 2.7 # Based on https://github.com/python/cpython/blob/2.7/Lib/subprocess.py#L545 def check_output(*popenargs, **kwargs): r"""Run command with arguments and return its output as a byte string. If the exit code was non-zero it raises a CalledProcessError. The CalledProcessError object will have the return code in the returncode attribute and output in the output attribute. The arguments are the same as for the Popen constructor. Example: >>> check_output(["ls", "-l", "/dev/null"]) 'crw-rw-rw- 1 root root 1, 3 Oct 18 2007 /dev/null\n' The stdout argument is not allowed as it is used internally. To capture standard error in the result, use stderr=STDOUT. >>> check_output(["/bin/sh", "-c", ... "ls -l non_existent_file ; exit 0"], ... stderr=STDOUT) 'ls: non_existent_file: No such file or directory\n' """ if 'stdout' in kwargs: raise ValueError('stdout argument not allowed, it will be overridden.') process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) output, unused_err = process.communicate() retcode = process.poll() if retcode: cmd = kwargs.get("args") if cmd is None: cmd = popenargs[0] error = subprocess.CalledProcessError(retcode, cmd) error.output = output raise error return output def debug(message): print >> sys.stderr, "[DEBUG] ", message def warn(message): print >> sys.stderr, "[WARN] ", message def error(message): print >> sys.stderr, "[ERROR] ", message def find_jars(dir, glob='*.jar'): jars = [] for (dirpath, dirnames, filenames) in os.walk(dir): for filename in fnmatch.filter(filenames, glob): jars.append(os.path.join(dirpath, filename)) return jars def get_ycsb_home(): dir = os.path.abspath(os.path.dirname(sys.argv[0])) while "LICENSE.txt" not in os.listdir(dir): dir = os.path.join(dir, os.path.pardir) return os.path.abspath(dir) def is_distribution(): # If there's a top level pom, we're a source checkout. otherwise a dist artifact return "pom.xml" not in os.listdir(get_ycsb_home()) # Run the maven dependency plugin to get the local jar paths. # presumes maven can run, so should only be run on source checkouts # will invoke the 'package' goal for the given binding in order to resolve intra-project deps # presumes maven properly handles system-specific path separators # Given module is full module name eg. 'core' or 'couchbase-binding' def get_classpath_from_maven(module): try: debug("Running 'mvn -pl com.yahoo.ycsb:" + module + " -am package -DskipTests " "dependency:build-classpath -DincludeScope=compile -Dmdep.outputFilterFile=true'") mvn_output = check_output(["mvn", "-pl", "com.yahoo.ycsb:" + module, "-am", "package", "-DskipTests", "dependency:build-classpath", "-DincludeScope=compile", "-Dmdep.outputFilterFile=true"]) # the above outputs a "classpath=/path/tojar:/path/to/other/jar" for each module # the last module will be the datastore binding line = [x for x in mvn_output.splitlines() if x.startswith("classpath=")][-1:] return line[0][len("classpath="):] except subprocess.CalledProcessError, err: error("Attempting to generate a classpath from Maven failed " "with return code '" + str(err.returncode) + "'. The output from " "Maven follows, try running " "'mvn -DskipTests package dependency:build=classpath' on your " "own and correct errors." + os.linesep + os.linesep + "mvn output:" + os.linesep + err.output) sys.exit(err.returncode) def main(): p = argparse.ArgumentParser( usage=usage(), formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument('-cp', dest='classpath', help="""Additional classpath entries, e.g. '-cp /tmp/hbase-1.0.1.1/conf'. Will be prepended to the YCSB classpath.""") p.add_argument("-jvm-args", default=[], type=shlex.split, help="""Additional arguments to pass to 'java', e.g. '-Xmx4g'""") p.add_argument("command", choices=sorted(COMMANDS), help="""Command to run.""") p.add_argument("database", choices=sorted(DATABASES), help="""Database to test.""") args, remaining = p.parse_known_args() ycsb_home = get_ycsb_home() # Use JAVA_HOME to find java binary if set, otherwise just use PATH. java = "java" java_home = os.getenv("JAVA_HOME") if java_home: java = os.path.join(java_home, "bin", "java") db_classname = DATABASES[args.database] command = COMMANDS[args.command]["command"] main_classname = COMMANDS[args.command]["main"] # Classpath set up binding = args.database.split("-")[0] if binding == "cassandra2": warn("The 'cassandra2-cql' client has been deprecated. It has been " "renamed to simply 'cassandra-cql'. This alias will be removed" " in the next YCSB release.") binding = "cassandra" if binding == "couchbase": warn("The 'couchbase' client has been deprecated. If you are using " "Couchbase 4.0+ try using the 'couchbase2' client instead.") if is_distribution(): db_dir = os.path.join(ycsb_home, binding + "-binding") # include top-level conf for when we're a binding-specific artifact. # If we add top-level conf to the general artifact, starting here # will allow binding-specific conf to override (because it's prepended) cp = [os.path.join(ycsb_home, "conf")] cp.extend(find_jars(os.path.join(ycsb_home, "lib"))) cp.extend(find_jars(os.path.join(db_dir, "lib"))) else: warn("Running against a source checkout. In order to get our runtime " "dependencies we'll have to invoke Maven. Depending on the state " "of your system, this may take ~30-45 seconds") db_location = "core" if binding == "basic" else binding project = "core" if binding == "basic" else binding + "-binding" db_dir = os.path.join(ycsb_home, db_location) # goes first so we can rely on side-effect of package maven_says = get_classpath_from_maven(project) # TODO when we have a version property, skip the glob cp = find_jars(os.path.join(db_dir, "target"), project + "*.jar") # alredy in jar:jar:jar form cp.append(maven_says) cp.insert(0, os.path.join(db_dir, "conf")) classpath = os.pathsep.join(cp) if args.classpath: classpath = os.pathsep.join([args.classpath, classpath]) ycsb_command = ([java] + args.jvm_args + ["-cp", classpath, main_classname, "-db", db_classname] + remaining) if command: ycsb_command.append(command) print >> sys.stderr, " ".join(ycsb_command) try: return subprocess.call(ycsb_command) except OSError as e: if e.errno == errno.ENOENT: error('Command failed. Is java installed and on your PATH?') return 1 else: raise if __name__ == '__main__': sys.exit(main()) diff --git a/checkstyle.xml b/checkstyle.xml index 92d1ce37..af0065d0 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -1,187 +1,189 @@ - + + + diff --git a/core/src/main/java/com/yahoo/ycsb/workloads/RestWorkload.java b/core/src/main/java/com/yahoo/ycsb/workloads/RestWorkload.java new file mode 100644 index 00000000..b36c65a9 --- /dev/null +++ b/core/src/main/java/com/yahoo/ycsb/workloads/RestWorkload.java @@ -0,0 +1,304 @@ +/** + * Copyright (c) 2016 YCSB contributors. 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. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ + +package com.yahoo.ycsb.workloads; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import com.yahoo.ycsb.ByteIterator; +import com.yahoo.ycsb.DB; +import com.yahoo.ycsb.RandomByteIterator; +import com.yahoo.ycsb.WorkloadException; +import com.yahoo.ycsb.generator.DiscreteGenerator; +import com.yahoo.ycsb.generator.ExponentialGenerator; +import com.yahoo.ycsb.generator.HotspotIntegerGenerator; +import com.yahoo.ycsb.generator.NumberGenerator; +import com.yahoo.ycsb.generator.UniformIntegerGenerator; +import com.yahoo.ycsb.generator.ZipfianGenerator; + +/** + * Typical RESTFul services benchmarking scenario. Represents a set of client + * calling REST operations like HTTP DELETE, GET, POST, PUT on a web service. + * This scenario is completely different from CoreWorkload which is mainly + * designed for databases benchmarking. However due to some reusable + * functionality this class extends {@link CoreWorkload} and overrides necessary + * methods like init, doTransaction etc. + */ +public class RestWorkload extends CoreWorkload { + + /** + * The name of the property for the proportion of transactions that are + * delete. + */ + public static final String DELETE_PROPORTION_PROPERTY = "deleteproportion"; + + /** + * The default proportion of transactions that are delete. + */ + public static final String DELETE_PROPORTION_PROPERTY_DEFAULT = "0.00"; + + /** + * The name of the property for the file that holds the field length size for insert operations. + */ + public static final String FIELD_LENGTH_DISTRIBUTION_FILE_PROPERTY = "fieldlengthdistfile"; + + /** + * The default file name that holds the field length size for insert operations. + */ + public static final String FIELD_LENGTH_DISTRIBUTION_FILE_PROPERTY_DEFAULT = "fieldLengthDistFile.txt"; + + /** + * In web services even though the CRUD operations follow the same request + * distribution, they have different traces and distribution parameter + * values. Hence configuring the parameters of these operations separately + * makes the benchmark more flexible and capable of generating better + * realistic workloads. + */ + // Read related properties. + private static final String READ_TRACE_FILE = "url.trace.read"; + private static final String READ_TRACE_FILE_DEFAULT = "readtrace.txt"; + private static final String READ_ZIPFIAN_CONSTANT = "readzipfconstant"; + private static final String READ_ZIPFIAN_CONSTANT_DEAFULT = "0.99"; + private static final String READ_RECORD_COUNT_PROPERTY = "readrecordcount"; + // Insert related properties. + private static final String INSERT_TRACE_FILE = "url.trace.insert"; + private static final String INSERT_TRACE_FILE_DEFAULT = "inserttrace.txt"; + private static final String INSERT_ZIPFIAN_CONSTANT = "insertzipfconstant"; + private static final String INSERT_ZIPFIAN_CONSTANT_DEAFULT = "0.99"; + private static final String INSERT_SIZE_ZIPFIAN_CONSTANT = "insertsizezipfconstant"; + private static final String INSERT_SIZE_ZIPFIAN_CONSTANT_DEAFULT = "0.99"; + private static final String INSERT_RECORD_COUNT_PROPERTY = "insertrecordcount"; + // Delete related properties. + private static final String DELETE_TRACE_FILE = "url.trace.delete"; + private static final String DELETE_TRACE_FILE_DEFAULT = "deletetrace.txt"; + private static final String DELETE_ZIPFIAN_CONSTANT = "deletezipfconstant"; + private static final String DELETE_ZIPFIAN_CONSTANT_DEAFULT = "0.99"; + private static final String DELETE_RECORD_COUNT_PROPERTY = "deleterecordcount"; + // Delete related properties. + private static final String UPDATE_TRACE_FILE = "url.trace.update"; + private static final String UPDATE_TRACE_FILE_DEFAULT = "updatetrace.txt"; + private static final String UPDATE_ZIPFIAN_CONSTANT = "updatezipfconstant"; + private static final String UPDATE_ZIPFIAN_CONSTANT_DEAFULT = "0.99"; + private static final String UPDATE_RECORD_COUNT_PROPERTY = "updaterecordcount"; + + private Map readUrlMap; + private Map insertUrlMap; + private Map deleteUrlMap; + private Map updateUrlMap; + private int readRecordCount; + private int insertRecordCount; + private int deleteRecordCount; + private int updateRecordCount; + private NumberGenerator readKeyChooser; + private NumberGenerator insertKeyChooser; + private NumberGenerator deleteKeyChooser; + private NumberGenerator updateKeyChooser; + + @Override + public void init(Properties p) throws WorkloadException { + + readRecordCount = Integer.parseInt(p.getProperty(READ_RECORD_COUNT_PROPERTY, String.valueOf(Integer.MAX_VALUE))); + insertRecordCount = Integer + .parseInt(p.getProperty(INSERT_RECORD_COUNT_PROPERTY, String.valueOf(Integer.MAX_VALUE))); + deleteRecordCount = Integer + .parseInt(p.getProperty(DELETE_RECORD_COUNT_PROPERTY, String.valueOf(Integer.MAX_VALUE))); + updateRecordCount = Integer + .parseInt(p.getProperty(UPDATE_RECORD_COUNT_PROPERTY, String.valueOf(Integer.MAX_VALUE))); + + readUrlMap = getTrace(p.getProperty(READ_TRACE_FILE, READ_TRACE_FILE_DEFAULT), readRecordCount); + insertUrlMap = getTrace(p.getProperty(INSERT_TRACE_FILE, INSERT_TRACE_FILE_DEFAULT), insertRecordCount); + deleteUrlMap = getTrace(p.getProperty(DELETE_TRACE_FILE, DELETE_TRACE_FILE_DEFAULT), deleteRecordCount); + updateUrlMap = getTrace(p.getProperty(UPDATE_TRACE_FILE, UPDATE_TRACE_FILE_DEFAULT), updateRecordCount); + + operationchooser = createOperationGenerator(p); + + // Common distribution for all operations. + String requestDistrib = p.getProperty(REQUEST_DISTRIBUTION_PROPERTY, REQUEST_DISTRIBUTION_PROPERTY_DEFAULT); + + double readZipfconstant = Double + .parseDouble(p.getProperty(READ_ZIPFIAN_CONSTANT, READ_ZIPFIAN_CONSTANT_DEAFULT)); + readKeyChooser = getKeyChooser(requestDistrib, readUrlMap.size(), readZipfconstant, p); + double updateZipfconstant = Double + .parseDouble(p.getProperty(UPDATE_ZIPFIAN_CONSTANT, UPDATE_ZIPFIAN_CONSTANT_DEAFULT)); + updateKeyChooser = getKeyChooser(requestDistrib, updateUrlMap.size(), updateZipfconstant, p); + double insertZipfconstant = Double + .parseDouble(p.getProperty(INSERT_ZIPFIAN_CONSTANT, INSERT_ZIPFIAN_CONSTANT_DEAFULT)); + insertKeyChooser = getKeyChooser(requestDistrib, insertUrlMap.size(), insertZipfconstant, p); + double deleteZipfconstant = Double + .parseDouble(p.getProperty(DELETE_ZIPFIAN_CONSTANT, DELETE_ZIPFIAN_CONSTANT_DEAFULT)); + deleteKeyChooser = getKeyChooser(requestDistrib, deleteUrlMap.size(), deleteZipfconstant, p); + + fieldlengthgenerator = getFieldLengthGenerator(p); + } + + public static DiscreteGenerator createOperationGenerator(final Properties p) { + // Re-using CoreWorkload method. + final DiscreteGenerator operationChooser = CoreWorkload.createOperationGenerator(p); + // Needs special handling for delete operations not supported in CoreWorkload. + double deleteproportion = Double + .parseDouble(p.getProperty(DELETE_PROPORTION_PROPERTY, DELETE_PROPORTION_PROPERTY_DEFAULT)); + if (deleteproportion > 0) + operationChooser.addValue(deleteproportion, "DELETE"); + return operationChooser; + } + + private static NumberGenerator getKeyChooser(String requestDistrib, int recordCount, double zipfContant, + Properties p) throws WorkloadException { + NumberGenerator keychooser = null; + + switch(requestDistrib) { + case "exponential": + double percentile = Double.parseDouble(p.getProperty(ExponentialGenerator.EXPONENTIAL_PERCENTILE_PROPERTY, + ExponentialGenerator.EXPONENTIAL_PERCENTILE_DEFAULT)); + double frac = Double.parseDouble(p.getProperty(ExponentialGenerator.EXPONENTIAL_FRAC_PROPERTY, + ExponentialGenerator.EXPONENTIAL_FRAC_DEFAULT)); + keychooser = new ExponentialGenerator(percentile, recordCount * frac); + break; + case "uniform": + keychooser = new UniformIntegerGenerator(0, recordCount - 1); + break; + case "zipfian": + keychooser = new ZipfianGenerator(recordCount, zipfContant); + break; + case "latest": + throw new WorkloadException("Latest request distribution is not supported for RestWorkload."); + case "hotspot": + double hotsetfraction = Double + .parseDouble(p.getProperty(HOTSPOT_DATA_FRACTION, HOTSPOT_DATA_FRACTION_DEFAULT)); + double hotopnfraction = Double + .parseDouble(p.getProperty(HOTSPOT_OPN_FRACTION, HOTSPOT_OPN_FRACTION_DEFAULT)); + keychooser = new HotspotIntegerGenerator(0, recordCount - 1, hotsetfraction, hotopnfraction); + break; + default: + throw new WorkloadException("Unknown request distribution \"" + requestDistrib + "\""); + } + return keychooser; + } + + protected static NumberGenerator getFieldLengthGenerator(Properties p) throws WorkloadException { + // Re-using CoreWorkload method. + NumberGenerator fieldLengthGenerator = CoreWorkload.getFieldLengthGenerator(p); + String fieldlengthdistribution = p.getProperty( + FIELD_LENGTH_DISTRIBUTION_PROPERTY, FIELD_LENGTH_DISTRIBUTION_PROPERTY_DEFAULT); + // Needs special handling for Zipfian distribution for variable Zipf Constant. + if (fieldlengthdistribution.compareTo("zipfian") == 0) { + int fieldlength = + Integer.parseInt(p.getProperty(FIELD_LENGTH_PROPERTY, FIELD_LENGTH_PROPERTY_DEFAULT)); + double insertsizezipfconstant = Double + .parseDouble(p.getProperty(INSERT_SIZE_ZIPFIAN_CONSTANT, INSERT_SIZE_ZIPFIAN_CONSTANT_DEAFULT)); + fieldLengthGenerator = new ZipfianGenerator(1, fieldlength, insertsizezipfconstant); + } + return fieldLengthGenerator; + } + + /** + * Reads the trace file and returns a URL map. + */ + private static Map getTrace(String filePath, int recordCount) + throws WorkloadException { + Map urlMap = new HashMap(); + int count = 0; + String line; + try { + FileReader inputFile = new FileReader(filePath); + BufferedReader bufferReader = new BufferedReader(inputFile); + while ((line = bufferReader.readLine()) != null) { + urlMap.put(count++, line.trim()); + if (count >= recordCount) + break; + } + bufferReader.close(); + } catch (IOException e) { + throw new WorkloadException( + "Error while reading the trace. Please make sure the trace file path is correct. " + + e.getLocalizedMessage()); + } + return urlMap; + } + + /** + * Not required for Rest Clients as data population is service specific. + */ + @Override + public boolean doInsert(DB db, Object threadstate) { + return false; + } + + @Override + public boolean doTransaction(DB db, Object threadstate) { + switch (operationchooser.nextString()) { + case "UPDATE": + doTransactionUpdate(db); + break; + case "INSERT": + doTransactionInsert(db); + break; + case "DELETE": + doTransactionDelete(db); + break; + default: + doTransactionRead(db); + } + return true; + } + + /** + * Returns next URL to be called. + */ + private String getNextURL(int opType) { + if (opType == 1) + return readUrlMap.get(readKeyChooser.nextValue().intValue()); + else if (opType == 2) + return insertUrlMap.get(insertKeyChooser.nextValue().intValue()); + else if (opType == 3) + return deleteUrlMap.get(deleteKeyChooser.nextValue().intValue()); + else + return updateUrlMap.get(updateKeyChooser.nextValue().intValue()); + } + + @Override + public void doTransactionRead(DB db) { + HashMap result = new HashMap(); + db.read(null, getNextURL(1), null, result); + } + + @Override + public void doTransactionInsert(DB db) { + HashMap value = new HashMap(); + // Create random bytes of insert data with a specific size. + value.put("data", new RandomByteIterator(fieldlengthgenerator.nextValue().longValue())); + db.insert(null, getNextURL(2), value); + } + + public void doTransactionDelete(DB db) { + db.delete(null, getNextURL(3)); + } + + @Override + public void doTransactionUpdate(DB db) { + HashMap value = new HashMap(); + // Create random bytes of update data with a specific size. + value.put("data", new RandomByteIterator(fieldlengthgenerator.nextValue().longValue())); + db.update(null, getNextURL(4), value); + } + +} diff --git a/distribution/pom.xml b/distribution/pom.xml index ff3b1116..db690ffc 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -1,227 +1,232 @@ 4.0.0 com.yahoo.ycsb root 0.12.0-SNAPSHOT ycsb YCSB Release Distribution Builder pom This module creates the release package of the YCSB with all DB library bindings. It is only used by the build process and does not contain any real code of itself. com.yahoo.ycsb core ${project.version} com.yahoo.ycsb accumulo-binding ${project.version} com.yahoo.ycsb aerospike-binding ${project.version} com.yahoo.ycsb arangodb-binding ${project.version} com.yahoo.ycsb asynchbase-binding ${project.version} com.yahoo.ycsb cassandra-binding ${project.version} com.yahoo.ycsb couchbase-binding ${project.version} com.yahoo.ycsb couchbase2-binding ${project.version} com.yahoo.ycsb dynamodb-binding ${project.version} com.yahoo.ycsb elasticsearch-binding ${project.version} com.yahoo.ycsb geode-binding ${project.version} com.yahoo.ycsb googledatastore-binding ${project.version} com.yahoo.ycsb googlebigtable-binding ${project.version} com.yahoo.ycsb hbase094-binding ${project.version} com.yahoo.ycsb hbase098-binding ${project.version} com.yahoo.ycsb hbase10-binding ${project.version} com.yahoo.ycsb hypertable-binding ${project.version} com.yahoo.ycsb infinispan-binding ${project.version} com.yahoo.ycsb jdbc-binding ${project.version} com.yahoo.ycsb kudu-binding ${project.version} com.yahoo.ycsb memcached-binding ${project.version} com.yahoo.ycsb mongodb-binding ${project.version} com.yahoo.ycsb nosqldb-binding ${project.version} com.yahoo.ycsb orientdb-binding ${project.version} com.yahoo.ycsb rados-binding ${project.version} com.yahoo.ycsb redis-binding ${project.version} + + com.yahoo.ycsb + rest-binding + ${project.version} + com.yahoo.ycsb riak-binding ${project.version} com.yahoo.ycsb s3-binding ${project.version} com.yahoo.ycsb solr-binding ${project.version} com.yahoo.ycsb solr6-binding ${project.version} com.yahoo.ycsb tarantool-binding ${project.version} org.apache.maven.plugins maven-assembly-plugin ${maven.assembly.version} src/main/assembly/distribution.xml false posix package single diff --git a/pom.xml b/pom.xml index 510dace4..585bf584 100644 --- a/pom.xml +++ b/pom.xml @@ -1,182 +1,183 @@ 4.0.0 com.yahoo.ycsb root 0.12.0-SNAPSHOT pom YCSB Root This is the top level project that builds, packages the core and all the DB bindings for YCSB infrastructure. scm:git:git://github.com/brianfrankcooper/YCSB.git master https://github.com/brianfrankcooper/YCSB com.puppycrawl.tools checkstyle 7.7.1 org.jdom jdom 1.1 com.google.collections google-collections 1.0 org.slf4j slf4j-api 1.6.4 2.5.5 2.10 1.7.1 0.94.27 0.98.14-hadoop2 1.0.2 1.6.0 3.0.0 1.0.0-incubating.M3 0.2.3 7.2.2.Final 0.9.0 2.1.1 3.0.3 2.0.1 2.1.8 2.0.0 1.10.20 0.81 UTF-8 0.8.0 0.9.5.6 1.4.10 2.3.1 1.6.5 2.0.5 3.1.2 5.4.0 6.2.1 2.7.3 4.0.0 core binding-parent accumulo aerospike arangodb asynchbase azuretablestorage cassandra couchbase couchbase2 distribution dynamodb elasticsearch geode googlebigtable googledatastore hbase094 hbase098 hbase10 hypertable infinispan jdbc kudu memcached mongodb nosqldb orientdb rados redis + rest riak s3 solr solr6 tarantool org.apache.maven.plugins maven-checkstyle-plugin 2.16 org.apache.maven.plugins maven-compiler-plugin 3.3 1.7 1.7 org.apache.maven.plugins maven-checkstyle-plugin validate validate check checkstyle.xml diff --git a/rest/README.md b/rest/README.md new file mode 100644 index 00000000..ffc9a1b8 --- /dev/null +++ b/rest/README.md @@ -0,0 +1,181 @@ + + +## Quick Start + +This section describes how to run YCSB to benchmark HTTP RESTful +webservices. The aim of the rest binding is to benchmark the +performance of any sepecific HTTP RESTful webservices with real +life (production) dataset. This must not be confused with benchmarking +various webservers (like Apache Tomcat, Nginx, Jetty) using a dummy +dataset. + +### 1. Set Up YCSB + +Clone the YCSB git repository and compile: + + git clone git://github.com/brianfrankcooper/YCSB.git + cd YCSB + mvn -pl com.yahoo.ycsb:rest-binding -am clean package + +### 2. Set Up an HTTP Web Service + +There must be a running HTTP RESTful webservice accesible from +the instance on which YCSB is running. If the webservice is +running on the local instance default HTTP port 80, it's base +URL will look like http://127.0.0.1:80/{service_endpoint}. The +rest binding assumes that the webservice to be benchmarked already +has a valid dataset. THe rest module has been designed in this +way for two reasons: + +1. The performance of most webservices depends on the size, pattern +and the nature of the real life dataset accesible from these services. +Hence creating a dummy dataset might not actually reflect the true +performance of a webservice to be benchmarked. + +2. Since many webservices have a non-naive backend which includes +interaction with multiple backend components, tables and databases. +Generating a dummy dataset for such webservices is a non-trivial and +a time consuming task. + +However to benchmark a webservice before it has access to a real +dataset, support for automatic data insertion can be added in the +future. An example of such a scenario is benchmarking a webservice +before it moves to production. + +### 3. Run YCSB + +At this point we assume that you've setup a webservice accesible at +an HTTP endpoint like this: http://{host}:{port}/{service_endpoint}. + +Before you are ready to run please ensure that you have prepared a +trace for the CRUD operations to benchmark your webservice. + +Trace is a collection of URL resources that should be hit in order +to benchmark any webservice. The more realistic this collection of +URL is, the more reliable and accurate are the benchmarking results +because this means simulating the real life workload more accurately. +Tracefile is a file that holds the trace. For example, if your +webservice exists at http://{host}:{port}/{endpoint}, and you want +to benchmark the performance of READS on this webservice with five +resources (namely resource_1, resource_2 ... resource_5) then the +url.trace.read file will look like this: + +http://{host}:{port}/{endpoint}/resource_1 +http://{host}:{port}/{endpoint}/resource_2 +http://{host}:{port}/{endpoint}/resource_3 +http://{host}:{port}/{endpoint}/resource_4 +http://{host}:{port}/{endpoint}/resource_5 + +The rest module will pick up URLs from the above file according to +the `requestdistribution` property (default is zipfian) mentioned in +the rest_workload. In the example above we assume that the property +`url.prefix` (see below for property description) is set to empty. If +url.prefix property is set to `http://{host}:{port}/{endpoint}/` the +equivalent of the read trace given above would look like: + +resource_1 +resource_2 +resource_3 +resource_4 +resource_5 + +In real life the traces for various CRUD operations are diffent +from one another. HTTP GET will rarely have the same URL access +pattern as that of HTTP POST or HTTP PUT. Hence to give enough +flexibility to benchmark webservices, different trace files can +be used for different CRUD operations. However if you wish to use +the same trace for all these operations, just pass the same file +to all these properties - `url.trace.read`, `url.trace.insert`, +`url.trace.update` & `url.trace.delete`. + +Now you are ready to run! Run the rest_workload: + + ./bin/ycsb run rest -s -P workloads/rest_workload + +For further configuration see below: + +### Default Configuration Parameters +The default settings for the rest binding are as follows: + +- `url.prefix` + - The base endpoint URL where the webservice is running. URLs from trace files (DELETE, GET, POST, PUT) will be prefixed with this value before making an HTTP request. A common usage value would be http://127.0.0.1:8080/{yourService} + - Default value is `http://127.0.0.1:80/`. + +- `url.trace.read` + - The path to a trace file that holds the URLs to be invoked for HTTP GET method. URLs must be seperated by a newline. + +- `url.trace.insert` + - The path to a trace file that holds the URLs to be invoked for HTTP POST method. URLs must be seperated by a newline. + +- `url.trace.update` + - The path to a trace file that holds the URLs to be invoked for HTTP PUT method. URLs must be seperated by a newline. + +- `url.trace.delete` + - The path to a trace file that holds the URLs to be invoked for HTTP DELETE method. URLs must be seperated by a newline. + +- `headers` + - The HTTP request headers used for all requests. Headers must be separated by space as a delimiter. + - Default value is `Accept */* Accept-Language en-US,en;q=0.5 Content-Type application/x-www-form-urlencoded user-agent Mozilla/5.0` + +- `timeout.con` + - The HTTP connection timeout in seconds. The response will be considered as an error if the client fails to connect with the server within this time limit. + - Default value is `10` seconds. + +- `timeout.read` + - The HTTP read timeout in seconds. The response will be considered as an error if the client fails to read from the server within this time limit. + - Default value is `10` seconds. + +- `timeout.exec` + - The time within which request must return a response. The response will be considered as an error if the client fails to complete the request within this time limit. + - Default value is `10` seconds. + +- `log.enable` + - A Boolean value to enable console status logs. When true, it will print all the HTTP requests being made and thier response status on the YCSB console window. + - Default value is `false`. + +- `readrecordcount` + - An integer value that signifies the top k URLs (entries) to be picked from the `url.trace.read` file for making HTTP GET requests. Must have a value greater than 0. If this value exceeds the number of entries present in `url.trace.read` file, then k will be set to the number of entries in the file. + - Default value is `10000`. + +- `insertrecordcount` + - An integer value that signifies the top k URLs to be picked from the `url.trace.insert` file for making HTTP POST requests. Must have a value greater than 0. If this value exceeds the number of entries present in `url.trace.insert` file, then k will be set to the number of entries in the file. + - Default value is `5000`. + +- `deleterecordcount` + - An integer value that signifies the top k URLs to be picked from the `url.trace.delete` file for making HTTP DELETE requests. Must have a value greater than 0. If this value exceeds the number of entries present in `url.trace.delete` file, then k will be set to the number of entries in the file. + - Default value is `1000`. + +- `updaterecordcount` + - An integer value that signifies the top k URLs to be picked from the `url.trace.update` file for making HTTP PUT requests. Must have a value greater than 0. If this value exceeds the number of entries present in `url.trace.update` file, then k will be set to the number of entries in the file. + - Default value is `1000`. + +- `readzipfconstant` + - An double value of the Zipf's constant to be used for insert requests. Applicable only if the requestdistribution = `zipfian`. + - Default value is `0.9`. + +- `insertzipfconstant` + - An double value of the Zipf's constant to be used for insert requests. Applicable only if the requestdistribution = `zipfian`. + - Default value is `0.9`. + +- `updatezipfconstant` + - An double value of the Zipf's constant to be used for insert requests. Applicable only if the requestdistribution = `zipfian`. + - Default value is `0.9`. + +- `deletezipfconstant` + - An double value of the Zipf's constant to be used for insert requests. Applicable only if the requestdistribution = `zipfian`. + - Default value is `0.9`. diff --git a/rest/pom.xml b/rest/pom.xml new file mode 100644 index 00000000..2fec0059 --- /dev/null +++ b/rest/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + com.yahoo.ycsb + binding-parent + 0.12.0-SNAPSHOT + ../binding-parent + + + rest-binding + Rest Client Binding + jar + + + 8.0.28 + 2.6 + 4.5.1 + 4.4.4 + 4.12 + 1.16.0 + + + + + com.yahoo.ycsb + core + ${project.version} + provided + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + org.apache.httpcomponents + httpcore + ${httpcore.version} + + + junit + junit + ${junit.version} + test + + + com.github.stefanbirkner + system-rules + ${system-rules.version} + + + org.glassfish.jersey.core + jersey-server + ${jersey.version} + + + org.glassfish.jersey.core + jersey-client + ${jersey.version} + + + org.glassfish.jersey.containers + jersey-container-servlet-core + ${jersey.version} + + + org.apache.tomcat + tomcat-dbcp + ${tomcat.version} + + + org.apache.tomcat + tomcat-juli + + + + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-logging-juli + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-logging-log4j + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-jasper + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${tomcat.version} + + + + diff --git a/rest/src/main/java/com/yahoo/ycsb/webservice/rest/RestClient.java b/rest/src/main/java/com/yahoo/ycsb/webservice/rest/RestClient.java new file mode 100644 index 00000000..2fd14673 --- /dev/null +++ b/rest/src/main/java/com/yahoo/ycsb/webservice/rest/RestClient.java @@ -0,0 +1,370 @@ +/** + * Copyright (c) 2016 YCSB contributors. 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. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ + +package com.yahoo.ycsb.webservice.rest; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Properties; +import java.util.Set; +import java.util.Vector; +import java.util.zip.GZIPInputStream; + +import javax.ws.rs.HttpMethod; + +import org.apache.http.HttpEntity; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; + +import com.yahoo.ycsb.ByteIterator; +import com.yahoo.ycsb.DB; +import com.yahoo.ycsb.DBException; +import com.yahoo.ycsb.Status; +import com.yahoo.ycsb.StringByteIterator; + +/** + * Class responsible for making web service requests for benchmarking purpose. + * Using Apache HttpClient over standard Java HTTP API as this is more flexible + * and provides better functionality. For example HttpClient can automatically + * handle redirects and proxy authentication which the standard Java API can't. + */ +public class RestClient extends DB { + + private static final String URL_PREFIX = "url.prefix"; + private static final String CON_TIMEOUT = "timeout.con"; + private static final String READ_TIMEOUT = "timeout.read"; + private static final String EXEC_TIMEOUT = "timeout.exec"; + private static final String LOG_ENABLED = "log.enable"; + private static final String HEADERS = "headers"; + private static final String COMPRESSED_RESPONSE = "response.compression"; + private boolean compressedResponse; + private boolean logEnabled; + private String urlPrefix; + private Properties props; + private String[] headers; + private CloseableHttpClient client; + private int conTimeout = 10000; + private int readTimeout = 10000; + private int execTimeout = 10000; + private volatile Criteria requestTimedout = new Criteria(false); + + @Override + public void init() throws DBException { + props = getProperties(); + urlPrefix = props.getProperty(URL_PREFIX, "http://127.0.0.1:8080"); + conTimeout = Integer.valueOf(props.getProperty(CON_TIMEOUT, "10")) * 1000; + readTimeout = Integer.valueOf(props.getProperty(READ_TIMEOUT, "10")) * 1000; + execTimeout = Integer.valueOf(props.getProperty(EXEC_TIMEOUT, "10")) * 1000; + logEnabled = Boolean.valueOf(props.getProperty(LOG_ENABLED, "false").trim()); + compressedResponse = Boolean.valueOf(props.getProperty(COMPRESSED_RESPONSE, "false").trim()); + headers = props.getProperty(HEADERS, "Accept */* Content-Type application/xml user-agent Mozilla/5.0 ").trim() + .split(" "); + setupClient(); + } + + private void setupClient() { + RequestConfig.Builder requestBuilder = RequestConfig.custom(); + requestBuilder = requestBuilder.setConnectTimeout(conTimeout); + requestBuilder = requestBuilder.setConnectionRequestTimeout(readTimeout); + requestBuilder = requestBuilder.setSocketTimeout(readTimeout); + HttpClientBuilder clientBuilder = HttpClientBuilder.create().setDefaultRequestConfig(requestBuilder.build()); + this.client = clientBuilder.setConnectionManagerShared(true).build(); + } + + @Override + public Status read(String table, String endpoint, Set fields, HashMap result) { + int responseCode; + try { + responseCode = httpGet(urlPrefix + endpoint, result); + } catch (Exception e) { + responseCode = handleExceptions(e, urlPrefix + endpoint, HttpMethod.GET); + } + if (logEnabled) { + System.err.println(new StringBuilder("GET Request: ").append(urlPrefix).append(endpoint) + .append(" | Response Code: ").append(responseCode).toString()); + } + return getStatus(responseCode); + } + + @Override + public Status insert(String table, String endpoint, HashMap values) { + int responseCode; + try { + responseCode = httpExecute(new HttpPost(urlPrefix + endpoint), values.get("data").toString()); + } catch (Exception e) { + responseCode = handleExceptions(e, urlPrefix + endpoint, HttpMethod.POST); + } + if (logEnabled) { + System.err.println(new StringBuilder("POST Request: ").append(urlPrefix).append(endpoint) + .append(" | Response Code: ").append(responseCode).toString()); + } + return getStatus(responseCode); + } + + @Override + public Status delete(String table, String endpoint) { + int responseCode; + try { + responseCode = httpDelete(urlPrefix + endpoint); + } catch (Exception e) { + responseCode = handleExceptions(e, urlPrefix + endpoint, HttpMethod.DELETE); + } + if (logEnabled) { + System.err.println(new StringBuilder("DELETE Request: ").append(urlPrefix).append(endpoint) + .append(" | Response Code: ").append(responseCode).toString()); + } + return getStatus(responseCode); + } + + @Override + public Status update(String table, String endpoint, HashMap values) { + int responseCode; + try { + responseCode = httpExecute(new HttpPut(urlPrefix + endpoint), values.get("data").toString()); + } catch (Exception e) { + responseCode = handleExceptions(e, urlPrefix + endpoint, HttpMethod.PUT); + } + if (logEnabled) { + System.err.println(new StringBuilder("PUT Request: ").append(urlPrefix).append(endpoint) + .append(" | Response Code: ").append(responseCode).toString()); + } + return getStatus(responseCode); + } + + @Override + public Status scan(String table, String startkey, int recordcount, Set fields, + Vector> result) { + return Status.NOT_IMPLEMENTED; + } + + // Maps HTTP status codes to YCSB status codes. + private Status getStatus(int responseCode) { + int rc = responseCode / 100; + if (responseCode == 400) { + return Status.BAD_REQUEST; + } else if (responseCode == 403) { + return Status.FORBIDDEN; + } else if (responseCode == 404) { + return Status.NOT_FOUND; + } else if (responseCode == 501) { + return Status.NOT_IMPLEMENTED; + } else if (responseCode == 503) { + return Status.SERVICE_UNAVAILABLE; + } else if (rc == 5) { + return Status.ERROR; + } + return Status.OK; + } + + private int handleExceptions(Exception e, String url, String method) { + if (logEnabled) { + System.err.println(new StringBuilder(method).append(" Request: ").append(url).append(" | ") + .append(e.getClass().getName()).append(" occured | Error message: ") + .append(e.getMessage()).toString()); + } + + if (e instanceof ClientProtocolException) { + return 400; + } + return 500; + } + + // Connection is automatically released back in case of an exception. + private int httpGet(String endpoint, HashMap result) throws IOException { + requestTimedout.setIsSatisfied(false); + Thread timer = new Thread(new Timer(execTimeout, requestTimedout)); + timer.start(); + int responseCode = 200; + HttpGet request = new HttpGet(endpoint); + for (int i = 0; i < headers.length; i = i + 2) { + request.setHeader(headers[i], headers[i + 1]); + } + CloseableHttpResponse response = client.execute(request); + responseCode = response.getStatusLine().getStatusCode(); + HttpEntity responseEntity = response.getEntity(); + // If null entity don't bother about connection release. + if (responseEntity != null) { + InputStream stream = responseEntity.getContent(); + /* + * TODO: Gzip Compression must be supported in the future. Header[] + * header = response.getAllHeaders(); + * if(response.getHeaders("Content-Encoding")[0].getValue().contains + * ("gzip")) stream = new GZIPInputStream(stream); + */ + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + StringBuffer responseContent = new StringBuffer(); + String line = ""; + while ((line = reader.readLine()) != null) { + if (requestTimedout.isSatisfied()) { + // Must avoid memory leak. + reader.close(); + stream.close(); + EntityUtils.consumeQuietly(responseEntity); + response.close(); + client.close(); + throw new TimeoutException(); + } + responseContent.append(line); + } + timer.interrupt(); + result.put("response", new StringByteIterator(responseContent.toString())); + // Closing the input stream will trigger connection release. + stream.close(); + } + EntityUtils.consumeQuietly(responseEntity); + response.close(); + client.close(); + return responseCode; + } + + private int httpExecute(HttpEntityEnclosingRequestBase request, String data) throws IOException { + requestTimedout.setIsSatisfied(false); + Thread timer = new Thread(new Timer(execTimeout, requestTimedout)); + timer.start(); + int responseCode = 200; + for (int i = 0; i < headers.length; i = i + 2) { + request.setHeader(headers[i], headers[i + 1]); + } + InputStreamEntity reqEntity = new InputStreamEntity(new ByteArrayInputStream(data.getBytes()), + ContentType.APPLICATION_FORM_URLENCODED); + reqEntity.setChunked(true); + request.setEntity(reqEntity); + CloseableHttpResponse response = client.execute(request); + responseCode = response.getStatusLine().getStatusCode(); + HttpEntity responseEntity = response.getEntity(); + // If null entity don't bother about connection release. + if (responseEntity != null) { + InputStream stream = responseEntity.getContent(); + if (compressedResponse) { + stream = new GZIPInputStream(stream); + } + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + StringBuffer responseContent = new StringBuffer(); + String line = ""; + while ((line = reader.readLine()) != null) { + if (requestTimedout.isSatisfied()) { + // Must avoid memory leak. + reader.close(); + stream.close(); + EntityUtils.consumeQuietly(responseEntity); + response.close(); + client.close(); + throw new TimeoutException(); + } + responseContent.append(line); + } + timer.interrupt(); + // Closing the input stream will trigger connection release. + stream.close(); + } + EntityUtils.consumeQuietly(responseEntity); + response.close(); + client.close(); + return responseCode; + } + + private int httpDelete(String endpoint) throws IOException { + requestTimedout.setIsSatisfied(false); + Thread timer = new Thread(new Timer(execTimeout, requestTimedout)); + timer.start(); + int responseCode = 200; + HttpDelete request = new HttpDelete(endpoint); + for (int i = 0; i < headers.length; i = i + 2) { + request.setHeader(headers[i], headers[i + 1]); + } + CloseableHttpResponse response = client.execute(request); + responseCode = response.getStatusLine().getStatusCode(); + response.close(); + client.close(); + return responseCode; + } + + /** + * Marks the input {@link Criteria} as satisfied when the input time has elapsed. + */ + class Timer implements Runnable { + + private long timeout; + private Criteria timedout; + + public Timer(long timeout, Criteria timedout) { + this.timedout = timedout; + this.timeout = timeout; + } + + @Override + public void run() { + try { + Thread.sleep(timeout); + this.timedout.setIsSatisfied(true); + } catch (InterruptedException e) { + // Do nothing. + } + } + + } + + /** + * Sets the flag when a criteria is fulfilled. + */ + class Criteria { + + private boolean isSatisfied; + + public Criteria(boolean isSatisfied) { + this.isSatisfied = isSatisfied; + } + + public boolean isSatisfied() { + return isSatisfied; + } + + public void setIsSatisfied(boolean satisfied) { + this.isSatisfied = satisfied; + } + + } + + /** + * Private exception class for execution timeout. + */ + class TimeoutException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public TimeoutException() { + super("HTTP Request exceeded execution time limit."); + } + + } + +} diff --git a/rest/src/main/java/com/yahoo/ycsb/webservice/rest/package-info.java b/rest/src/main/java/com/yahoo/ycsb/webservice/rest/package-info.java new file mode 100644 index 00000000..117670c9 --- /dev/null +++ b/rest/src/main/java/com/yahoo/ycsb/webservice/rest/package-info.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2016 YCSB contributors. 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. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ + +/** + * YCSB binding for RESTFul Web Services. + */ +package com.yahoo.ycsb.webservice.rest; + diff --git a/rest/src/test/java/com/yahoo/ycsb/webservice/rest/IntegrationTest.java b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/IntegrationTest.java new file mode 100644 index 00000000..20d3bb7f --- /dev/null +++ b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/IntegrationTest.java @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2016 YCSB contributors. 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. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ + +package com.yahoo.ycsb.webservice.rest; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +import javax.servlet.ServletException; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.contrib.java.lang.system.Assertion; +import org.junit.contrib.java.lang.system.ExpectedSystemExit; +import org.junit.runners.MethodSorters; + +import com.yahoo.ycsb.Client; +import com.yahoo.ycsb.DBException; +import com.yahoo.ycsb.webservice.rest.Utils; + +/** + * Integration test cases to verify the end to end working of the rest-binding + * module. It performs these steps in order. 1. Runs an embedded Tomcat + * server with a mock RESTFul web service. 2. Invokes the {@link Client} + * class with the required parameters to start benchmarking the mock REST + * service. 3. Compares the response stored in the output file by {@link Client} + * class with the response expected. 4. Stops the embedded Tomcat server. + * Cases for verifying the handling of different HTTP status like 2xx & 5xx have + * been included in success and failure test cases. + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class IntegrationTest { + + @Rule + public final ExpectedSystemExit exit = ExpectedSystemExit.none(); + + private static int port = 8080; + private static Tomcat tomcat; + private static final String WORKLOAD_FILEPATH = IntegrationTest.class.getClassLoader().getResource("workload_rest").getPath(); + private static final String TRACE_FILEPATH = IntegrationTest.class.getClassLoader().getResource("trace.txt").getPath(); + private static final String ERROR_TRACE_FILEPATH = IntegrationTest.class.getClassLoader().getResource("error_trace.txt").getPath(); + private static final String RESULTS_FILEPATH = IntegrationTest.class.getClassLoader().getResource(".").getPath() + "results.txt"; + + @BeforeClass + public static void init() throws ServletException, LifecycleException, FileNotFoundException, IOException, + DBException, InterruptedException { + String webappDirLocation = IntegrationTest.class.getClassLoader().getResource("WebContent").getPath(); + while (!Utils.available(port)) { + port++; + } + tomcat = new Tomcat(); + tomcat.setPort(Integer.valueOf(port)); + Context context = tomcat.addWebapp("/webService", new File(webappDirLocation).getAbsolutePath()); + Tomcat.addServlet(context, "jersey-container-servlet", resourceConfig()); + context.addServletMapping("/rest/*", "jersey-container-servlet"); + tomcat.start(); + // Allow time for proper startup. + Thread.sleep(1000); + } + + @AfterClass + public static void cleanUp() throws LifecycleException { + tomcat.stop(); + } + + // All read operations during benchmark are executed successfully with an HTTP OK status. + @Test + public void testReadOpsBenchmarkSuccess() throws InterruptedException { + exit.expectSystemExit(); + exit.checkAssertionAfterwards(new Assertion() { + @Override + public void checkAssertion() throws Exception { + List results = Utils.read(RESULTS_FILEPATH); + assertEquals(true, results.contains("[READ], Return=OK, 1")); + Utils.delete(RESULTS_FILEPATH); + } + }); + Client.main(getArgs(TRACE_FILEPATH, 1, 0, 0, 0)); + } + + //All read operations during benchmark are executed with an HTTP 500 error. + @Test + public void testReadOpsBenchmarkFailure() throws InterruptedException { + exit.expectSystemExit(); + exit.checkAssertionAfterwards(new Assertion() { + @Override + public void checkAssertion() throws Exception { + List results = Utils.read(RESULTS_FILEPATH); + assertEquals(true, results.contains("[READ], Return=ERROR, 1")); + Utils.delete(RESULTS_FILEPATH); + } + }); + Client.main(getArgs(ERROR_TRACE_FILEPATH, 1, 0, 0, 0)); + } + + //All insert operations during benchmark are executed successfully with an HTTP OK status. + @Test + public void testInsertOpsBenchmarkSuccess() throws InterruptedException { + exit.expectSystemExit(); + exit.checkAssertionAfterwards(new Assertion() { + @Override + public void checkAssertion() throws Exception { + List results = Utils.read(RESULTS_FILEPATH); + assertEquals(true, results.contains("[INSERT], Return=OK, 1")); + Utils.delete(RESULTS_FILEPATH); + } + }); + Client.main(getArgs(TRACE_FILEPATH, 0, 1, 0, 0)); + } + + //All read operations during benchmark are executed with an HTTP 500 error. + @Test + public void testInsertOpsBenchmarkFailure() throws InterruptedException { + exit.expectSystemExit(); + exit.checkAssertionAfterwards(new Assertion() { + @Override + public void checkAssertion() throws Exception { + List results = Utils.read(RESULTS_FILEPATH); + assertEquals(true, results.contains("[INSERT], Return=ERROR, 1")); + Utils.delete(RESULTS_FILEPATH); + } + }); + Client.main(getArgs(ERROR_TRACE_FILEPATH, 0, 1, 0, 0)); + } + + //All update operations during benchmark are executed successfully with an HTTP OK status. + @Test + public void testUpdateOpsBenchmarkSuccess() throws InterruptedException { + exit.expectSystemExit(); + exit.checkAssertionAfterwards(new Assertion() { + @Override + public void checkAssertion() throws Exception { + List results = Utils.read(RESULTS_FILEPATH); + assertEquals(true, results.contains("[UPDATE], Return=OK, 1")); + Utils.delete(RESULTS_FILEPATH); + } + }); + Client.main(getArgs(TRACE_FILEPATH, 0, 0, 1, 0)); + } + + //All read operations during benchmark are executed with an HTTP 500 error. + @Test + public void testUpdateOpsBenchmarkFailure() throws InterruptedException { + exit.expectSystemExit(); + exit.checkAssertionAfterwards(new Assertion() { + @Override + public void checkAssertion() throws Exception { + List results = Utils.read(RESULTS_FILEPATH); + assertEquals(true, results.contains("[UPDATE], Return=ERROR, 1")); + Utils.delete(RESULTS_FILEPATH); + } + }); + Client.main(getArgs(ERROR_TRACE_FILEPATH, 0, 0, 1, 0)); + } + + //All delete operations during benchmark are executed successfully with an HTTP OK status. + @Test + public void testDeleteOpsBenchmarkSuccess() throws InterruptedException { + exit.expectSystemExit(); + exit.checkAssertionAfterwards(new Assertion() { + @Override + public void checkAssertion() throws Exception { + List results = Utils.read(RESULTS_FILEPATH); + assertEquals(true, results.contains("[DELETE], Return=OK, 1")); + Utils.delete(RESULTS_FILEPATH); + } + }); + Client.main(getArgs(TRACE_FILEPATH, 0, 0, 0, 1)); + } + + //All read operations during benchmark are executed with an HTTP 500 error. + @Test + public void testDeleteOpsBenchmarkFailure() throws InterruptedException { + exit.expectSystemExit(); + exit.checkAssertionAfterwards(new Assertion() { + @Override + public void checkAssertion() throws Exception { + List results = Utils.read(RESULTS_FILEPATH); + assertEquals(true, results.contains("[DELETE], Return=ERROR, 1")); + Utils.delete(RESULTS_FILEPATH); + } + }); + Client.main(getArgs(ERROR_TRACE_FILEPATH, 0, 0, 0, 1)); + } + + private String[] getArgs(String traceFilePath, float rp, float ip, float up, float dp) { + String[] args = new String[25]; + args[0] = "-target"; + args[1] = "1"; + args[2] = "-t"; + args[3] = "-P"; + args[4] = WORKLOAD_FILEPATH; + args[5] = "-p"; + args[6] = "url.prefix=http://127.0.0.1:"+port+"/webService/rest/resource/"; + args[7] = "-p"; + args[8] = "url.trace.read=" + traceFilePath; + args[9] = "-p"; + args[10] = "url.trace.insert=" + traceFilePath; + args[11] = "-p"; + args[12] = "url.trace.update=" + traceFilePath; + args[13] = "-p"; + args[14] = "url.trace.delete=" + traceFilePath; + args[15] = "-p"; + args[16] = "exportfile=" + RESULTS_FILEPATH; + args[17] = "-p"; + args[18] = "readproportion=" + rp; + args[19] = "-p"; + args[20] = "updateproportion=" + up; + args[21] = "-p"; + args[22] = "deleteproportion=" + dp; + args[23] = "-p"; + args[24] = "insertproportion=" + ip; + return args; + } + + private static ServletContainer resourceConfig() { + return new ServletContainer(new ResourceConfig(new ResourceLoader().getClasses())); + } + +} \ No newline at end of file diff --git a/rest/src/test/java/com/yahoo/ycsb/webservice/rest/ResourceLoader.java b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/ResourceLoader.java new file mode 100644 index 00000000..4a7a9f8e --- /dev/null +++ b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/ResourceLoader.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2016 YCSB contributors. 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. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ + +package com.yahoo.ycsb.webservice.rest; + +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.core.Application; + +/** + * Class responsible for loading mock rest resource class like + * {@link RestTestResource}. + */ +public class ResourceLoader extends Application { + + @Override + public Set> getClasses() { + final Set> classes = new HashSet>(); + classes.add(RestTestResource.class); + return classes; + } + +} \ No newline at end of file diff --git a/rest/src/test/java/com/yahoo/ycsb/webservice/rest/RestClientTest.java b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/RestClientTest.java new file mode 100644 index 00000000..d48f1b7e --- /dev/null +++ b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/RestClientTest.java @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2016 YCSB contributors. 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. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ + +package com.yahoo.ycsb.webservice.rest; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Properties; + +import javax.servlet.ServletException; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.yahoo.ycsb.ByteIterator; +import com.yahoo.ycsb.DBException; +import com.yahoo.ycsb.Status; +import com.yahoo.ycsb.StringByteIterator; + +/** + * Test cases to verify the {@link RestClient} of the rest-binding + * module. It performs these steps in order. 1. Runs an embedded Tomcat + * server with a mock RESTFul web service. 2. Invokes the {@link RestClient} + * class for all the various methods which make HTTP calls to the mock REST + * service. 3. Compares the response from such calls to the mock REST + * service with the response expected. 4. Stops the embedded Tomcat server. + * Cases for verifying the handling of different HTTP status like 2xx, 4xx & + * 5xx have been included in success and failure test cases. + */ +public class RestClientTest { + + private static Integer port = 8080; + private static Tomcat tomcat; + private static RestClient rc = new RestClient(); + private static final String RESPONSE_TAG = "response"; + private static final String DATA_TAG = "data"; + private static final String VALID_RESOURCE = "resource_valid"; + private static final String INVALID_RESOURCE = "resource_invalid"; + private static final String ABSENT_RESOURCE = "resource_absent"; + private static final String UNAUTHORIZED_RESOURCE = "resource_unauthorized"; + private static final String INPUT_DATA = "onetwo"; + + @BeforeClass + public static void init() throws IOException, DBException, ServletException, LifecycleException, InterruptedException { + String webappDirLocation = IntegrationTest.class.getClassLoader().getResource("WebContent").getPath(); + while (!Utils.available(port)) { + port++; + } + tomcat = new Tomcat(); + tomcat.setPort(Integer.valueOf(port)); + Context context = tomcat.addWebapp("/webService", new File(webappDirLocation).getAbsolutePath()); + Tomcat.addServlet(context, "jersey-container-servlet", resourceConfig()); + context.addServletMapping("/rest/*", "jersey-container-servlet"); + tomcat.start(); + // Allow time for proper startup. + Thread.sleep(1000); + Properties props = new Properties(); + props.load(new FileReader(RestClientTest.class.getClassLoader().getResource("workload_rest").getPath())); + // Update the port value in the url.prefix property. + props.setProperty("url.prefix", props.getProperty("url.prefix").replaceAll("PORT", port.toString())); + rc.setProperties(props); + rc.init(); + } + + @AfterClass + public static void cleanUp() throws DBException { + rc.cleanup(); + } + + // Read success. + @Test + public void read_200() { + HashMap result = new HashMap(); + Status status = rc.read(null, VALID_RESOURCE, null, result); + assertEquals(Status.OK, status); + assertEquals(result.get(RESPONSE_TAG).toString(), "HTTP GET response to: "+ VALID_RESOURCE); + } + + // Unauthorized request error. + @Test + public void read_403() { + HashMap result = new HashMap(); + Status status = rc.read(null, UNAUTHORIZED_RESOURCE, null, result); + assertEquals(Status.FORBIDDEN, status); + } + + //Not found error. + @Test + public void read_404() { + HashMap result = new HashMap(); + Status status = rc.read(null, ABSENT_RESOURCE, null, result); + assertEquals(Status.NOT_FOUND, status); + } + + // Server error. + @Test + public void read_500() { + HashMap result = new HashMap(); + Status status = rc.read(null, INVALID_RESOURCE, null, result); + assertEquals(Status.ERROR, status); + } + + // Insert success. + @Test + public void insert_200() { + HashMap data = new HashMap(); + data.put(DATA_TAG, new StringByteIterator(INPUT_DATA)); + Status status = rc.insert(null, VALID_RESOURCE, data); + assertEquals(Status.OK, status); + } + + @Test + public void insert_403() { + HashMap data = new HashMap(); + data.put(DATA_TAG, new StringByteIterator(INPUT_DATA)); + Status status = rc.insert(null, UNAUTHORIZED_RESOURCE, data); + assertEquals(Status.FORBIDDEN, status); + } + + @Test + public void insert_404() { + HashMap data = new HashMap(); + data.put(DATA_TAG, new StringByteIterator(INPUT_DATA)); + Status status = rc.insert(null, ABSENT_RESOURCE, data); + assertEquals(Status.NOT_FOUND, status); + } + + @Test + public void insert_500() { + HashMap data = new HashMap(); + data.put(DATA_TAG, new StringByteIterator(INPUT_DATA)); + Status status = rc.insert(null, INVALID_RESOURCE, data); + assertEquals(Status.ERROR, status); + } + + // Delete success. + @Test + public void delete_200() { + Status status = rc.delete(null, VALID_RESOURCE); + assertEquals(Status.OK, status); + } + + @Test + public void delete_403() { + Status status = rc.delete(null, UNAUTHORIZED_RESOURCE); + assertEquals(Status.FORBIDDEN, status); + } + + @Test + public void delete_404() { + Status status = rc.delete(null, ABSENT_RESOURCE); + assertEquals(Status.NOT_FOUND, status); + } + + @Test + public void delete_500() { + Status status = rc.delete(null, INVALID_RESOURCE); + assertEquals(Status.ERROR, status); + } + + @Test + public void update_200() { + HashMap data = new HashMap(); + data.put(DATA_TAG, new StringByteIterator(INPUT_DATA)); + Status status = rc.update(null, VALID_RESOURCE, data); + assertEquals(Status.OK, status); + } + + @Test + public void update_403() { + HashMap data = new HashMap(); + data.put(DATA_TAG, new StringByteIterator(INPUT_DATA)); + Status status = rc.update(null, UNAUTHORIZED_RESOURCE, data); + assertEquals(Status.FORBIDDEN, status); + } + + @Test + public void update_404() { + HashMap data = new HashMap(); + data.put(DATA_TAG, new StringByteIterator(INPUT_DATA)); + Status status = rc.update(null, ABSENT_RESOURCE, data); + assertEquals(Status.NOT_FOUND, status); + } + + @Test + public void update_500() { + HashMap data = new HashMap(); + data.put(DATA_TAG, new StringByteIterator(INPUT_DATA)); + Status status = rc.update(null, INVALID_RESOURCE, data); + assertEquals(Status.ERROR, status); + } + + @Test + public void scan() { + assertEquals(Status.NOT_IMPLEMENTED, rc.scan(null, null, 0, null, null)); + } + + private static ServletContainer resourceConfig() { + return new ServletContainer(new ResourceConfig(new ResourceLoader().getClasses())); + } + +} diff --git a/rest/src/test/java/com/yahoo/ycsb/webservice/rest/RestTestResource.java b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/RestTestResource.java new file mode 100644 index 00000000..11de7248 --- /dev/null +++ b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/RestTestResource.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2016 YCSB contributors. 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. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ + +package com.yahoo.ycsb.webservice.rest; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Class that implements a mock RESTFul web service to be used for integration + * testing. + */ +@Path("/resource/{id}") +public class RestTestResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Response respondToGET(@PathParam("id") String id) { + return processRequests(id, HttpMethod.GET); + } + + @POST + @Produces(MediaType.TEXT_PLAIN) + public Response respondToPOST(@PathParam("id") String id) { + return processRequests(id, HttpMethod.POST); + } + + @DELETE + @Produces(MediaType.TEXT_PLAIN) + public Response respondToDELETE(@PathParam("id") String id) { + return processRequests(id, HttpMethod.DELETE); + } + + @PUT + @Produces(MediaType.TEXT_PLAIN) + public Response respondToPUT(@PathParam("id") String id) { + return processRequests(id, HttpMethod.PUT); + } + + private static Response processRequests(String id, String method) { + if (id.equals("resource_invalid")) + return Response.serverError().build(); + else if (id.equals("resource_absent")) + return Response.status(Response.Status.NOT_FOUND).build(); + else if (id.equals("resource_unauthorized")) + return Response.status(Response.Status.FORBIDDEN).build(); + return Response.ok("HTTP " + method + " response to: " + id).build(); + } +} \ No newline at end of file diff --git a/rest/src/test/java/com/yahoo/ycsb/webservice/rest/Utils.java b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/Utils.java new file mode 100644 index 00000000..0b8ae1bb --- /dev/null +++ b/rest/src/test/java/com/yahoo/ycsb/webservice/rest/Utils.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2016 YCSB contributors. 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. See the License for the specific language governing + * permissions and limitations under the License. See accompanying + * LICENSE file. + */ + +package com.yahoo.ycsb.webservice.rest; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.List; + +/** + * Holds the common utility methods. + */ +public class Utils { + + /** + * Returns true if the port is available. + * + * @param port + * @return isAvailable + */ + public static boolean available(int port) { + ServerSocket ss = null; + DatagramSocket ds = null; + try { + ss = new ServerSocket(port); + ss.setReuseAddress(true); + ds = new DatagramSocket(port); + ds.setReuseAddress(true); + return true; + } catch (IOException e) { + } finally { + if (ds != null) { + ds.close(); + } + if (ss != null) { + try { + ss.close(); + } catch (IOException e) { + /* should not be thrown */ + } + } + } + return false; + } + + public static List read(String filepath) { + List list = new ArrayList(); + try { + BufferedReader file = new BufferedReader(new FileReader(filepath)); + String line = null; + while ((line = file.readLine()) != null) { + list.add(line.trim()); + } + file.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return list; + } + + public static void delete(String filepath) { + try { + new File(filepath).delete(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/rest/src/test/resources/WebContent/index.html b/rest/src/test/resources/WebContent/index.html new file mode 100644 index 00000000..ded87fc2 --- /dev/null +++ b/rest/src/test/resources/WebContent/index.html @@ -0,0 +1,13 @@ + + + + + + rest-binding + + + + Welcome to the rest-binding integration test cases! + + + \ No newline at end of file diff --git a/rest/src/test/resources/error_trace.txt b/rest/src/test/resources/error_trace.txt new file mode 100644 index 00000000..18ff9cd6 --- /dev/null +++ b/rest/src/test/resources/error_trace.txt @@ -0,0 +1 @@ +resource_invalid \ No newline at end of file diff --git a/rest/src/test/resources/trace.txt b/rest/src/test/resources/trace.txt new file mode 100644 index 00000000..65a600da --- /dev/null +++ b/rest/src/test/resources/trace.txt @@ -0,0 +1,5 @@ +resource_1 +resource_2 +resource_3 +resource_4 +resource_5 \ No newline at end of file diff --git a/rest/src/test/resources/workload_rest b/rest/src/test/resources/workload_rest new file mode 100644 index 00000000..e4df8323 --- /dev/null +++ b/rest/src/test/resources/workload_rest @@ -0,0 +1,68 @@ +# Copyright (c) 2016 Yahoo! Inc. 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. See the License for the specific language governing +# permissions and limitations under the License. See accompanying +# LICENSE file. + + +# Yahoo! Cloud System Benchmark +# Workload A: Update heavy workload +# Application example: Session store recording recent actions +# +# Read/update ratio: 50/50 +# Default data size: 1 KB records (10 fields, 100 bytes each, plus key) +# Request distribution: zipfian + +# Core Properties +workload=com.yahoo.ycsb.workloads.RestWorkload +db=com.yahoo.ycsb.webservice.rest.RestClient +exporter=com.yahoo.ycsb.measurements.exporter.TextMeasurementsExporter +threadcount=1 +fieldlengthdistribution=uniform +measurementtype=hdrhistogram + +# Workload Properties +fieldcount=1 +fieldlength=2500 +readproportion=1 +updateproportion=0 +deleteproportion=0 +insertproportion=0 +requestdistribution=zipfian +operationcount=1 +maxexecutiontime=720 + +# Custom Properties +url.prefix=http://127.0.0.1:PORT/webService/rest/resource/ +url.trace.read=/src/test/resource/trace.txt +url.trace.insert=/src/test/resource/trace.txt +url.trace.update=/src/test/resource/trace.txt +url.trace.delete=/src/test/resource/trace.txt +# Header must be separated by space. Other delimiters might occur as header values and hence can not be used. +headers=Accept */* Accept-Language en-US,en;q=0.5 Content-Type application/x-www-form-urlencoded user-agent Mozilla/5.0 Connection close +timeout.con=60 +timeout.read=60 +timeout.exec=60 +log.enable=false +readrecordcount=10000 +insertrecordcount=5000 +deleterecordcount=1000 +updaterecordcount=1000 +readzipfconstant=0.9 +insertzipfconstant=0.9 +updatezipfconstant=0.9 +deletezipfconstant=0.9 + + +# Measurement Properties +hdrhistogram.percentiles=50,90,95,99 +histogram.buckets=1 \ No newline at end of file