diff --git a/bin/bindings.properties b/bin/bindings.properties index d75730c12e524ef1e81ca11c2b3493fbea93a4f9..580ae74291d2d86e1ca75b1cec38979a3f1ee624 100644 --- a/bin/bindings.properties +++ b/bin/bindings.properties @@ -56,6 +56,7 @@ 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 diff --git a/bin/ycsb b/bin/ycsb index e0f7092e62aebb7404aaeb44d47f6049a11b814e..72e218a5c19055d557cbff79e69051af21bb8c2c 100755 --- a/bin/ycsb +++ b/bin/ycsb @@ -81,6 +81,7 @@ DATABASES = { "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", diff --git a/checkstyle.xml b/checkstyle.xml index 92d1ce37f8751a1cd2611cdf68ffca76a7e14fd1..af0065d07693733fdfdf5a8f5524816f8a8d2f6a 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -144,7 +144,9 @@ LICENSE file. <!-- Checks for blocks. You know, those {}'s --> <!-- See http://checkstyle.sf.net/config_blocks.html --> <module name="AvoidNestedBlocks"/> - <module name="EmptyBlock"/> + <module name="EmptyBlock"> + <property name="option" value="text"/> + </module> <module name="LeftCurly"/> <module name="NeedBraces"/> <module name="RightCurly"/> 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 0000000000000000000000000000000000000000..b36c65a99c55676c557e2fcb79f516a190411bee --- /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<Integer, String> readUrlMap; + private Map<Integer, String> insertUrlMap; + private Map<Integer, String> deleteUrlMap; + private Map<Integer, String> 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<Integer, String> getTrace(String filePath, int recordCount) + throws WorkloadException { + Map<Integer, String> urlMap = new HashMap<Integer, String>(); + 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<String, ByteIterator> result = new HashMap<String, ByteIterator>(); + db.read(null, getNextURL(1), null, result); + } + + @Override + public void doTransactionInsert(DB db) { + HashMap<String, ByteIterator> value = new HashMap<String, ByteIterator>(); + // 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<String, ByteIterator> value = new HashMap<String, ByteIterator>(); + // 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 ff3b111602a7f880895be920d82b47beb73338c5..db690ffc187f4bf12f81628a61a91af9b934d47d 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -164,6 +164,11 @@ LICENSE file. <artifactId>redis-binding</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>com.yahoo.ycsb</groupId> + <artifactId>rest-binding</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>com.yahoo.ycsb</groupId> <artifactId>riak-binding</artifactId> diff --git a/pom.xml b/pom.xml index 510dace400b7021f3702da335d1d3f56a0125099..585bf58447b6d0866456682752b4aedd0702a887 100644 --- a/pom.xml +++ b/pom.xml @@ -133,6 +133,7 @@ LICENSE file. <module>orientdb</module> <module>rados</module> <module>redis</module> + <module>rest</module> <module>riak</module> <module>s3</module> <module>solr</module> diff --git a/rest/README.md b/rest/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ffc9a1b89b97545229ffabe0f69f09c7e7b39fe2 --- /dev/null +++ b/rest/README.md @@ -0,0 +1,181 @@ +<!-- +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. +--> + +## 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 0000000000000000000000000000000000000000..2fec005943d0b8121ae5a1209e50a9f73cf3dc1f --- /dev/null +++ b/rest/pom.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Copyright (c) 2011 YCSB project, 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. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.yahoo.ycsb</groupId> + <artifactId>binding-parent</artifactId> + <version>0.12.0-SNAPSHOT</version> + <relativePath>../binding-parent</relativePath> + </parent> + + <artifactId>rest-binding</artifactId> + <name>Rest Client Binding</name> + <packaging>jar</packaging> + + <properties> + <tomcat.version>8.0.28</tomcat.version> + <jersey.version>2.6</jersey.version> + <httpclient.version>4.5.1</httpclient.version> + <httpcore.version>4.4.4</httpcore.version> + <junit.version>4.12</junit.version> + <system-rules.version>1.16.0</system-rules.version> + </properties> + + <dependencies> + <dependency> + <groupId>com.yahoo.ycsb</groupId> + <artifactId>core</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>${httpclient.version}</version> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpcore</artifactId> + <version>${httpcore.version}</version> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.github.stefanbirkner</groupId> + <artifactId>system-rules</artifactId> + <version>${system-rules.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${jersey.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <version>${jersey.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + <version>${jersey.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-dbcp</artifactId> + <version>${tomcat.version}</version> + <exclusions> + <exclusion> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-juli</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-core</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-logging-juli</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-logging-log4j</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-jasper</artifactId> + <version>${tomcat.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-websocket</artifactId> + <version>${tomcat.version}</version> + </dependency> + </dependencies> + +</project> 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 0000000000000000000000000000000000000000..2fd14673c3e241bb21b48f559c78ffbd37d4e886 --- /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<String> fields, HashMap<String, ByteIterator> 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<String, ByteIterator> 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<String, ByteIterator> 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<String> fields, + Vector<HashMap<String, ByteIterator>> 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<String, ByteIterator> 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 0000000000000000000000000000000000000000..117670c9a91ab44d3c0318f275cb6b45bc3e20b0 --- /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 0000000000000000000000000000000000000000..20d3bb7fb34a63515be9adc143ad5f2f4c6f0419 --- /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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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 0000000000000000000000000000000000000000..4a7a9f8ef86cfaf46e910de92fae69222b1cb4ea --- /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<Class<?>> getClasses() { + final Set<Class<?>> classes = new HashSet<Class<?>>(); + 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 0000000000000000000000000000000000000000..d48f1b7e229733d24291fbb2930373de61a1cfe0 --- /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 = "<field1>one</field1><field2>two</field2>"; + + @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<String, ByteIterator> result = new HashMap<String, ByteIterator>(); + 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<String, ByteIterator> result = new HashMap<String, ByteIterator>(); + Status status = rc.read(null, UNAUTHORIZED_RESOURCE, null, result); + assertEquals(Status.FORBIDDEN, status); + } + + //Not found error. + @Test + public void read_404() { + HashMap<String, ByteIterator> result = new HashMap<String, ByteIterator>(); + Status status = rc.read(null, ABSENT_RESOURCE, null, result); + assertEquals(Status.NOT_FOUND, status); + } + + // Server error. + @Test + public void read_500() { + HashMap<String, ByteIterator> result = new HashMap<String, ByteIterator>(); + Status status = rc.read(null, INVALID_RESOURCE, null, result); + assertEquals(Status.ERROR, status); + } + + // Insert success. + @Test + public void insert_200() { + HashMap<String, ByteIterator> data = new HashMap<String, ByteIterator>(); + 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<String, ByteIterator> data = new HashMap<String, ByteIterator>(); + 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<String, ByteIterator> data = new HashMap<String, ByteIterator>(); + 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<String, ByteIterator> data = new HashMap<String, ByteIterator>(); + 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<String, ByteIterator> data = new HashMap<String, ByteIterator>(); + 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<String, ByteIterator> data = new HashMap<String, ByteIterator>(); + 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<String, ByteIterator> data = new HashMap<String, ByteIterator>(); + 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<String, ByteIterator> data = new HashMap<String, ByteIterator>(); + 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 0000000000000000000000000000000000000000..11de724816e377e4718ecc00c3edef582dd62cd2 --- /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 0000000000000000000000000000000000000000..0b8ae1bba1ba2e08e3778443f1532f5a339804dc --- /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<String> read(String filepath) { + List<String> list = new ArrayList<String>(); + 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 0000000000000000000000000000000000000000..ded87fc20c405fe6bb44e26afccf754eaacb462a --- /dev/null +++ b/rest/src/test/resources/WebContent/index.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + + <head> + <meta charset="ISO-8859-1"> + <title>rest-binding</title> + </head> + + <body> + Welcome to the rest-binding integration test cases! + </body> + +</html> \ 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 0000000000000000000000000000000000000000..18ff9cd632190a6c263b1cb2cfcec7f13c3464bb --- /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 0000000000000000000000000000000000000000..65a600da2fafbf9e29c5649936f1164a49a92323 --- /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 0000000000000000000000000000000000000000..e4df83235a0fa538764e89c6f1a70288c8eb8721 --- /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