diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b5d9cac --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +# Copyright (c) 2015, 2017 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. +# For more info, see: http://EditorConfig.org +root = true + +[*.java] +indent_style = space +indent_size = 2 +continuation_indent_size = 4 + +[*.md] +indent_style = space +indent_size = 2 +continuation_indent_size = 4 + +[*.xml] +indent_style = space +indent_size = 2 +continuation_indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa65bac --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# ignore compiled byte code +target + +# ignore output files from testing +output* + +# ignore standard Eclipse files +.project +.classpath +.settings +.checkstyle + +# ignore standard IntelliJ files +.idea/ +*.iml +*.ipr +*.iws + +# ignore standard Vim and Emacs temp files +*.swp +*~ + +# ignore standard Mac OS X files/dirs +.DS_Store +/differentbin/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5fb89e9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,53 @@ +# Copyright (c) 2010 Yahoo! Inc., 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. + +# more info here about TravisCI and Java projects +# http://docs.travis-ci.com/user/languages/java/ + +language: java + +jdk: + - openjdk8 + - openjdk11 + - oraclejdk11 + +addons: + hosts: + - myshorthost + hostname: myshorthost + postgresql: "9.5" + +install: + - mvn -N io.takari:maven:0.7.7:wrapper -Dmaven=3.6.3 + - ./mvnw install -q -DskipTests=true + +script: ./mvnw test -q + +before_script: + - psql -c 'CREATE database test;' -U postgres + - psql -c 'CREATE TABLE usertable (YCSB_KEY VARCHAR(255) PRIMARY KEY not NULL, YCSB_VALUE JSONB not NULL);' -U postgres -d test + - psql -c 'GRANT ALL PRIVILEGES ON DATABASE test to postgres;' -U postgres + +# Services to start for tests. +services: + - ignite + - mongodb + - postgresql +# temporarily disable riak. failing, docs offline. +# - riak + +# Can't use container based infra because of hosts/hostname +sudo: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b2e0ee4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,133 @@ + +## How To Contribute + +As more and more databases are created to handle distributed or "cloud" workloads, YCSB needs contributors to write clients to test them. And of course we always need bug fixes, updates for existing databases and new features to keep YCSB going. Here are some guidelines to follow when digging into the code. + +## Project Source + +YCSB is located in a Git repository hosted on GitHub at [https://github.com/brianfrankcooper/YCSB](https://github.com/brianfrankcooper/YCSB). To modify the code, fork the main repo into your own GitHub account or organization and commit changes there. + +YCSB is written in Java (as most of the new cloud data stores at beginning of the project were written in Java) and is laid out as a multi-module Maven project. You should be able to import the project into your favorite IDE or environment easily. For more details about the Maven layout see the [Guide to Working with Multiple Modules](https://maven.apache.org/guides/mini/guide-multiple-modules.html). + +## Licensing + +YCSB is licensed under the Apache License, Version 2.0 (APL2). Every file included in the project must include the APL header. For example, each Java source file must have a header similar to the following: + +```java +/** + * Copyright (c) 2015-2017 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. + */ +``` + +When modifying files that already have a license header, please update the year when you made your edits. E.g. change ``Copyright (c) 2010 Yahoo! Inc., 2012 - 2016 YCSB contributors.`` to ``Copyright (c) 2010 Yahoo! Inc., 2012 - 2017 YCSB contributors.`` If the file only has ``Copyright (c) 2010 Yahoo! Inc.``, append the current year as in ``Copyright (c) 2010 Yahoo! Inc., 2017 YCSB contributors.``. + +**WARNING**: It should go without saying, but don't copy and paste code from outside authors or sources. If you are a database author and want to copy some example code, it must be APL2 compatible. + +Client bindings to non-APL databases are perfectly acceptable, as data stores are meant to be used from all kinds of projects. Just make sure not to copy any code or commit libraries or binaries into the YCSB code base. Link to them in the Maven pom file. + +## Issues and Support + +To track bugs, feature requests and releases we use GitHub's integrated [Issues](https://github.com/brianfrankcooper/YCSB/issues). If you find a bug or problem, open an issue with a descriptive title and as many details as you can give us in the body (stack traces, log files, etc). Then if you can create a fix, follow the PR guidelines below. + +**Note** Before embarking on a code change or DB, search through the existing issues and pull requests to see if anyone is already working on it. Reach out to them if so. + +For general support, please use the mailing list hosted (of course) with Yahoo groups at [http://groups.yahoo.com/group/ycsb-users](http://groups.yahoo.com/group/ycsb-users). + +## Code Style + +A Java coding style guide is enforced via the Maven CheckStyle plugin. We try not to be too draconian with enforcement but the biggies include: + +* Whitespaces instead of tabs. +* Proper Javadocs for methods and classes. +* Camel case member names. +* Upper camel case classes and method names. +* Line length. + +CheckStyle will run for pull requests or if you create a package locally so if you just compile and push a commit, you may be surprised when the build fails with a style issue. Just execute ``mvn checkstyle:checkstyle `` before you open a PR and you should avoid any suprises. + +## Platforms + +Since most data bases aim to support multiple platforms, YCSB aims to run on as many as possible as well. Besides **Linux** and **macOS**, YCSB must compile and run for **Windows**. While not all DBs will run under every platform, the YCSB tool itself must be able to execute on all of these systems and hopefully be able to communicate with remote data stores. + +Additionally, YCSB is targeting Java 7 (1.7.0) as its build version as some users are glacially slow moving to Java 8. So please avoid those Lambdas and Streams for now. + +## Pull Requests + +You've written some amazing code and are excited to share it with the community! It's time to open a PR! Here's what you should do. + +* Checkout YCSB's ``master`` branch in your own fork and create a new branch based off of it with a name that is reflective of your work. E.g. ``i123`` for fixing an issue or ``db_xyz`` when working on a binding. +* Add your changes to the branch. +* Commit the code and start the commit message with the component you are working on in square braces. E.g. ``[core] Add another format for exporting histograms.`` or ``[hbase12] Fix interrupted exception bug.``. +* Push to your fork and click the ``Create Pull Request`` button. +* Wait for the build to complete in the CI pipeline. If it fails with a red X, click through the logs for details and fix any issues and commit your changes. +* If you have made changes, please flatten the commits so that the commit logs are nice and clean. Just run a ``git rebase -i ``. + +After you have opened your PR, a YCSB maintainer will review it and offer constructive feedback via the GitHub review feature. If no one has responded to your PR, please bump the thread by adding comments. + +**NOTE**: For maintainers, please get another maintainer to sign off on your changes before merging a PR. And if you're writing code, please do create a PR from your fork, don't just push code directly to the master branch. + +## Core, Bindings and Workloads + +The main components of the code base include the core library and benchmarking utility, various database client bindings and workload classes and definitions. + +### Core +When working on the core classes, keep in mind the following: + +* Do not change the core behavior or operation of the main benchmarking classes (Particularly the Client and Workload classes). YCSB is used all over the place because it's a consistent standard that allows different users to compare results with the same workloads. If you find a way to drastically improve throughput, that's great! But please check with the rest of the maintainers to see if we can add the tweaks without invalidating years of benchmarks. +* Do not remove or modify measurements. Users may have tooling to parse the outputs so if you take something out, they'll be a wee bit unhappy. Extending or adding measurements is fine (so if you do have tooling, expect additions.) +* Do not modify existing generators. Again we don't want to invalidate years of benchmarks. Instead, create a new generator or option that can be enabled explicitly (not implicitly!) for users to try out. +* Utility classes and methods are welcome. But if they're only ever used by a specific database binding, co-locate the code with that binding. +* Don't change the DB interface if at all possible. Implementations can squeeze all kinds of workloads through the existing interface and while it may be easy to change the bindings included with the source code, some users may have private clients they can't share with the community. + +### Bindings and Clients + +When a new database is released a *binding* can be created that implements a client communicating with the given data store that will execute YCSB workloads. Details about writing a DB binding can be found on our [GitHub Wiki page](https://github.com/brianfrankcooper/YCSB/wiki/Adding-a-Database). Some development guidelines to follow include: + +* Create a new Maven module for your binding. Follow the existing bindings as examples. +* The module *must* include a README.md file with details such as: + * Database setup with links to documentation so that the YCSB benchmarks will execute properly. + * Example command line executions (workload selection, etc). + * Required and optional properties (e.g. connection strings, behavior settings, etc) along with the default values. + * Versions of the database the binding supports. +* Javadoc the binding and all of the methods. Tell us what it does and how it works. + +Because YCSB is a utility to compare multiple data stores, we need each binding to behave similarly by default. That means each data store should enforce the strictest consistency guarantees available and avoid client side buffering or optimizations. This allows users to evaluate different DBs with a common baseline and tough standards. + +However you *should* include parameters to tune and improve performance as much as possible to reach those flashy marketing numbers. Just be honest and document what the settings do and what trade-offs are made. (e.g. client side buffering reduces I/O but a crash can lead to data loss). + +### Workloads + +YCSB began comparing various key/value data stores with simple CRUD operations. However as DBs have become more specialized we've added more workloads for various tasks and would love to have more in the future. Keep the following in mind: + +* Make sure more than one publicly available database can handle your workload. It's no fun if only one player is in the game. +* Use the existing DB interface to pass your data around. If you really need another API, discuss with the maintainers to see if there isn't a workaround. +* Provide real-world use cases for the workload, not just theoretical idealizations. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..cd1f104 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,13 @@ +========================================================================= +NOTICE file for use with, and corresponding to Section 4 of, +the Apache License, Version 2.0, +in this case for the YCSB project. +========================================================================= + + This product includes software developed by + Yahoo! Inc. (www.yahoo.com) + Copyright (c) 2010 Yahoo! Inc. All rights reserved. + + This product includes software developed by + Google Inc. (www.google.com) + Copyright (c) 2015 Google Inc. All rights reserved. diff --git a/README.md b/README.md index 8b13789..d554d2a 100644 --- a/README.md +++ b/README.md @@ -1 +1,157 @@ + + +About +==================================== + +This is a modification of the Yahoo! Cloud Serving Benchmark (YCSB) that brings a new workload and a set of new workload-related features for the following five different database bindings + +1. MongoDB +2. Scylla +3. JDBC +4. DynamoDB +5. Couchbase3 + +### Workload + +The new workload is built around a custom document structure that also comprises nested documents. + +``` +{ + "airline":{ + "name": str, pre-generated 50 unique values, + "alias": str, align with name + }, + "src_airport": str, pre-generated 500 unique values, + "dst_airport": str, pre-generated 500 unique values, + "codeshares": str array, length 0 to 3, from airline aliases + "stops": int, from 0 to 3 + "airplane": str, pre-generated 10 unique values, + "field1": str, random length from 0 to 1000 +} +``` + +The workload can be enabled by setting `workload=site.ycsb.workloads.airport.AirportWorkload`. It makes use of four different types of operations: + +* insert a single document +* insert bulks of documents +* delete a document by primary key +* query any document by `src_airport` (equality), `dst_airport` (equality), and `stops` (lower or equal) +* update one document by `airline.alias` + +### Binding + +In order to support this workload, the implemented bindings support two new functions compared to default YCSB: `findOne` and `updateOne`. Further, all bindings have been updated to support bulk inserts and delete by primary key. + +While YCSB only uses stringified content, the new workload requires the use of more diverse data types. Therefore the property`typedfields=true` needs to be set. Also, the bindings have been updated to support and distinguish between different data types. What is more, bindings for document-oriented DBMS are built such that they handle nested documents. Other bindings, in particular for JDBC and ScyllaDB are designed such that they use a flattened data model. Where possible, the updated bindings also support the definition of indexes. The full feature list is shown in the following table. + +| | **typed content** | **support for indexes** | **nested elements** | +| ---------- | ----------------- | ----------------------- | ------------------- | +| mongodb | yes | full | yes | +| jdbc | yes | full | no | +| couchbase3 | yes | full | yes | +| dynamodb | yes | single column | yes | +| scylla | yes | single column | no | + +The workload class can be configured to either use nested elements or use a flat data model. + +``` +nesteddata = false | true +``` + +Other changes this branch makes compared to original YCSB: + +* Full support for `long` (64 bits) ids so that more data can be added to the databases in the LOAD phase +* A new Id generator that does not use a window of in-transit ids and does not crash when large amounts of data are inserted in the database. It can be enabled with `transactioninsertkeygenerator=simple` + +In order to support queries all bindings now come with basic support for indexes. These can be set with a binding-specific flag: + +```json +couchbase.indexlist=[ ] +dynamodb.indexlist=[ ] +jdbc.indexlist=[ ] +mongodb.indexlist=[ ] +scylla.indexlist=[ ] +``` + +Example indexes are available in the `workloads/airport ` directory. + +YCSB +==================================== + +[![Build Status](https://travis-ci.org/brianfrankcooper/YCSB.png?branch=master)](https://travis-ci.org/brianfrankcooper/YCSB) + + + +Links +----- +* To get here, use https://ycsb.site +* [Our project docs](https://github.com/brianfrankcooper/YCSB/wiki) +* [The original announcement from Yahoo!](https://labs.yahoo.com/news/yahoo-cloud-serving-benchmark/) + +Getting Started +--------------- + +1. Download the [latest release of YCSB](https://github.com/brianfrankcooper/YCSB/releases/latest): + + ```sh + curl -O --location https://github.com/brianfrankcooper/YCSB/releases/download/0.17.0/ycsb-0.17.0.tar.gz + tar xfvz ycsb-0.17.0.tar.gz + cd ycsb-0.17.0 + ``` + +2. Set up a database to benchmark. There is a README file under each binding + directory. + +3. Run YCSB command. + + On Linux: + ```sh + bin/ycsb.sh load basic -P workloads/workloada + bin/ycsb.sh run basic -P workloads/workloada + ``` + + On Windows: + ```bat + bin/ycsb.bat load basic -P workloads\workloada + bin/ycsb.bat run basic -P workloads\workloada + ``` + + Running the `ycsb` command without any argument will print the usage. + + See https://github.com/brianfrankcooper/YCSB/wiki/Running-a-Workload + for a detailed documentation on how to run a workload. + + See https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties for + the list of available workload properties. + + +Building from source +-------------------- + +YCSB requires the use of Maven 3; if you use Maven 2, you may see [errors +such as these](https://github.com/brianfrankcooper/YCSB/issues/406). + +To build the full distribution, with all database bindings: + + mvn clean package + +To build a single database binding: + + mvn -pl site.ycsb:mongodb-binding -am clean package diff --git a/bin/bindings.properties b/bin/bindings.properties new file mode 100755 index 0000000..cf1066b --- /dev/null +++ b/bin/bindings.properties @@ -0,0 +1,77 @@ +# +# Copyright (c) 2012 - 2020 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) +# +accumulo1.9:site.ycsb.db.accumulo.AccumuloClient +aerospike:site.ycsb.db.AerospikeClient +asynchbase:site.ycsb.db.AsyncHBaseClient +arangodb:site.ycsb.db.arangodb.ArangoDBClient +arangodb3:site.ycsb.db.arangodb.ArangoDBClient +azurecosmos:site.ycsb.db.AzureCosmosClient +azuretablestorage:site.ycsb.db.azuretablestorage.AzureClient +basic:site.ycsb.BasicDB +basicts:site.ycsb.BasicTSDB +cassandra-cql:site.ycsb.db.CassandraCQLClient +cassandra2-cql:site.ycsb.db.CassandraCQLClient +cloudspanner:site.ycsb.db.cloudspanner.CloudSpannerClient +couchbase:site.ycsb.db.CouchbaseClient +couchbase2:site.ycsb.db.couchbase2.Couchbase2Client +couchbase3:site.ycsb.db.couchbase3.Couchbase3Client +crail:site.ycsb.db.crail.CrailClient +dynamodb:site.ycsb.db.DynamoDBClient +elasticsearch:site.ycsb.db.ElasticsearchClient +elasticsearch5:site.ycsb.db.elasticsearch5.ElasticsearchClient +elasticsearch5-rest:site.ycsb.db.elasticsearch5.ElasticsearchRestClient +foundationdb:site.ycsb.db.foundationdb.FoundationDBClient +geode:site.ycsb.db.GeodeClient +googlebigtable:site.ycsb.db.GoogleBigtableClient +googledatastore:site.ycsb.db.GoogleDatastoreClient +hbase1:site.ycsb.db.hbase1.HBaseClient1 +hbase2:site.ycsb.db.hbase2.HBaseClient2 +ignite:site.ycsb.db.ignite.IgniteClient +ignite-sql:site.ycsb.db.ignite.IgniteSqlClient +infinispan-cs:site.ycsb.db.InfinispanRemoteClient +infinispan:site.ycsb.db.InfinispanClient +jdbc:site.ycsb.db.JdbcDBClient +kudu:site.ycsb.db.KuduYCSBClient +memcached:site.ycsb.db.MemcachedClient +mongodb:site.ycsb.db.MongoDbClient +mongodb-async:site.ycsb.db.AsyncMongoDbClient +nosqldb:site.ycsb.db.NoSqlDbClient +orientdb:site.ycsb.db.OrientDBClient +postgrenosql:site.ycsb.postgrenosql.PostgreNoSQLDBClient +rados:site.ycsb.db.RadosClient +redis:site.ycsb.db.RedisClient +rest:site.ycsb.webservice.rest.RestClient +riak:site.ycsb.db.riak.RiakKVClient +rocksdb:site.ycsb.db.rocksdb.RocksDBClient +s3:site.ycsb.db.S3Client +scylla:site.ycsb.db.scylla.ScyllaCQLClient +solr7:site.ycsb.db.solr7.SolrClient +tarantool:site.ycsb.db.TarantoolClient +tablestore:site.ycsb.db.tablestore.TableStoreClient +voltdb:site.ycsb.db.voltdb.VoltClient4 +zookeeper:site.ycsb.db.zookeeper.ZKClient \ No newline at end of file diff --git a/bin/ycsb b/bin/ycsb new file mode 100755 index 0000000..0104672 --- /dev/null +++ b/bin/ycsb @@ -0,0 +1,326 @@ +#!/usr/bin/env python +# +# Copyright (c) 2012 - 2020 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" : "site.ycsb.CommandLine", + }, + "load" : { + "command" : "-load", + "description" : "Execute the load phase", + "main" : "site.ycsb.Client", + }, + "run" : { + "command" : "-t", + "description" : "Execute the transaction phase", + "main" : "site.ycsb.Client", + }, +} + +DATABASES = { + "accumulo1.9" : "site.ycsb.db.accumulo.AccumuloClient", + "aerospike" : "site.ycsb.db.AerospikeClient", + "arangodb" : "site.ycsb.db.arangodb.ArangoDBClient", + "arangodb3" : "site.ycsb.db.arangodb.ArangoDBClient", + "asynchbase" : "site.ycsb.db.AsyncHBaseClient", + "azurecosmos" : "site.ycsb.db.AzureCosmosClient", + "azuretablestorage" : "site.ycsb.db.azuretablestorage.AzureClient", + "basic" : "site.ycsb.BasicDB", + "basicts" : "site.ycsb.BasicTSDB", + "cassandra-cql": "site.ycsb.db.CassandraCQLClient", + "cassandra2-cql": "site.ycsb.db.CassandraCQLClient", + "cloudspanner" : "site.ycsb.db.cloudspanner.CloudSpannerClient", + "couchbase" : "site.ycsb.db.CouchbaseClient", + "couchbase2" : "site.ycsb.db.couchbase2.Couchbase2Client", + "couchbase3" : "site.ycsb.db.couchbase3.Couchbase3Client", + "crail" : "site.ycsb.db.crail.CrailClient", + "dynamodb" : "site.ycsb.db.DynamoDBClient", + "elasticsearch": "site.ycsb.db.ElasticsearchClient", + "elasticsearch5": "site.ycsb.db.elasticsearch5.ElasticsearchClient", + "elasticsearch5-rest": "site.ycsb.db.elasticsearch5.ElasticsearchRestClient", + "foundationdb" : "site.ycsb.db.foundationdb.FoundationDBClient", + "geode" : "site.ycsb.db.GeodeClient", + "googlebigtable" : "site.ycsb.db.GoogleBigtableClient", + "googledatastore" : "site.ycsb.db.GoogleDatastoreClient", + "griddb" : "site.ycsb.db.griddb.GridDBClient", + "hbase1" : "site.ycsb.db.hbase1.HBaseClient1", + "hbase2" : "site.ycsb.db.hbase2.HBaseClient2", + "ignite" : "site.ycsb.db.ignite.IgniteClient", + "ignite-sql" : "site.ycsb.db.ignite.IgniteSqlClient", + "infinispan-cs": "site.ycsb.db.InfinispanRemoteClient", + "infinispan" : "site.ycsb.db.InfinispanClient", + "jdbc" : "site.ycsb.db.JdbcDBClient", + "kudu" : "site.ycsb.db.KuduYCSBClient", + "memcached" : "site.ycsb.db.MemcachedClient", + "maprdb" : "site.ycsb.db.mapr.MapRDBClient", + "maprjsondb" : "site.ycsb.db.mapr.MapRJSONDBClient", + "mongodb" : "site.ycsb.db.MongoDbClient", + "mongodb-async": "site.ycsb.db.AsyncMongoDbClient", + "nosqldb" : "site.ycsb.db.NoSqlDbClient", + "orientdb" : "site.ycsb.db.OrientDBClient", + "postgrenosql" : "site.ycsb.postgrenosql.PostgreNoSQLDBClient", + "rados" : "site.ycsb.db.RadosClient", + "redis" : "site.ycsb.db.RedisClient", + "rest" : "site.ycsb.webservice.rest.RestClient", + "riak" : "site.ycsb.db.riak.RiakKVClient", + "rocksdb" : "site.ycsb.db.rocksdb.RocksDBClient", + "s3" : "site.ycsb.db.S3Client", + "seaweedfs" : "site.ycsb.db.seaweed.SeaweedClient", + "scylla" : "site.ycsb.db.scylla.ScyllaCQLClient", + "solr7" : "site.ycsb.db.solr7.SolrClient", + "tarantool" : "site.ycsb.db.TarantoolClient", + "tablestore" : "site.ycsb.db.tablestore.TableStoreClient", + "zookeeper" : "site.ycsb.db.zookeeper.ZKClient" +} + +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 site.ycsb:" + module + " -am package -DskipTests " + "dependency:build-classpath -DincludeScope=compile -Dmdep.outputFilterFile=true'") + mvn_output = check_output(["mvn", "-pl", "site.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 binding == "hbase14": + warn("The 'hbase14' client has been deprecated. HBase 1.y users should " + "rely on the 'hbase1' client instead.") + binding = "hbase1" + + if binding == "arangodb3": + warn("The 'arangodb3' client has been deprecated. The binding 'arangodb' " + "now covers every ArangoDB version. This alias will be removed " + "in the next YCSB release.") + binding = "arangodb" + + 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" or binding == "basicts") else binding + project = "core" if (binding == "basic" or binding == "basicts") 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/bin/ycsb.bat b/bin/ycsb.bat new file mode 100755 index 0000000..7802659 --- /dev/null +++ b/bin/ycsb.bat @@ -0,0 +1,231 @@ +@REM +@REM Copyright (c) 2012 - 2016 YCSB contributors. All rights reserved. +@REM +@REM Licensed under the Apache License, Version 2.0 (the "License"); you +@REM may not use this file except in compliance with the License. You +@REM may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, software +@REM distributed under the License is distributed on an "AS IS" BASIS, +@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +@REM implied. See the License for the specific language governing +@REM permissions and limitations under the License. See accompanying +@REM LICENSE file. +@REM +@REM ----------------------------------------------------------------------- +@REM Control Script for YCSB +@REM +@REM Environment Variable Prerequisites +@REM +@REM Do not set the variables in this script. Instead put them into a script +@REM setenv.sh in YCSB_HOME/bin to keep your customizations separate. +@REM +@REM YCSB_HOME (Optional) YCSB installation directory. If not set +@REM this script will use the parent directory of where this +@REM script is run from. +@REM +@REM JAVA_HOME (Required) Must point at your Java Development Kit +@REM or Java Runtime Environment installation. +@REM +@REM JAVA_OPTS (Optional) Java runtime options used when any command +@REM is executed. +@REM +@REM WARNING!!! YCSB home must be located in a directory path that doesn't +@REM contain spaces. +@REM + +@ECHO OFF +SETLOCAL ENABLEDELAYEDEXPANSION + +@REM Only set YCSB_HOME if not already set +PUSHD %~dp0.. +IF NOT DEFINED YCSB_HOME SET YCSB_HOME=%CD% +POPD + +@REM Ensure that any extra CLASSPATH variables are set via setenv.bat +SET CLASSPATH= + +@REM Pull in customization options +if exist "%YCSB_HOME%\bin\setenv.bat" call "%YCSB_HOME%\bin\setenv.bat" + +@REM Check if we have a usable JDK +IF "%JAVA_HOME%." == "." GOTO noJavaHome +IF NOT EXIST "%JAVA_HOME%\bin\java.exe" GOTO noJavaHome +GOTO okJava +:noJavaHome +ECHO The JAVA_HOME environment variable is not defined correctly. +GOTO exit +:okJava + +@REM Determine YCSB command argument +IF NOT "load" == "%1" GOTO noload +SET YCSB_COMMAND=-load +SET YCSB_CLASS=site.ycsb.Client +GOTO gotCommand +:noload +IF NOT "run" == "%1" GOTO noRun +SET YCSB_COMMAND=-t +SET YCSB_CLASS=site.ycsb.Client +GOTO gotCommand +:noRun +IF NOT "shell" == "%1" GOTO noShell +SET YCSB_COMMAND= +SET YCSB_CLASS=site.ycsb.CommandLine +GOTO gotCommand +:noShell +ECHO [ERROR] Found unknown command '%1' +ECHO [ERROR] Expected one of 'load', 'run', or 'shell'. Exiting. +GOTO exit +:gotCommand + +@REM Find binding information +FOR /F "delims=" %%G in ( + 'FINDSTR /B "%2:" %YCSB_HOME%\bin\bindings.properties' +) DO SET "BINDING_LINE=%%G" + +IF NOT "%BINDING_LINE%." == "." GOTO gotBindingLine +ECHO [ERROR] The specified binding '%2' was not found. Exiting. +GOTO exit +:gotBindingLine + +@REM Pull out binding name and class +FOR /F "tokens=1-2 delims=:" %%G IN ("%BINDING_LINE%") DO ( + SET BINDING_NAME=%%G + SET BINDING_CLASS=%%H +) + +@REM Some bindings have multiple versions that are managed in the same +@REM directory. +@REM They are noted with a '-' after the binding name. +@REM (e.g. cassandra-7 & cassandra-8) +FOR /F "tokens=1 delims=-" %%G IN ("%BINDING_NAME%") DO ( + SET BINDING_DIR=%%G +) + +@REM The 'basic' binding is core functionality +IF NOT "%BINDING_NAME%" == "basic" GOTO noBasic +SET BINDING_DIR=core +:noBasic + +@REM Add Top level conf to classpath +IF "%CLASSPATH%." == "." GOTO emptyClasspath +SET CLASSPATH=%CLASSPATH%;%YCSB_HOME%\conf +GOTO confAdded +:emptyClasspath +SET CLASSPATH=%YCSB_HOME%\conf +:confAdded + +@REM Cassandra2 deprecation message +IF NOT "%BINDING_DIR%" == "cassandra2" GOTO notAliasCassandra +echo [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. +SET BINDING_DIR=cassandra +:notAliasCassandra + +@REM hbase14 replaced with hbase1 +IF NOT "%BINDING_DIR%" == "hbase14" GOTO notAliasHBase14 +echo [WARN] The 'hbase14' client has been deprecated. HBase 1.y users should rely on the 'hbase1' client instead. +SET BINDING_DIR=hbase1 +:notAliasHBase14 + +@REM arangodb3 deprecation message +IF NOT "%BINDING_DIR%" == "arangodb3" GOTO notAliasArangodb3 +echo [WARN] The 'arangodb3' client has been deprecated. The binding 'arangodb' now covers every ArangoDB version. This alias will be removed in the next YCSB release. +SET BINDING_DIR=arangodb +:notAliasArangodb3 + +@REM Build classpath according to source checkout or release distribution +IF EXIST "%YCSB_HOME%\pom.xml" GOTO gotSource +@REM Build classpath according to source checkout or release distribution +IF EXIST "%YCSB_HOME%\pom.xml" GOTO gotSource + +@REM Core libraries +FOR %%F IN (%YCSB_HOME%\lib\*.jar) DO ( + SET CLASSPATH=!CLASSPATH!;%%F% +) + +@REM Database conf dir +IF NOT EXIST "%YCSB_HOME%\%BINDING_DIR%-binding\conf" GOTO noBindingConf +set CLASSPATH=%CLASSPATH%;%YCSB_HOME%\%BINDING_DIR%-binding\conf +:noBindingConf + +@REM Database libraries +FOR %%F IN (%YCSB_HOME%\%BINDING_DIR%-binding\lib\*.jar) DO ( + SET CLASSPATH=!CLASSPATH!;%%F% +) +GOTO classpathComplete + +:gotSource +@REM Check for some basic libraries to see if the source has been built. +IF EXIST "%YCSB_HOME%\core\target\dependency\*.jar" ( + IF EXIST "%YCSB_HOME%\%BINDING_DIR%\target\*.jar" ( + GOTO gotJars + ) +) + +@REM Call mvn to build source checkout. +IF "%BINDING_NAME%" == "basic" GOTO buildCore +SET MVN_PROJECT=%BINDING_DIR%-binding +goto gotMvnProject +:buildCore +SET MVN_PROJECT=core +:gotMvnProject + +ECHO [WARN] YCSB libraries not found. Attempting to build... +CALL mvn -Psource-run -pl site.ycsb:%MVN_PROJECT% -am package -DskipTests +IF %ERRORLEVEL% NEQ 0 ( + ECHO [ERROR] Error trying to build project. Exiting. + GOTO exit +) + +:gotJars +@REM Core libraries +FOR %%F IN (%YCSB_HOME%\core\target\*.jar) DO ( + SET CLASSPATH=!CLASSPATH!;%%F% +) + +@REM Core dependency libraries +FOR %%F IN (%YCSB_HOME%\core\target\dependency\*.jar) DO ( + SET CLASSPATH=!CLASSPATH!;%%F% +) + +@REM Database conf (need to find because location is not consistent) +FOR /D /R %YCSB_HOME%\%BINDING_DIR% %%F IN (*) DO ( + IF "%%~nxF" == "conf" SET CLASSPATH=!CLASSPATH!;%%F% +) + +@REM Database libraries +FOR %%F IN (%YCSB_HOME%\%BINDING_DIR%\target\*.jar) DO ( + SET CLASSPATH=!CLASSPATH!;%%F% +) + +@REM Database dependency libraries +FOR %%F IN (%YCSB_HOME%\%BINDING_DIR%\target\dependency\*.jar) DO ( + SET CLASSPATH=!CLASSPATH!;%%F% +) + +:classpathComplete + +@REM Couchbase deprecation message +IF NOT "%BINDING_DIR%" == "couchbase" GOTO notOldCouchbase +echo [WARN] The 'couchbase' client is deprecated. If you are using Couchbase 4.0+ try using the 'couchbase2' client instead. +:notOldCouchbase + +@REM Get the rest of the arguments, skipping the first 2 +FOR /F "tokens=2*" %%G IN ("%*") DO ( + SET YCSB_ARGS=%%H +) + +@REM Run YCSB +@ECHO ON +"%JAVA_HOME%\bin\java.exe" %JAVA_OPTS% -classpath "%CLASSPATH%" %YCSB_CLASS% %YCSB_COMMAND% -db %BINDING_CLASS% %YCSB_ARGS% +@ECHO OFF + +GOTO end + +:exit +EXIT /B 1; + +:end + diff --git a/bin/ycsb.sh b/bin/ycsb.sh new file mode 100755 index 0000000..7a1165b --- /dev/null +++ b/bin/ycsb.sh @@ -0,0 +1,260 @@ +#!/bin/sh +# +# 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. +# +# ----------------------------------------------------------------------------- +# Control Script for YCSB +# +# Environment Variable Prerequisites +# +# Do not set the variables in this script. Instead put them into a script +# setenv.sh in YCSB_HOME/bin to keep your customizations separate. +# +# YCSB_HOME (Optional) YCSB installation directory. If not set +# this script will use the parent directory of where this +# script is run from. +# +# JAVA_HOME (Optional) Must point at your Java Development Kit +# installation. If empty, this script tries use the +# available java executable. +# +# JAVA_OPTS (Optional) Java runtime options used when any command +# is executed. +# +# WARNING!!! YCSB home must be located in a directory path that doesn't +# contain spaces. +# +# www.shellcheck.net was used to validate this script + +# Cygwin support +CYGWIN=false +case "$(uname)" in +CYGWIN*) CYGWIN=true;; +esac + +# Get script path +SCRIPT_DIR=$(dirname "$0" 2>/dev/null) + +# Only set YCSB_HOME if not already set +[ -z "$YCSB_HOME" ] && YCSB_HOME=$(cd "$SCRIPT_DIR/.." || exit; pwd) + +# Ensure that any extra CLASSPATH variables are set via setenv.sh +CLASSPATH= + +# Pull in customization options +if [ -r "$YCSB_HOME/bin/setenv.sh" ]; then + # Shellcheck wants a source, but this directive only runs if available + # So, tell shellcheck to ignore + # shellcheck source=/dev/null + . "$YCSB_HOME/bin/setenv.sh" +fi + +# Attempt to find the available JAVA, if JAVA_HOME not set +if [ -z "$JAVA_HOME" ]; then + JAVA_PATH=$(which java 2>/dev/null) + if [ "x$JAVA_PATH" != "x" ]; then + JAVA_HOME=$(dirname "$(dirname "$JAVA_PATH" 2>/dev/null)") + fi +fi + +# If JAVA_HOME still not set, error +if [ -z "$JAVA_HOME" ]; then + echo "[ERROR] Java executable not found. Exiting." + exit 1; +fi + +# Determine YCSB command argument +if [ "load" = "$1" ] ; then + YCSB_COMMAND=-load + YCSB_CLASS=site.ycsb.Client +elif [ "run" = "$1" ] ; then + YCSB_COMMAND=-t + YCSB_CLASS=site.ycsb.Client +elif [ "shell" = "$1" ] ; then + YCSB_COMMAND= + YCSB_CLASS=site.ycsb.CommandLine +else + echo "[ERROR] Found unknown command '$1'" + echo "[ERROR] Expected one of 'load', 'run', or 'shell'. Exiting." + exit 1; +fi + +# Find binding information +BINDING_LINE=$(grep "^$2:" "$YCSB_HOME/bin/bindings.properties" -m 1) + +if [ -z "$BINDING_LINE" ] ; then + echo "[ERROR] The specified binding '$2' was not found. Exiting." + exit 1; +fi + +# Get binding name and class +BINDING_NAME=$(echo "$BINDING_LINE" | cut -d':' -f1) +BINDING_CLASS=$(echo "$BINDING_LINE" | cut -d':' -f2) + +# Some bindings have multiple versions that are managed in the same directory. +# They are noted with a '-' after the binding name. +# (e.g. cassandra-7 & cassandra-8) +BINDING_DIR=$(echo "$BINDING_NAME" | cut -d'-' -f1) + +# The 'basic' binding is core functionality +if [ "$BINDING_NAME" = "basic" ] ; then + BINDING_DIR=core +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $CYGWIN; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# Check if source checkout, or release distribution +DISTRIBUTION=true +if [ -r "$YCSB_HOME/pom.xml" ]; then + DISTRIBUTION=false; +fi + +# Add Top level conf to classpath +if [ -z "$CLASSPATH" ] ; then + CLASSPATH="$YCSB_HOME/conf" +else + CLASSPATH="$CLASSPATH:$YCSB_HOME/conf" +fi + +# Cassandra2 deprecation message +if [ "${BINDING_DIR}" = "cassandra2" ] ; then + echo "[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_DIR="cassandra" +fi + +# hbase14 replaced by hbas1 +if [ "${BINDING_DIR}" = "hbase14" ] ; then + echo "[WARN] The 'hbase14' client has been deprecated. HBase 1.y users should \ +rely on the 'hbase1' client instead." + BINDING_DIR="hbase1" +fi + +# arangodb3 deprecation message +if [ "${BINDING_DIR}" = "arangodb3" ] ; then + echo "[WARN] The 'arangodb3' client has been deprecated. The binding 'arangodb' \ +now covers every ArangoDB version. This alias will be removed \ +in the next YCSB release." + BINDING_DIR="arangodb" +fi + +# Build classpath +# The "if" check after the "for" is because glob may just return the pattern +# when no files are found. The "if" makes sure the file is really there. +if $DISTRIBUTION; then + # Core libraries + for f in "$YCSB_HOME"/lib/*.jar ; do + if [ -r "$f" ] ; then + CLASSPATH="$CLASSPATH:$f" + fi + done + + # Database conf dir + if [ -r "$YCSB_HOME"/"$BINDING_DIR"-binding/conf ] ; then + CLASSPATH="$CLASSPATH:$YCSB_HOME/$BINDING_DIR-binding/conf" + fi + + # Database libraries + for f in "$YCSB_HOME"/"$BINDING_DIR"-binding/lib/*.jar ; do + if [ -r "$f" ] ; then + CLASSPATH="$CLASSPATH:$f" + fi + done + +# Source checkout +else + # Check for some basic libraries to see if the source has been built. + if ! ls "$YCSB_HOME"/core/target/*.jar 1> /dev/null 2>&1 || \ + ! ls "$YCSB_HOME"/"$BINDING_DIR"/target/*.jar 1>/dev/null 2>&1; then + # Call mvn to build source checkout. + if [ "$BINDING_NAME" = "basic" ] ; then + MVN_PROJECT=core + else + MVN_PROJECT="$BINDING_DIR"-binding + fi + + echo "[WARN] YCSB libraries not found. Attempting to build..." + if mvn -Psource-run -pl site.ycsb:"$MVN_PROJECT" -am package -DskipTests; then + echo "[ERROR] Error trying to build project. Exiting." + exit 1; + fi + fi + + # Core libraries + for f in "$YCSB_HOME"/core/target/*.jar ; do + if [ -r "$f" ] ; then + CLASSPATH="$CLASSPATH:$f" + fi + done + + # Core dependency libraries + for f in "$YCSB_HOME"/core/target/dependency/*.jar ; do + if [ -r "$f" ] ; then + CLASSPATH="$CLASSPATH:$f" + fi + done + + # Database conf (need to find because location is not consistent) + CLASSPATH_CONF=$(find "$YCSB_HOME"/$BINDING_DIR -name "conf" | while IFS="" read -r file; do echo ":$file"; done) + if [ "x$CLASSPATH_CONF" != "x" ]; then + CLASSPATH="$CLASSPATH$CLASSPATH_CONF" + fi + + # Database libraries + for f in "$YCSB_HOME"/"$BINDING_DIR"/target/*.jar ; do + if [ -r "$f" ] ; then + CLASSPATH="$CLASSPATH:$f" + fi + done + + # Database dependency libraries + for f in "$YCSB_HOME"/"$BINDING_DIR"/target/dependency/*.jar ; do + if [ -r "$f" ] ; then + CLASSPATH="$CLASSPATH:$f" + fi + done +fi + +# Couchbase deprecation message +if [ "${BINDING_DIR}" = "couchbase" ] ; then + echo "[WARN] The 'couchbase' client is deprecated. If you are using \ +Couchbase 4.0+ try using the 'couchbase2' client instead." +fi + +# For Cygwin, switch paths to Windows format before running java +if $CYGWIN; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") +fi + +# Get the rest of the arguments +YCSB_ARGS=$(echo "$@" | cut -d' ' -f3-) + +# About to run YCSB +echo "$JAVA_HOME/bin/java $JAVA_OPTS -classpath $CLASSPATH $YCSB_CLASS $YCSB_COMMAND -db $BINDING_CLASS $YCSB_ARGS" + +# Run YCSB +# Shellcheck reports the following line as needing double quotes to prevent +# globbing and word splitting. However, word splitting is the desired effect +# here. So, the shellcheck error is disabled for this line. +# shellcheck disable=SC2086 +"$JAVA_HOME/bin/java" $JAVA_OPTS -classpath "$CLASSPATH" $YCSB_CLASS $YCSB_COMMAND -db $BINDING_CLASS $YCSB_ARGS + diff --git a/binding-parent/datastore-specific-descriptor/pom.xml b/binding-parent/datastore-specific-descriptor/pom.xml new file mode 100644 index 0000000..afcb1fc --- /dev/null +++ b/binding-parent/datastore-specific-descriptor/pom.xml @@ -0,0 +1,44 @@ + + + + 4.0.0 + + + site.ycsb + root + 0.18.0-SNAPSHOT + ../../ + + + datastore-specific-descriptor + Per Datastore Binding descriptor + jar + + + This module contains the assembly descriptor used by the individual components + to build binding-specific distributions. + + + + site.ycsb + core + ${project.version} + + + + diff --git a/binding-parent/datastore-specific-descriptor/src/main/resources/assemblies/datastore-specific-assembly.xml b/binding-parent/datastore-specific-descriptor/src/main/resources/assemblies/datastore-specific-assembly.xml new file mode 100644 index 0000000..05273f8 --- /dev/null +++ b/binding-parent/datastore-specific-descriptor/src/main/resources/assemblies/datastore-specific-assembly.xml @@ -0,0 +1,85 @@ + + + + dist + true + ycsb-${artifactId}-${version} + + + README.md + + + + + + .. + + 0644 + + LICENSE.txt + NOTICE.txt + + + + ../bin + bin + 0755 + + ycsb* + + + + ../bin + bin + 0644 + + bindings.properties + + + + ../workloads + workloads + 0644 + + + src/main/conf + conf + 0644 + + + + + lib + + site.ycsb:core + + provided + true + + + lib + + *:jar:* + + + *:sources + + + + diff --git a/binding-parent/pom.xml b/binding-parent/pom.xml new file mode 100644 index 0000000..77cc2a4 --- /dev/null +++ b/binding-parent/pom.xml @@ -0,0 +1,205 @@ + + + + 4.0.0 + + + site.ycsb + root + 0.18.0-SNAPSHOT + + + binding-parent + YCSB Datastore Binding Parent + pom + + + This module acts as the parent for new datastore bindings. + It creates a datastore specific binary artifact. + + + + datastore-specific-descriptor + + + + + false + false + false + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven.assembly.version} + + + site.ycsb + datastore-specific-descriptor + ${project.version} + + + + + datastore-specific-assembly + + ycsb-${project.artifactId}-${project.version} + + tar.gz + + false + posix + + + + package + + single + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + validate + + ../checkstyle.xml + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven.dependency.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + stage-dependencies + package + + copy-dependencies + + + runtime + + + + + + + + + + datastore-binding + + + README.md + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + + tests-on-jdk9 + + 9 + + + ${skipJDK9Tests} + + + + + tests-on-jdk10 + + 10 + + + ${skipJDK10Tests} + + + + + tests-on-jdk11 + + 11 + + + ${skipJDK11Tests} + + + + + ycsb-release + + + true + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + + but-still-deploy-the-binding-parent + + deploy + + deploy + false + + false + + + + + + + + + + diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..de1166c --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/CHANGES.md b/core/CHANGES.md new file mode 100644 index 0000000..8b651bc --- /dev/null +++ b/core/CHANGES.md @@ -0,0 +1,84 @@ + + +When used as a latency under load benchmark YCSB in it's original form suffers from +Coordinated Omission[1] and related measurement issue: + +* Load is controlled by response time +* Measurement does not account for missing time +* Measurement starts at beginning of request rather than at intended beginning +* Measurement is limited in scope as the histogram does not provide data on overflow values + +To provide a minimal correction patch the following were implemented: + +1. Replace internal histogram implementation with HdrHistogram[2]: +HdrHistogram offers a dynamic range of measurement at a given precision and will +improve the fidelity of reporting. It allows capturing a much wider range of latencies. +HdrHistogram also supports compressed loss-less serialization which enable capturing +snapshot histograms from which lower resolution histograms can be constructed for plotting +latency over time. Snapshot interval histograms are serialized on status reporting which +must be enabled using the '-s' option. + +2. Track intended operation start and report latencies from that point in time: +Assuming the benchmark sets a target schedule of execution in which every operation +is supposed to happen at a given time the benchmark should measure the latency between +intended start time and operation completion. +This required the introduction of a new measurement point and inevitably +includes measuring some of the internal preparation steps of the load generator. +These overhead should be negligible in the context of a network hop, but could +be corrected for by estimating the load-generator overheads (e.g. by measuring a +no-op DB or by measuring the setup time for an operation and deducting that from total). +This intended measurement point is only used when there is a target load (specified by +the -target paramaeter) + +This branch supports the following new options: + +* -p measurementtype=[histogram|hdrhistogram|hdrhistogram+histogram|timeseries] (default=histogram) +The new measurement types are hdrhistogram and hdrhistogram+histogram. Default is still +histogram, which is the old histogram. Ultimately we would remove the old measurement types +and use only HdrHistogram but the old measurement is left in there for comparison sake. + +* -p measurement.interval=[op|intended|both] (default=op) +This new option deferentiates between measured intervals and adds the intended interval(as described) +above, and the option to record both the op and intended for comparison. + +* -p hdrhistogram.fileoutput=[true|false] (default=false) +This new option will enable periodical writes of the interval histogram into an output file. The path can be set using '-p hdrhistogram.output.path='. + +Example parameters: +-target 1000 -s -p workload=site.ycsb.workloads.CoreWorkload -p basicdb.verbose=false -p basicdb.simulatedelay=4 -p measurement.interval=both -p measurementtype=hdrhistogram -p hdrhistogram.fileoutput=true -p maxexecutiontime=60 + +Further changes made: + +* -p status.interval= (default=10) +Controls the number of seconds between status reports and therefore between HdrHistogram snapshots reported. + +* -p basicdb.randomizedelay=[true|false] (default=true) +Controls weather the delay simulated by the mock DB is uniformly random or not. + +Further suggestions: + +1. Correction load control: currently after a pause the load generator will do +operations back to back to catchup, this leads to a flat out throughput mode +of testing as opposed to controlled load. + +2. Move to async model: Scenarios where Ops have no dependency could delegate the +Op execution to a threadpool and thus separate the request rate control from the +synchronous execution of Ops. Measurement would start on queuing for execution. + +1. https://groups.google.com/forum/#!msg/mechanical-sympathy/icNZJejUHfE/BfDekfBEs_sJ +2. https://github.com/HdrHistogram/HdrHistogram \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000..7013b0c --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,99 @@ + + + + + 4.0.0 + + site.ycsb + root + 0.18.0-SNAPSHOT + + + core + Core YCSB + jar + + + 1.9.4 + + + + + org.apache.htrace + htrace-core4 + 4.1.0-incubating + + + org.codehaus.jackson + jackson-mapper-asl + ${jackson.api.version} + + + org.codehaus.jackson + jackson-core-asl + ${jackson.api.version} + + + org.testng + testng + 6.1.1 + test + + + org.hdrhistogram + HdrHistogram + 2.1.4 + + + + + + + src/main/resources + true + + + + + + + + source-run + + + + org.apache.maven.plugins + maven-dependency-plugin + + + stage-dependencies + package + + copy-dependencies + + + runtime + + + + + + + + + diff --git a/core/src/main/java/site/ycsb/BasicDB.java b/core/src/main/java/site/ycsb/BasicDB.java new file mode 100644 index 0000000..1b66a3f --- /dev/null +++ b/core/src/main/java/site/ycsb/BasicDB.java @@ -0,0 +1,484 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 YCSB contributors All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; +import java.util.Vector; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +/** + * Basic DB that just prints out the requested operations, instead of doing them against a database. + */ +public class BasicDB extends DB implements IndexableDB { + public static final String COUNT = "basicdb.count"; + public static final String COUNT_DEFAULT = "false"; + + public static final String VERBOSE = "basicdb.verbose"; + public static final String VERBOSE_DEFAULT = "true"; + + public static final String SIMULATE_DELAY = "basicdb.simulatedelay"; + public static final String SIMULATE_DELAY_DEFAULT = "0"; + + public static final String RANDOMIZE_DELAY = "basicdb.randomizedelay"; + public static final String RANDOMIZE_DELAY_DEFAULT = "true"; + + protected static final Object MUTEX = new Object(); + protected static int counter = 0; + protected static Map reads; + protected static Map scans; + protected static Map updates; + protected static Map inserts; + protected static Map deletes; + protected static Map finds; + + protected boolean verbose; + protected boolean randomizedelay; + protected int todelay; + protected boolean count; + + public BasicDB() { + todelay = 0; + } + + protected void delay() { + if (todelay > 0) { + long delayNs; + if (randomizedelay) { + delayNs = TimeUnit.MILLISECONDS.toNanos(ThreadLocalRandom.current().nextInt(todelay)); + if (delayNs == 0) { + return; + } + } else { + delayNs = TimeUnit.MILLISECONDS.toNanos(todelay); + } + + final long deadline = System.nanoTime() + delayNs; + do { + LockSupport.parkNanos(deadline - System.nanoTime()); + } while (System.nanoTime() < deadline && !Thread.interrupted()); + } + } + + /** + * Initialize any state for this DB. + * Called once per DB instance; there is one DB instance per client thread. + */ + public void init() { + verbose = Boolean.parseBoolean(getProperties().getProperty(VERBOSE, VERBOSE_DEFAULT)); + todelay = Integer.parseInt(getProperties().getProperty(SIMULATE_DELAY, SIMULATE_DELAY_DEFAULT)); + randomizedelay = Boolean.parseBoolean(getProperties().getProperty(RANDOMIZE_DELAY, RANDOMIZE_DELAY_DEFAULT)); + count = Boolean.parseBoolean(getProperties().getProperty(COUNT, COUNT_DEFAULT)); + if (verbose) { + synchronized (System.out) { + System.out.println("***************** properties *****************"); + Properties p = getProperties(); + if (p != null) { + for (Enumeration e = p.propertyNames(); e.hasMoreElements();) { + String k = (String) e.nextElement(); + System.out.println("\"" + k + "\"=\"" + p.getProperty(k) + "\""); + } + } + System.out.println("**********************************************"); + } + } + + synchronized (MUTEX) { + if (counter == 0 && count) { + reads = new HashMap(); + scans = new HashMap(); + updates = new HashMap(); + inserts = new HashMap(); + deletes = new HashMap(); + finds = new HashMap(); + } + counter++; + } + } + + protected static final ThreadLocal TL_STRING_BUILDER = new ThreadLocal() { + @Override + protected StringBuilder initialValue() { + return new StringBuilder(); + } + }; + + protected static StringBuilder getStringBuilder() { + StringBuilder sb = TL_STRING_BUILDER.get(); + sb.setLength(0); + return sb; + } + + /** + * Read a record from the database. Each field/value pair from the result will be stored in a HashMap. + * + * @param table The name of the table + * @param key The record key of the record to read. + * @param fields The list of fields to read, or null for all of them + * @param result A HashMap of field/value pairs for the result + * @return Zero on success, a non-zero error code on error + */ + public Status read(String table, String key, Set fields, Map result) { + delay(); + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("READ ").append(table).append(" ").append(key).append(" [ "); + if (fields != null) { + for (String f : fields) { + sb.append(f).append(" "); + } + } else { + sb.append(""); + } + + sb.append("]"); + System.out.println(sb); + } + + if (count) { + incCounter(reads, hash(table, key, fields)); + } + + return Status.OK; + } + + /** + * Perform a range scan for a set of records in the database. Each field/value pair from the result will be stored + * in a HashMap. + * + * @param table The name of the table + * @param startkey The record key of the first record to read. + * @param recordcount The number of records to read + * @param fields The list of fields to read, or null for all of them + * @param result A Vector of HashMaps, where each HashMap is a set field/value pairs for one record + * @return Zero on success, a non-zero error code on error + */ + public Status scan(String table, String startkey, int recordcount, Set fields, + Vector> result) { + delay(); + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("SCAN ").append(table).append(" ").append(startkey).append(" ").append(recordcount).append(" [ "); + if (fields != null) { + for (String f : fields) { + sb.append(f).append(" "); + } + } else { + sb.append(""); + } + + sb.append("]"); + System.out.println(sb); + } + + if (count) { + incCounter(scans, hash(table, startkey, fields)); + } + + return Status.OK; + } + + /** + * Update a record in the database. Any field/value pairs in the specified values HashMap will be written into the + * record with the specified record key, overwriting any existing values with the same field name. + * + * @param table The name of the table + * @param key The record key of the record to write. + * @param values A HashMap of field/value pairs to update in the record + * @return Zero on success, a non-zero error code on error + */ + public Status update(String table, String key, Map values) { + delay(); + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("UPDATE ").append(table).append(" ").append(key).append(" [ "); + if (values != null) { + for (Map.Entry entry : values.entrySet()) { + sb.append(entry.getKey()).append("=").append(entry.getValue()).append(" "); + } + } + sb.append("]"); + System.out.println(sb); + } + + if (count) { + incCounter(updates, oldHash(table, key, values)); + } + + return Status.OK; + } + + /** + * Insert a record in the database. Any field/value pairs in the specified values HashMap will be written into the + * record with the specified record key. + * + * @param table The name of the table + * @param key The record key of the record to insert. + * @param values A HashMap of field/value pairs to insert in the record + * @return Zero on success, a non-zero error code on error + */ + public Status insert(String table, String key, List values) { + delay(); + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("INSERT ").append(table).append(" ").append(key).append(" [ "); + if (values != null) { + for (DatabaseField field : values) { + // for (Map.Entry entry : values.entrySet()) { + sb.append(field.getFieldname()).append("=").append(field.getContent().asIterator()).append(" "); + } + } + + sb.append("]"); + System.out.println(sb); + } + + if (count) { + incCounter(inserts, hashWithDatabaseField(table, key, values)); + } + + return Status.OK; + } + + + /** + * Delete a record from the database. + * + * @param table The name of the table + * @param key The record key of the record to delete. + * @return Zero on success, a non-zero error code on error + */ + public Status delete(String table, String key) { + delay(); + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("DELETE ").append(table).append(" ").append(key); + System.out.println(sb); + } + + if (count) { + incCounter(deletes, (table + key).hashCode()); + } + + return Status.OK; + } + + @Override + public void cleanup() { + synchronized (MUTEX) { + int countDown = --counter; + if (count && countDown < 1) { + // TODO - would be nice to call something like: + // Measurements.getMeasurements().oneOffMeasurement("READS", "Uniques", reads.size()); + System.out.println("[READS], Uniques, " + reads.size()); + System.out.println("[SCANS], Uniques, " + scans.size()); + System.out.println("[UPDATES], Uniques, " + updates.size()); + System.out.println("[INSERTS], Uniques, " + inserts.size()); + System.out.println("[DELETES], Uniques, " + deletes.size()); + } + } + } + + /** + * Increments the count on the hash in the map. + * @param map A non-null map to sync and use for incrementing. + * @param hash A hash code to increment. + */ + protected void incCounter(final Map map, final int hash) { + synchronized (map) { + Integer ctr = map.get(hash); + if (ctr == null) { + map.put(hash, 1); + } else { + map.put(hash, ctr + 1); + } + } + } + + /** + * Hashes the table, key and fields, sorting the fields first for a consistent + * hash. + * Note that this is expensive as we generate a copy of the fields and a string + * buffer to hash on. Hashing on the objects is problematic. + * @param table The user table. + * @param key The key read or scanned. + * @param fields The fields read or scanned. + * @return The hash code. + */ + protected int hash(final String table, final String key, final Set fields) { + if (fields == null) { + return (table + key).hashCode(); + } + StringBuilder buf = getStringBuilder().append(table).append(key); + List sorted = new ArrayList(fields); + Collections.sort(sorted); + for (final String field : sorted) { + buf.append(field); + } + return buf.toString().hashCode(); + } + + /** + * Hashes the table, key and fields, sorting the fields first for a consistent + * hash. + * Note that this is expensive as we generate a copy of the fields and a string + * buffer to hash on. Hashing on the objects is problematic. + * @param table The user table. + * @param key The key read or scanned. + * @param values The values to hash on. + * @return The hash code. + */ + protected int oldHash(final String table, final String key, final Map values) { + if (values == null) { + return (table + key).hashCode(); + } + final TreeMap sorted = + new TreeMap(values); + + StringBuilder buf = getStringBuilder().append(table).append(key); + for (final Entry entry : sorted.entrySet()) { + entry.getValue().reset(); + buf.append(entry.getKey()) + .append(entry.getValue().toString()); + } + return buf.toString().hashCode(); + } + + protected int hash(final String table, final String key, final List values) { + if (values == null) { + return (table + key).hashCode(); + } + List sorted = new ArrayList<>(values); + sorted.sort((el1, el2) -> { return el1.getFieldname().compareTo(el2.getFieldname()); }); + + StringBuilder buf = getStringBuilder().append(table).append(key); + for (final Comparison field : sorted) { + buf.append(field.getFieldname()) + .append(field.getContentAsString()); + } + return buf.toString().hashCode(); + } + + protected int hashWithDatabaseField(final String table, final String key, final List values) { + if (values == null) { + return (table + key).hashCode(); + } + List sorted = new ArrayList<>(values); + sorted.sort((el1, el2) -> { return el1.getFieldname().compareTo(el2.getFieldname()); }); + + StringBuilder buf = getStringBuilder().append(table).append(key); + for (final DatabaseField field : sorted) { + ByteIterator b = field.getContent().asIterator(); + b.reset(); + buf.append(field.getFieldname()) + .append(b.toString()); + } + return buf.toString().hashCode(); + } + + /** + * Find a record from the database. + * + * @param table The name of the table + * @param key The values for the filter + */ + @Override + public Status findOne(String table, List filters, + Set fields, Map result) { + delay(); + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("FINDONE ").append(table).append(" ").append(filters) + .append(" -> ").append(fields); + System.out.println(sb); + } + + if (count) { + incCounter(finds, hash(table, "", filters)); + } + + return Status.OK; + } + + @Override + public Status updateOne(String table, List filters, List fields) { + delay(); + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("UPDATEONE ").append(table).append(" ").append(filters) + .append(" -> ").append(fields); + System.out.println(sb); + } + + if (count) { + incCounter(finds, hash(table, "", filters)); + } + + return Status.OK; + } + /** + * Short test of BasicDB + */ + /* + public static void main(String[] args) { + BasicDB bdb = new BasicDB(); + + Properties p = new Properties(); + p.setProperty("Sky", "Blue"); + p.setProperty("Ocean", "Wet"); + + bdb.setProperties(p); + + bdb.init(); + + HashMap fields = new HashMap(); + fields.put("A", new StringByteIterator("X")); + fields.put("B", new StringByteIterator("Y")); + + bdb.read("table", "key", null, null); + bdb.insert("table", "key", fields); + + fields = new HashMap(); + fields.put("C", new StringByteIterator("Z")); + + bdb.update("table", "key", fields); + + bdb.delete("table", "key"); + } + */ +} diff --git a/core/src/main/java/site/ycsb/BasicTSDB.java b/core/src/main/java/site/ycsb/BasicTSDB.java new file mode 100644 index 0000000..b3f1016 --- /dev/null +++ b/core/src/main/java/site/ycsb/BasicTSDB.java @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2017 YCSB contributors All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +import site.ycsb.workloads.TimeSeriesWorkload; +import site.ycsb.wrappers.DatabaseField; + +/** + * Basic DB for printing out time series workloads and/or tracking the distribution + * of keys and fields. + */ +public class BasicTSDB extends BasicDB { + + /** Time series workload specific counters. */ + protected static Map timestamps; + protected static Map floats; + protected static Map integers; + + private String timestampKey; + private String valueKey; + private String tagPairDelimiter; + private String queryTimeSpanDelimiter; + private long lastTimestamp; + + @Override + public void init() { + super.init(); + + synchronized (MUTEX) { + if (timestamps == null) { + timestamps = new HashMap(); + floats = new HashMap(); + integers = new HashMap(); + } + } + + timestampKey = getProperties().getProperty( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY, + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT); + valueKey = getProperties().getProperty( + TimeSeriesWorkload.VALUE_KEY_PROPERTY, + TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT); + tagPairDelimiter = getProperties().getProperty( + TimeSeriesWorkload.PAIR_DELIMITER_PROPERTY, + TimeSeriesWorkload.PAIR_DELIMITER_PROPERTY_DEFAULT); + queryTimeSpanDelimiter = getProperties().getProperty( + TimeSeriesWorkload.QUERY_TIMESPAN_DELIMITER_PROPERTY, + TimeSeriesWorkload.QUERY_TIMESPAN_DELIMITER_PROPERTY_DEFAULT); + } + + public Status read(String table, String key, Set fields, Map result) { + delay(); + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("READ ").append(table).append(" ").append(key).append(" [ "); + if (fields != null) { + for (String f : fields) { + sb.append(f).append(" "); + } + } else { + sb.append(""); + } + + sb.append("]"); + System.out.println(sb); + } + + if (count) { + Set filtered = null; + if (fields != null) { + filtered = new HashSet(); + for (final String field : fields) { + if (field.startsWith(timestampKey)) { + String[] parts = field.split(tagPairDelimiter); + if (parts[1].contains(queryTimeSpanDelimiter)) { + parts = parts[1].split(queryTimeSpanDelimiter); + lastTimestamp = Long.parseLong(parts[0]); + } else { + lastTimestamp = Long.parseLong(parts[1]); + } + synchronized(timestamps) { + Integer ctr = timestamps.get(lastTimestamp); + if (ctr == null) { + timestamps.put(lastTimestamp, 1); + } else { + timestamps.put(lastTimestamp, ctr + 1); + } + } + } else { + filtered.add(field); + } + } + } + incCounter(reads, hash(table, key, filtered)); + } + return Status.OK; + } + + @Override + public Status update(String table, String key, Map values) { + delay(); + + boolean isFloat = false; + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("UPDATE ").append(table).append(" ").append(key).append(" [ "); + if (values != null) { + final TreeMap tree = new TreeMap(values); + for (Map.Entry entry : tree.entrySet()) { + if (entry.getKey().equals(timestampKey)) { + sb.append(entry.getKey()).append("=") + .append(Utils.bytesToLong(entry.getValue().toArray())).append(" "); + } else if (entry.getKey().equals(valueKey)) { + final NumericByteIterator it = (NumericByteIterator) entry.getValue(); + isFloat = it.isFloatingPoint(); + sb.append(entry.getKey()).append("=") + .append(isFloat ? it.getDouble() : it.getLong()).append(" "); + } else { + sb.append(entry.getKey()).append("=").append(entry.getValue()).append(" "); + } + } + } + sb.append("]"); + System.out.println(sb); + } + + if (count) { + if (!verbose) { + isFloat = ((NumericByteIterator) values.get(valueKey)).isFloatingPoint(); + } + int hash = hash(table, key, values); + incCounter(updates, hash); + synchronized(timestamps) { + Integer ctr = timestamps.get(lastTimestamp); + if (ctr == null) { + timestamps.put(lastTimestamp, 1); + } else { + timestamps.put(lastTimestamp, ctr + 1); + } + } + if (isFloat) { + incCounter(floats, hash); + } else { + incCounter(integers, hash); + } + } + + return Status.OK; + } + + @Override + public Status insert(String table, String key, List values_) { + Map values = DB.fieldListAsIteratorMap(values_); + delay(); + + boolean isFloat = false; + + if (verbose) { + StringBuilder sb = getStringBuilder(); + sb.append("INSERT ").append(table).append(" ").append(key).append(" [ "); + if (values != null) { + final TreeMap tree = new TreeMap(values); + for (Map.Entry entry : tree.entrySet()) { + if (entry.getKey().equals(timestampKey)) { + sb.append(entry.getKey()).append("=") + .append(Utils.bytesToLong(entry.getValue().toArray())).append(" "); + } else if (entry.getKey().equals(valueKey)) { + final NumericByteIterator it = (NumericByteIterator) entry.getValue(); + isFloat = it.isFloatingPoint(); + sb.append(entry.getKey()).append("=") + .append(isFloat ? it.getDouble() : it.getLong()).append(" "); + } else { + sb.append(entry.getKey()).append("=").append(entry.getValue()).append(" "); + } + } + } + sb.append("]"); + System.out.println(sb); + } + + if (count) { + if (!verbose) { + isFloat = ((NumericByteIterator) values.get(valueKey)).isFloatingPoint(); + } + int hash = hash(table, key, values); + incCounter(inserts, hash); + synchronized(timestamps) { + Integer ctr = timestamps.get(lastTimestamp); + if (ctr == null) { + timestamps.put(lastTimestamp, 1); + } else { + timestamps.put(lastTimestamp, ctr + 1); + } + } + if (isFloat) { + incCounter(floats, hash); + } else { + incCounter(integers, hash); + } + } + + return Status.OK; + } + + @Override + public void cleanup() { + super.cleanup(); + if (count && counter < 1) { + System.out.println("[TIMESTAMPS], Unique, " + timestamps.size()); + System.out.println("[FLOATS], Unique series, " + floats.size()); + System.out.println("[INTEGERS], Unique series, " + integers.size()); + + long minTs = Long.MAX_VALUE; + long maxTs = Long.MIN_VALUE; + for (final long ts : timestamps.keySet()) { + if (ts > maxTs) { + maxTs = ts; + } + if (ts < minTs) { + minTs = ts; + } + } + System.out.println("[TIMESTAMPS], Min, " + minTs); + System.out.println("[TIMESTAMPS], Max, " + maxTs); + } + } + + + protected int hash(final String table, final String key, final Map values) { + final TreeMap sorted = new TreeMap(); + for (final Entry entry : values.entrySet()) { + if (entry.getKey().equals(valueKey)) { + continue; + } else if (entry.getKey().equals(timestampKey)) { + lastTimestamp = ((NumericByteIterator) entry.getValue()).getLong(); + entry.getValue().reset(); + continue; + } + sorted.put(entry.getKey(), entry.getValue()); + } + // yeah it's ugly but gives us a unique hash without having to add hashers + // to all of the ByteIterators. + StringBuilder buf = new StringBuilder().append(table).append(key); + for (final Entry entry : sorted.entrySet()) { + entry.getValue().reset(); + buf.append(entry.getKey()) + .append(entry.getValue().toString()); + } + return buf.toString().hashCode(); + } + +} \ No newline at end of file diff --git a/core/src/main/java/site/ycsb/ByteArrayByteIterator.java b/core/src/main/java/site/ycsb/ByteArrayByteIterator.java new file mode 100644 index 0000000..b27c808 --- /dev/null +++ b/core/src/main/java/site/ycsb/ByteArrayByteIterator.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +/** + * A ByteIterator that iterates through a byte array. + */ +public class ByteArrayByteIterator extends ByteIterator { + private final int originalOffset; + private final byte[] str; + private int off; + private final int len; + + public ByteArrayByteIterator(byte[] s) { + this.str = s; + this.off = 0; + this.len = s.length; + originalOffset = 0; + } + + public ByteArrayByteIterator(byte[] s, int off, int len) { + this.str = s; + this.off = off; + this.len = off + len; + originalOffset = off; + } + + @Override + public boolean hasNext() { + return off < len; + } + + @Override + public byte nextByte() { + byte ret = str[off]; + off++; + return ret; + } + + @Override + public long bytesLeft() { + return len - off; + } + + @Override + public void reset() { + off = originalOffset; + } + + @Override + public byte[] toArray() { + int size = (int) bytesLeft(); + byte[] bytes = new byte[size]; + System.arraycopy(str, off, bytes, 0, size); + off = len; + return bytes; + } + +} diff --git a/core/src/main/java/site/ycsb/ByteIterator.java b/core/src/main/java/site/ycsb/ByteIterator.java new file mode 100644 index 0000000..768f381 --- /dev/null +++ b/core/src/main/java/site/ycsb/ByteIterator.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.Iterator; + +/** + * YCSB-specific buffer class. ByteIterators are designed to support + * efficient field generation, and to allow backend drivers that can stream + * fields (instead of materializing them in RAM) to do so. + *

+ * YCSB originially used String objects to represent field values. This led to + * two performance issues. + *

+ * First, it leads to unnecessary conversions between UTF-16 and UTF-8, both + * during field generation, and when passing data to byte-based backend + * drivers. + *

+ * Second, Java strings are represented internally using UTF-16, and are + * built by appending to a growable array type (StringBuilder or + * StringBuffer), then calling a toString() method. This leads to a 4x memory + * overhead as field values are being built, which prevented YCSB from + * driving large object stores. + *

+ * The StringByteIterator class contains a number of convenience methods for + * backend drivers that convert between Map<String,String> and + * Map<String,ByteBuffer>. + * + */ +public abstract class ByteIterator implements Iterator { + + @Override + public abstract boolean hasNext(); + + @Override + public Byte next() { + throw new UnsupportedOperationException(); + } + + public abstract byte nextByte(); + + /** @return byte offset immediately after the last valid byte */ + public int nextBuf(byte[] buf, int bufOff) { + int sz = bufOff; + while (sz < buf.length && hasNext()) { + buf[sz] = nextByte(); + sz++; + } + return sz; + } + + public abstract long bytesLeft(); + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + /** Resets the iterator so that it can be consumed again. Not all + * implementations support this call. + * @throws UnsupportedOperationException if the implementation hasn't implemented + * the method. + */ + public void reset() { + throw new UnsupportedOperationException(); + } + + /** Consumes remaining contents of this object, and returns them as a string. */ + public String toString() { + Charset cset = Charset.forName("UTF-8"); + CharBuffer cb = cset.decode(ByteBuffer.wrap(this.toArray())); + return cb.toString(); + } + + /** Consumes remaining contents of this object, and returns them as a byte array. */ + public byte[] toArray() { + long left = bytesLeft(); + if (left != (int) left) { + throw new ArrayIndexOutOfBoundsException("Too much data to fit in one array!"); + } + byte[] ret = new byte[(int) left]; + for (int i = 0; i < ret.length; i++) { + ret[i] = nextByte(); + } + return ret; + } + +} diff --git a/core/src/main/java/site/ycsb/Client.java b/core/src/main/java/site/ycsb/Client.java new file mode 100644 index 0000000..7240616 --- /dev/null +++ b/core/src/main/java/site/ycsb/Client.java @@ -0,0 +1,680 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 YCSB contributors All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import site.ycsb.measurements.Measurements; +import site.ycsb.measurements.exporter.MeasurementsExporter; +import site.ycsb.measurements.exporter.TextMeasurementsExporter; +import org.apache.htrace.core.HTraceConfiguration; +import org.apache.htrace.core.TraceScope; +import org.apache.htrace.core.Tracer; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Turn seconds remaining into more useful units. + * i.e. if there are hours or days worth of seconds, use them. + */ +final class RemainingFormatter { + private RemainingFormatter() { + // not used + } + + public static StringBuilder format(long seconds) { + StringBuilder time = new StringBuilder(); + long days = TimeUnit.SECONDS.toDays(seconds); + if (days > 0) { + time.append(days).append(days == 1 ? " day " : " days "); + seconds -= TimeUnit.DAYS.toSeconds(days); + } + long hours = TimeUnit.SECONDS.toHours(seconds); + if (hours > 0) { + time.append(hours).append(hours == 1 ? " hour " : " hours "); + seconds -= TimeUnit.HOURS.toSeconds(hours); + } + /* Only include minute granularity if we're < 1 day. */ + if (days < 1) { + long minutes = TimeUnit.SECONDS.toMinutes(seconds); + if (minutes > 0) { + time.append(minutes).append(minutes == 1 ? " minute " : " minutes "); + seconds -= TimeUnit.MINUTES.toSeconds(seconds); + } + } + /* Only bother to include seconds if we're < 1 minute */ + if (time.length() == 0) { + time.append(seconds).append(time.length() == 1 ? " second " : " seconds "); + } + return time; + } +} + +/** + * Main class for executing YCSB. + */ +public final class Client { + private Client() { + //not used + } + + public static final String DEFAULT_RECORD_COUNT = "0"; + + /** + * The target number of operations to perform. + */ + public static final String OPERATION_COUNT_PROPERTY = "operationcount"; + + /** + * The number of records to load into the database initially. + */ + public static final String RECORD_COUNT_PROPERTY = "recordcount"; + + /** + * The workload class to be loaded. + */ + public static final String WORKLOAD_PROPERTY = "workload"; + + /** + * The database class to be used. + */ + public static final String DB_PROPERTY = "db"; + + /** + * The exporter class to be used. The default is + * site.ycsb.measurements.exporter.TextMeasurementsExporter. + */ + public static final String EXPORTER_PROPERTY = "exporter"; + + /** + * If set to the path of a file, YCSB will write all output to this file + * instead of STDOUT. + */ + public static final String EXPORT_FILE_PROPERTY = "exportfile"; + + /** + * The number of YCSB client threads to run. + */ + public static final String THREAD_COUNT_PROPERTY = "threadcount"; + + /** + * Indicates how many inserts to do if less than recordcount. + * Useful for partitioning the load among multiple servers if the client is the bottleneck. + * Additionally workloads should support the "insertstart" property which tells them which record to start at. + */ + public static final String INSERT_COUNT_PROPERTY = "insertcount"; + + /** + * Target number of operations per second. + */ + public static final String TARGET_PROPERTY = "target"; + + /** + * The maximum amount of time (in seconds) for which the benchmark will be run. + */ + public static final String MAX_EXECUTION_TIME = "maxexecutiontime"; + + /** + * Whether or not this is the transaction phase (run) or not (load). + */ + public static final String DO_TRANSACTIONS_PROPERTY = "dotransactions"; + + /** + * Whether or not to show status during run. + */ + public static final String STATUS_PROPERTY = "status"; + + /** + * Use label for status (e.g. to label one experiment out of a whole batch). + */ + public static final String LABEL_PROPERTY = "label"; + + /** + * An optional thread used to track progress and measure JVM stats. + */ + private static StatusThread statusthread = null; + + // HTrace integration related constants. + + /** + * All keys for configuring the tracing system start with this prefix. + */ + private static final String HTRACE_KEY_PREFIX = "htrace."; + private static final String CLIENT_WORKLOAD_INIT_SPAN = "Client#workload_init"; + private static final String CLIENT_INIT_SPAN = "Client#init"; + private static final String CLIENT_WORKLOAD_SPAN = "Client#workload"; + private static final String CLIENT_CLEANUP_SPAN = "Client#cleanup"; + private static final String CLIENT_EXPORT_MEASUREMENTS_SPAN = "Client#export_measurements"; + + public static void usageMessage() { + System.out.println("Usage: java site.ycsb.Client [options]"); + System.out.println("Options:"); + System.out.println(" -threads n: execute using n threads (default: 1) - can also be specified as the \n" + + " \"threadcount\" property using -p"); + System.out.println(" -target n: attempt to do n operations per second (default: unlimited) - can also\n" + + " be specified as the \"target\" property using -p"); + System.out.println(" -load: run the loading phase of the workload"); + System.out.println(" -t: run the transactions phase of the workload (default)"); + System.out.println(" -db dbname: specify the name of the DB to use (default: site.ycsb.BasicDB) - \n" + + " can also be specified as the \"db\" property using -p"); + System.out.println(" -P propertyfile: load properties from the given file. Multiple files can"); + System.out.println(" be specified, and will be processed in the order specified"); + System.out.println(" -p name=value: specify a property to be passed to the DB and workloads;"); + System.out.println(" multiple properties can be specified, and override any"); + System.out.println(" values in the propertyfile"); + System.out.println(" -s: show status during run (default: no status)"); + System.out.println(" -l label: use label for status (e.g. to label one experiment out of a whole batch)"); + System.out.println(""); + System.out.println("Required properties:"); + System.out.println(" " + WORKLOAD_PROPERTY + ": the name of the workload class to use (e.g. " + + "site.ycsb.workloads.CoreWorkload)"); + System.out.println(""); + System.out.println("To run the transaction phase from multiple servers, start a separate client on each."); + System.out.println("To run the load phase from multiple servers, start a separate client on each; additionally,"); + System.out.println("use the \"insertcount\" and \"insertstart\" properties to divide up the records " + + "to be inserted"); + } + + public static boolean checkRequiredProperties(Properties props) { + if (props.getProperty(WORKLOAD_PROPERTY) == null) { + System.out.println("Missing property: " + WORKLOAD_PROPERTY); + return false; + } + + return true; + } + + + /** + * Exports the measurements to either sysout or a file using the exporter + * loaded from conf. + * + * @throws IOException Either failed to write to output stream or failed to close it. + */ + private static void exportMeasurements(Properties props, long opcount, long runtime) + throws IOException { + MeasurementsExporter exporter = null; + try { + // if no destination file is provided the results will be written to stdout + OutputStream out; + String exportFile = props.getProperty(EXPORT_FILE_PROPERTY); + if (exportFile == null) { + out = System.out; + } else { + out = new FileOutputStream(exportFile); + } + + // if no exporter is provided the default text one will be used + String exporterStr = props.getProperty(EXPORTER_PROPERTY, + "site.ycsb.measurements.exporter.TextMeasurementsExporter"); + try { + exporter = (MeasurementsExporter) Class.forName(exporterStr).getConstructor(OutputStream.class) + .newInstance(out); + } catch (Exception e) { + System.err.println("Could not find exporter " + exporterStr + + ", will use default text reporter."); + e.printStackTrace(); + exporter = new TextMeasurementsExporter(out); + } + + exporter.write("OVERALL", "RunTime(ms)", runtime); + double throughput = 1000.0 * (opcount) / (runtime); + exporter.write("OVERALL", "Throughput(ops/sec)", throughput); + + final Map gcs = Utils.getGCStatst(); + long totalGCCount = 0; + long totalGCTime = 0; + for (final Entry entry : gcs.entrySet()) { + exporter.write("TOTAL_GCS_" + entry.getKey(), "Count", entry.getValue()[0]); + exporter.write("TOTAL_GC_TIME_" + entry.getKey(), "Time(ms)", entry.getValue()[1]); + exporter.write("TOTAL_GC_TIME_%_" + entry.getKey(), "Time(%)", + ((double) entry.getValue()[1] / runtime) * (double) 100); + totalGCCount += entry.getValue()[0]; + totalGCTime += entry.getValue()[1]; + } + exporter.write("TOTAL_GCs", "Count", totalGCCount); + + exporter.write("TOTAL_GC_TIME", "Time(ms)", totalGCTime); + exporter.write("TOTAL_GC_TIME_%", "Time(%)", ((double) totalGCTime / runtime) * (double) 100); + if (statusthread != null && statusthread.trackJVMStats()) { + exporter.write("MAX_MEM_USED", "MBs", statusthread.getMaxUsedMem()); + exporter.write("MIN_MEM_USED", "MBs", statusthread.getMinUsedMem()); + exporter.write("MAX_THREADS", "Count", statusthread.getMaxThreads()); + exporter.write("MIN_THREADS", "Count", statusthread.getMinThreads()); + exporter.write("MAX_SYS_LOAD_AVG", "Load", statusthread.getMaxLoadAvg()); + exporter.write("MIN_SYS_LOAD_AVG", "Load", statusthread.getMinLoadAvg()); + } + + Measurements.getMeasurements().exportMeasurements(exporter); + } finally { + if (exporter != null) { + exporter.close(); + } + } + } + + @SuppressWarnings("unchecked") + public static void main(String[] args) { + Properties props = parseArguments(args); + + boolean status = Boolean.valueOf(props.getProperty(STATUS_PROPERTY, String.valueOf(false))); + String label = props.getProperty(LABEL_PROPERTY, ""); + + long maxExecutionTime = Integer.parseInt(props.getProperty(MAX_EXECUTION_TIME, "0")); + + //get number of threads, target and db + int threadcount = Integer.parseInt(props.getProperty(THREAD_COUNT_PROPERTY, "1")); + String dbname = props.getProperty(DB_PROPERTY, "site.ycsb.BasicDB"); + int target = Integer.parseInt(props.getProperty(TARGET_PROPERTY, "0")); + + //compute the target throughput + double targetperthreadperms = -1; + if (target > 0) { + double targetperthread = ((double) target) / ((double) threadcount); + targetperthreadperms = targetperthread / 1000.0; + } + + Thread warningthread = setupWarningThread(); + warningthread.start(); + + Measurements.setProperties(props); + + Workload workload = getWorkload(props); + + final Tracer tracer = getTracer(props, workload); + + initWorkload(props, warningthread, workload, tracer); + + System.err.println("Starting test."); + final CountDownLatch completeLatch = new CountDownLatch(threadcount); + + final List clients = initDb(dbname, props, threadcount, targetperthreadperms, + workload, tracer, completeLatch); + + if (status) { + boolean standardstatus = false; + if (props.getProperty(Measurements.MEASUREMENT_TYPE_PROPERTY, "").compareTo("timeseries") == 0) { + standardstatus = true; + } + int statusIntervalSeconds = Integer.parseInt(props.getProperty("status.interval", "10")); + boolean trackJVMStats = props.getProperty(Measurements.MEASUREMENT_TRACK_JVM_PROPERTY, + Measurements.MEASUREMENT_TRACK_JVM_PROPERTY_DEFAULT).equals("true"); + statusthread = new StatusThread(completeLatch, clients, label, standardstatus, statusIntervalSeconds, + trackJVMStats); + statusthread.start(); + } + + Thread terminator = null; + long st; + long en; + long opsDone; + + try (final TraceScope span = tracer.newScope(CLIENT_WORKLOAD_SPAN)) { + + final Map threads = new HashMap<>(threadcount); + for (ClientThread client : clients) { + threads.put(new Thread(tracer.wrap(client, "ClientThread")), client); + } + + st = System.currentTimeMillis(); + + for (Thread t : threads.keySet()) { + t.start(); + } + + if (maxExecutionTime > 0) { + terminator = new TerminatorThread(maxExecutionTime, threads.keySet(), workload); + terminator.start(); + } + + opsDone = 0; + + for (Map.Entry entry : threads.entrySet()) { + try { + entry.getKey().join(); + opsDone += entry.getValue().getOpsDone(); + } catch (InterruptedException ignored) { + // ignored + } + } + + en = System.currentTimeMillis(); + } + + try { + try (final TraceScope span = tracer.newScope(CLIENT_CLEANUP_SPAN)) { + + if (terminator != null && !terminator.isInterrupted()) { + terminator.interrupt(); + } + + if (status) { + // wake up status thread if it's asleep + statusthread.interrupt(); + // at this point we assume all the monitored threads are already gone as per above join loop. + try { + statusthread.join(); + } catch (InterruptedException ignored) { + // ignored + } + } + + workload.cleanup(); + } + } catch (WorkloadException e) { + e.printStackTrace(); + e.printStackTrace(System.out); + System.exit(0); + } + + try { + try (final TraceScope span = tracer.newScope(CLIENT_EXPORT_MEASUREMENTS_SPAN)) { + exportMeasurements(props, opsDone, en - st); + } + } catch (IOException e) { + System.err.println("Could not export measurements, error: " + e.getMessage()); + e.printStackTrace(); + System.exit(-1); + } + + System.exit(0); + } + + private static List initDb(String dbname, Properties props, int threadcount, + double targetperthreadperms, Workload workload, Tracer tracer, + CountDownLatch completeLatch) { + boolean initFailed = false; + boolean dotransactions = Boolean.valueOf(props.getProperty(DO_TRANSACTIONS_PROPERTY, String.valueOf(true))); + + final List clients = new ArrayList<>(threadcount); + try (final TraceScope span = tracer.newScope(CLIENT_INIT_SPAN)) { + long opcount; + if (dotransactions) { + opcount = Long.parseLong(props.getProperty(OPERATION_COUNT_PROPERTY, "0")); + } else { + if (props.containsKey(INSERT_COUNT_PROPERTY)) { + opcount = Long.parseLong(props.getProperty(INSERT_COUNT_PROPERTY, "0")); + } else { + opcount = Long.parseLong(props.getProperty(RECORD_COUNT_PROPERTY, DEFAULT_RECORD_COUNT)); + } + } + if (threadcount > opcount && opcount > 0){ + threadcount = (int) opcount; + System.out.println("Warning: the threadcount is bigger than recordcount, the threadcount will be recordcount!"); + } + for (int threadid = 0; threadid < threadcount; threadid++) { + DB db; + try { + db = DBFactory.newDB(dbname, props, tracer); + } catch (UnknownDBException e) { + System.out.println("Unknown DB " + dbname); + initFailed = true; + break; + } + + long threadopcount = opcount / threadcount; + + // ensure correct number of operations, in case opcount is not a multiple of threadcount + if (threadid < opcount % threadcount) { + ++threadopcount; + } + + ClientThread t = new ClientThread(db, dotransactions, workload, props, threadopcount, targetperthreadperms, + completeLatch); + t.setThreadId(threadid); + t.setThreadCount(threadcount); + clients.add(t); + } + + if (initFailed) { + System.err.println("Error initializing datastore bindings."); + System.exit(0); + } + } + return clients; + } + + private static Tracer getTracer(Properties props, Workload workload) { + return new Tracer.Builder("YCSB " + workload.getClass().getSimpleName()) + .conf(getHTraceConfiguration(props)) + .build(); + } + + private static void initWorkload(Properties props, Thread warningthread, Workload workload, Tracer tracer) { + try { + try (final TraceScope span = tracer.newScope(CLIENT_WORKLOAD_INIT_SPAN)) { + workload.init(props); + warningthread.interrupt(); + } + } catch (WorkloadException e) { + e.printStackTrace(); + e.printStackTrace(System.out); + System.exit(0); + } + } + + private static HTraceConfiguration getHTraceConfiguration(Properties props) { + final Map filteredProperties = new HashMap<>(); + for (String key : props.stringPropertyNames()) { + if (key.startsWith(HTRACE_KEY_PREFIX)) { + filteredProperties.put(key.substring(HTRACE_KEY_PREFIX.length()), props.getProperty(key)); + } + } + return HTraceConfiguration.fromMap(filteredProperties); + } + + private static Thread setupWarningThread() { + //show a warning message that creating the workload is taking a while + //but only do so if it is taking longer than 2 seconds + //(showing the message right away if the setup wasn't taking very long was confusing people) + return new Thread() { + @Override + public void run() { + try { + sleep(2000); + } catch (InterruptedException e) { + return; + } + System.err.println(" (might take a few minutes for large data sets)"); + } + }; + } + + private static Workload getWorkload(Properties props) { + ClassLoader classLoader = Client.class.getClassLoader(); + + try { + Properties projectProp = new Properties(); + projectProp.load(classLoader.getResourceAsStream("project.properties")); + System.err.println("YCSB Client " + projectProp.getProperty("version")); + } catch (IOException e) { + System.err.println("Unable to retrieve client version."); + } + + System.err.println(); + System.err.println("Loading workload..."); + try { + Class workloadclass = classLoader.loadClass(props.getProperty(WORKLOAD_PROPERTY)); + + return (Workload) workloadclass.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + e.printStackTrace(System.out); + System.exit(0); + } + + return null; + } + + private static Properties parseArguments(String[] args) { + Properties props = new Properties(); + System.err.print("Command line:"); + for (String arg : args) { + System.err.print(" " + arg); + } + System.err.println(); + + Properties fileprops = new Properties(); + int argindex = 0; + + if (args.length == 0) { + usageMessage(); + System.out.println("At least one argument specifying a workload is required."); + System.exit(0); + } + + while (args[argindex].startsWith("-")) { + if (args[argindex].compareTo("-threads") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.out.println("Missing argument value for -threads."); + System.exit(0); + } + int tcount = Integer.parseInt(args[argindex]); + props.setProperty(THREAD_COUNT_PROPERTY, String.valueOf(tcount)); + argindex++; + } else if (args[argindex].compareTo("-target") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.out.println("Missing argument value for -target."); + System.exit(0); + } + int ttarget = Integer.parseInt(args[argindex]); + props.setProperty(TARGET_PROPERTY, String.valueOf(ttarget)); + argindex++; + } else if (args[argindex].compareTo("-load") == 0) { + props.setProperty(DO_TRANSACTIONS_PROPERTY, String.valueOf(false)); + argindex++; + } else if (args[argindex].compareTo("-t") == 0) { + props.setProperty(DO_TRANSACTIONS_PROPERTY, String.valueOf(true)); + argindex++; + } else if (args[argindex].compareTo("-s") == 0) { + props.setProperty(STATUS_PROPERTY, String.valueOf(true)); + argindex++; + } else if (args[argindex].compareTo("-db") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.out.println("Missing argument value for -db."); + System.exit(0); + } + props.setProperty(DB_PROPERTY, args[argindex]); + argindex++; + } else if (args[argindex].compareTo("-l") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.out.println("Missing argument value for -l."); + System.exit(0); + } + props.setProperty(LABEL_PROPERTY, args[argindex]); + argindex++; + } else if (args[argindex].compareTo("-P") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.out.println("Missing argument value for -P."); + System.exit(0); + } + String propfile = args[argindex]; + argindex++; + + Properties myfileprops = new Properties(); + try { + myfileprops.load(new FileInputStream(propfile)); + } catch (IOException e) { + System.out.println("Unable to open the properties file " + propfile); + System.out.println(e.getMessage()); + System.exit(0); + } + + //Issue #5 - remove call to stringPropertyNames to make compilable under Java 1.5 + for (Enumeration e = myfileprops.propertyNames(); e.hasMoreElements();) { + String prop = (String) e.nextElement(); + + fileprops.setProperty(prop, myfileprops.getProperty(prop)); + } + + } else if (args[argindex].compareTo("-p") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.out.println("Missing argument value for -p"); + System.exit(0); + } + int eq = args[argindex].indexOf('='); + if (eq < 0) { + usageMessage(); + System.out.println("Argument '-p' expected to be in key=value format (e.g., -p operationcount=99999)"); + System.exit(0); + } + + String name = args[argindex].substring(0, eq); + String value = args[argindex].substring(eq + 1); + props.put(name, value); + argindex++; + } else { + usageMessage(); + System.out.println("Unknown option " + args[argindex]); + System.exit(0); + } + + if (argindex >= args.length) { + break; + } + } + + if (argindex != args.length) { + usageMessage(); + if (argindex < args.length) { + System.out.println("An argument value without corresponding argument specifier (e.g., -p, -s) was found. " + + "We expected an argument specifier and instead found " + args[argindex]); + } else { + System.out.println("An argument specifier without corresponding value was found at the end of the supplied " + + "command line arguments."); + } + System.exit(0); + } + + //overwrite file properties with properties from the command line + + //Issue #5 - remove call to stringPropertyNames to make compilable under Java 1.5 + for (Enumeration e = props.propertyNames(); e.hasMoreElements();) { + String prop = (String) e.nextElement(); + + fileprops.setProperty(prop, props.getProperty(prop)); + } + + props = fileprops; + + if (!checkRequiredProperties(props)) { + System.out.println("Failed check required properties."); + System.exit(0); + } + + return props; + } +} diff --git a/core/src/main/java/site/ycsb/ClientThread.java b/core/src/main/java/site/ycsb/ClientThread.java new file mode 100644 index 0000000..50841a0 --- /dev/null +++ b/core/src/main/java/site/ycsb/ClientThread.java @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 YCSB contributors All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import site.ycsb.measurements.Measurements; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.locks.LockSupport; + +/** + * A thread for executing transactions or data inserts to the database. + */ +public class ClientThread implements Runnable { + // Counts down each of the clients completing. + private final CountDownLatch completeLatch; + + private static boolean spinSleep; + private DB db; + private boolean dotransactions; + private Workload workload; + private long opcount; + private double targetOpsPerMs; + + private long opsdone; + private int threadid; + private int threadcount; + private Object workloadstate; + private Properties props; + private long targetOpsTickNs; + private final Measurements measurements; + + /** + * Constructor. + * + * @param db the DB implementation to use + * @param dotransactions true to do transactions, false to insert data + * @param workload the workload to use + * @param props the properties defining the experiment + * @param opcount the number of operations (transactions or inserts) to do + * @param targetperthreadperms target number of operations per thread per ms + * @param completeLatch The latch tracking the completion of all clients. + */ + public ClientThread(DB db, boolean dotransactions, Workload workload, Properties props, long opcount, + double targetperthreadperms, CountDownLatch completeLatch) { + this.db = db; + this.dotransactions = dotransactions; + this.workload = workload; + this.opcount = opcount; + opsdone = 0; + if (targetperthreadperms > 0) { + targetOpsPerMs = targetperthreadperms; + targetOpsTickNs = (long) (1000000 / targetOpsPerMs); + } + this.props = props; + measurements = Measurements.getMeasurements(); + spinSleep = Boolean.valueOf(this.props.getProperty("spin.sleep", "false")); + this.completeLatch = completeLatch; + } + + public void setThreadId(final int threadId) { + threadid = threadId; + } + + public void setThreadCount(final int threadCount) { + threadcount = threadCount; + } + + public long getOpsDone() { + return opsdone; + } + + @Override + public void run() { + try { + db.init(); + } catch (DBException e) { + e.printStackTrace(); + e.printStackTrace(System.out); + return; + } + + try { + workloadstate = workload.initThread(props, threadid, threadcount); + } catch (WorkloadException e) { + e.printStackTrace(); + e.printStackTrace(System.out); + return; + } + + //NOTE: Switching to using nanoTime and parkNanos for time management here such that the measurements + // and the client thread have the same view on time. + + //spread the thread operations out so they don't all hit the DB at the same time + // GH issue 4 - throws exception if _target>1 because random.nextInt argument must be >0 + // and the sleep() doesn't make sense for granularities < 1 ms anyway + if ((targetOpsPerMs > 0) && (targetOpsPerMs <= 1.0)) { + long randomMinorDelay = ThreadLocalRandom.current().nextInt((int) targetOpsTickNs); + sleepUntil(System.nanoTime() + randomMinorDelay); + } + try { + if (dotransactions) { + long startTimeNanos = System.nanoTime(); + + while (((opcount == 0) || (opsdone < opcount)) && !workload.isStopRequested()) { + + if (!workload.doTransaction(db, workloadstate)) { + break; + } + + opsdone++; + + throttleNanos(startTimeNanos); + } + } else { + long startTimeNanos = System.nanoTime(); + + while (((opcount == 0) || (opsdone < opcount)) && !workload.isStopRequested()) { + + if (!workload.doInsert(db, workloadstate)) { + break; + } + + opsdone++; + + throttleNanos(startTimeNanos); + } + } + } catch (Exception e) { + e.printStackTrace(); + e.printStackTrace(System.out); + System.exit(0); + } + + try { + measurements.setIntendedStartTimeNs(0); + db.cleanup(); + } catch (DBException e) { + e.printStackTrace(); + e.printStackTrace(System.out); + } finally { + completeLatch.countDown(); + } + } + + private static void sleepUntil(long deadline) { + while (System.nanoTime() < deadline) { + if (!spinSleep) { + LockSupport.parkNanos(deadline - System.nanoTime()); + } + } + } + + private void throttleNanos(long startTimeNanos) { + //throttle the operations + if (targetOpsPerMs > 0) { + // delay until next tick + long deadline = startTimeNanos + opsdone * targetOpsTickNs; + sleepUntil(deadline); + measurements.setIntendedStartTimeNs(deadline); + } + } + + /** + * The total amount of work this thread is still expected to do. + */ + long getOpsTodo() { + long todo = opcount - opsdone; + return todo < 0 ? 0 : todo; + } +} diff --git a/core/src/main/java/site/ycsb/CommandLine.java b/core/src/main/java/site/ycsb/CommandLine.java new file mode 100644 index 0000000..a87cc4d --- /dev/null +++ b/core/src/main/java/site/ycsb/CommandLine.java @@ -0,0 +1,354 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import site.ycsb.workloads.CoreWorkload; +import site.ycsb.workloads.core.CoreConstants; +import site.ycsb.wrappers.ByteIteratorWrapper; +import site.ycsb.wrappers.DatabaseField; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.*; + +/** + * A simple command line client to a database, using the appropriate site.ycsb.DB implementation. + */ +public final class CommandLine { + private CommandLine() { + //not used + } + + public static final String DEFAULT_DB = "site.ycsb.BasicDB"; + + public static void usageMessage() { + System.out.println("YCSB Command Line Client"); + System.out.println("Usage: java site.ycsb.CommandLine [options]"); + System.out.println("Options:"); + System.out.println(" -P filename: Specify a property file"); + System.out.println(" -p name=value: Specify a property value"); + System.out.println(" -db classname: Use a specified DB class (can also set the \"db\" property)"); + System.out.println(" -table tablename: Use the table name instead of the default \"" + + CoreConstants.TABLENAME_PROPERTY_DEFAULT + "\""); + System.out.println(); + } + + public static void help() { + System.out.println("Commands:"); + System.out.println(" read key [field1 field2 ...] - Read a record"); + System.out.println(" scan key recordcount [field1 field2 ...] - Scan starting at key"); + System.out.println(" insert key name1=value1 [name2=value2 ...] - Insert a new record"); + System.out.println(" update key name1=value1 [name2=value2 ...] - Update a record"); + System.out.println(" delete key - Delete a record"); + System.out.println(" table [tablename] - Get or [set] the name of the table"); + System.out.println(" quit - Quit"); + } + + public static void main(String[] args) { + + Properties props = new Properties(); + Properties fileprops = new Properties(); + + parseArguments(args, props, fileprops); + + for (Enumeration e = props.propertyNames(); e.hasMoreElements();) { + String prop = (String) e.nextElement(); + + fileprops.setProperty(prop, props.getProperty(prop)); + } + + props = fileprops; + + System.out.println("YCSB Command Line client"); + System.out.println("Type \"help\" for command line help"); + System.out.println("Start with \"-help\" for usage info"); + + String table = props.getProperty(CoreConstants.TABLENAME_PROPERTY, CoreConstants.TABLENAME_PROPERTY_DEFAULT); + + //create a DB + String dbname = props.getProperty(Client.DB_PROPERTY, DEFAULT_DB); + + ClassLoader classLoader = CommandLine.class.getClassLoader(); + + DB db = null; + + try { + Class dbclass = classLoader.loadClass(dbname); + db = (DB) dbclass.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(0); + } + + db.setProperties(props); + try { + db.init(); + } catch (DBException e) { + e.printStackTrace(); + System.exit(0); + } + + System.out.println("Connected."); + + //main loop + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + + for (;;) { + //get user input + System.out.print("> "); + + String input = null; + + try { + input = br.readLine(); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + + if (input.compareTo("") == 0) { + continue; + } + + if (input.compareTo("help") == 0) { + help(); + continue; + } + + if (input.compareTo("quit") == 0) { + break; + } + + String[] tokens = input.split(" "); + + long st = System.currentTimeMillis(); + //handle commands + if (tokens[0].compareTo("table") == 0) { + handleTable(tokens, table); + } else if (tokens[0].compareTo("read") == 0) { + handleRead(tokens, table, db); + } else if (tokens[0].compareTo("scan") == 0) { + handleScan(tokens, table, db); + } else if (tokens[0].compareTo("update") == 0) { + handleUpdate(tokens, table, db); + } else if (tokens[0].compareTo("insert") == 0) { + handleInsert(tokens, table, db); + } else if (tokens[0].compareTo("delete") == 0) { + handleDelete(tokens, table, db); + } else { + System.out.println("Error: unknown command \"" + tokens[0] + "\""); + } + + System.out.println((System.currentTimeMillis() - st) + " ms"); + } + } + + private static void parseArguments(String[] args, Properties props, Properties fileprops) { + int argindex = 0; + while ((argindex < args.length) && (args[argindex].startsWith("-"))) { + if ((args[argindex].compareTo("-help") == 0) || + (args[argindex].compareTo("--help") == 0) || + (args[argindex].compareTo("-?") == 0) || + (args[argindex].compareTo("--?") == 0)) { + usageMessage(); + System.exit(0); + } + + if (args[argindex].compareTo("-db") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + props.setProperty(Client.DB_PROPERTY, args[argindex]); + argindex++; + } else if (args[argindex].compareTo("-P") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + String propfile = args[argindex]; + argindex++; + + Properties myfileprops = new Properties(); + try { + myfileprops.load(new FileInputStream(propfile)); + } catch (IOException e) { + System.out.println(e.getMessage()); + System.exit(0); + } + + for (Enumeration e = myfileprops.propertyNames(); e.hasMoreElements();) { + String prop = (String) e.nextElement(); + + fileprops.setProperty(prop, myfileprops.getProperty(prop)); + } + + } else if (args[argindex].compareTo("-p") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + int eq = args[argindex].indexOf('='); + if (eq < 0) { + usageMessage(); + System.exit(0); + } + + String name = args[argindex].substring(0, eq); + String value = args[argindex].substring(eq + 1); + props.put(name, value); + argindex++; + } else if (args[argindex].compareTo("-table") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + props.put(CoreConstants.TABLENAME_PROPERTY, args[argindex]); + + argindex++; + } else { + System.out.println("Unknown option " + args[argindex]); + usageMessage(); + System.exit(0); + } + + if (argindex >= args.length) { + break; + } + } + + if (argindex != args.length) { + usageMessage(); + System.exit(0); + } + } + + private static void handleDelete(String[] tokens, String table, DB db) { + if (tokens.length != 2) { + System.out.println("Error: syntax is \"delete keyname\""); + } else { + Status ret = db.delete(table, tokens[1]); + System.out.println("Return result: " + ret.getName()); + } + } + + private static void handleInsert(String[] tokens, String table, DB db) { + if (tokens.length < 3) { + System.out.println("Error: syntax is \"insert keyname name1=value1 [name2=value2 ...]\""); + } else { + List values = new ArrayList<>(); + for (int i = 2; i < tokens.length; i++) { + String[] nv = tokens[i].split("="); + values.add(new DatabaseField( + nv[0], ByteIteratorWrapper.create(new StringByteIterator(nv[1]))) + ); + } + + Status ret = db.insert(table, tokens[1], values); + System.out.println("Result: " + ret.getName()); + } + } + + private static void handleUpdate(String[] tokens, String table, DB db) { + if (tokens.length < 3) { + System.out.println("Error: syntax is \"update keyname name1=value1 [name2=value2 ...]\""); + } else { + HashMap values = new HashMap<>(); + + for (int i = 2; i < tokens.length; i++) { + String[] nv = tokens[i].split("="); + values.put(nv[0], new StringByteIterator(nv[1])); + } + + Status ret = db.update(table, tokens[1], values); + System.out.println("Result: " + ret.getName()); + } + } + + private static void handleScan(String[] tokens, String table, DB db) { + if (tokens.length < 3) { + System.out.println("Error: syntax is \"scan keyname scanlength [field1 field2 ...]\""); + } else { + Set fields = null; + + if (tokens.length > 3) { + fields = new HashSet<>(); + + fields.addAll(Arrays.asList(tokens).subList(3, tokens.length)); + } + + Vector> results = new Vector<>(); + Status ret = db.scan(table, tokens[1], Integer.parseInt(tokens[2]), fields, results); + System.out.println("Result: " + ret.getName()); + int record = 0; + if (results.isEmpty()) { + System.out.println("0 records"); + } else { + System.out.println("--------------------------------"); + } + for (Map result : results) { + System.out.println("Record " + (record++)); + for (Map.Entry ent : result.entrySet()) { + System.out.println(ent.getKey() + "=" + ent.getValue()); + } + System.out.println("--------------------------------"); + } + } + } + + private static void handleRead(String[] tokens, String table, DB db) { + if (tokens.length == 1) { + System.out.println("Error: syntax is \"read keyname [field1 field2 ...]\""); + } else { + Set fields = null; + + if (tokens.length > 2) { + fields = new HashSet<>(); + + fields.addAll(Arrays.asList(tokens).subList(2, tokens.length)); + } + + HashMap result = new HashMap<>(); + Status ret = db.read(table, tokens[1], fields, result); + System.out.println("Return code: " + ret.getName()); + for (Map.Entry ent : result.entrySet()) { + System.out.println(ent.getKey() + "=" + ent.getValue()); + } + } + } + + private static void handleTable(String[] tokens, String table) { + if (tokens.length == 1) { + System.out.println("Using table \"" + table + "\""); + } else if (tokens.length == 2) { + table = tokens[1]; + System.out.println("Using table \"" + table + "\""); + } else { + System.out.println("Error: syntax is \"table tablename\""); + } + } + + +} diff --git a/core/src/main/java/site/ycsb/DB.java b/core/src/main/java/site/ycsb/DB.java new file mode 100644 index 0000000..5b60578 --- /dev/null +++ b/core/src/main/java/site/ycsb/DB.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 YCSB contributors All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.Vector; + +import site.ycsb.wrappers.DatabaseField; + +/** + * A layer for accessing a database to be benchmarked. Each thread in the client + * will be given its own instance of whatever DB class is to be used in the test. + * This class should be constructed using a no-argument constructor, so we can + * load it dynamically. Any argument-based initialization should be + * done by init(). + * + * Note that YCSB does not make any use of the return codes returned by this class. + * Instead, it keeps a count of the return values and presents them to the user. + * + * The semantics of methods such as insert, update and delete vary from database + * to database. In particular, operations may or may not be durable once these + * methods commit, and some systems may return 'success' regardless of whether + * or not a tuple with a matching key existed before the call. Rather than dictate + * the exact semantics of these methods, we recommend you either implement them + * to match the database's default semantics, or the semantics of your + * target application. For the sake of comparison between experiments we also + * recommend you explain the semantics you chose when presenting performance results. + */ +public abstract class DB { + /** + * Properties for configuring this DB. + */ + private Properties properties = new Properties(); + + /** + * Set the properties for this DB. + */ + public void setProperties(Properties p) { + properties = p; + + } + + /** + * Get the set of properties for this DB. + */ + public Properties getProperties() { + return properties; + } + + /** + * Initialize any state for this DB. + * Called once per DB instance; there is one DB instance per client thread. + */ + public void init() throws DBException { + } + + /** + * Cleanup any state for this DB. + * Called once per DB instance; there is one DB instance per client thread. + */ + public void cleanup() throws DBException { + } + + /** + * Read a record from the database. Each field/value pair from the result will be stored in a HashMap. + * + * @param table The name of the table + * @param key The record key of the record to read. + * @param fields The list of fields to read, or null for all of them + * @param result A HashMap of field/value pairs for the result + * @return The result of the operation. + */ + public abstract Status read(String table, String key, Set fields, Map result); + + /** + * Perform a range scan for a set of records in the database. Each field/value pair from the result will be stored + * in a HashMap. + * + * @param table The name of the table + * @param startkey The record key of the first record to read. + * @param recordcount The number of records to read + * @param fields The list of fields to read, or null for all of them + * @param result A Vector of HashMaps, where each HashMap is a set field/value pairs for one record + * @return The result of the operation. + */ + public abstract Status scan(String table, String startkey, int recordcount, Set fields, + Vector> result); + + /** + * Update a record in the database. Any field/value pairs in the specified values HashMap will be written into the + * record with the specified record key, overwriting any existing values with the same field name. + * + * @param table The name of the table + * @param key The record key of the record to write. + * @param values A HashMap of field/value pairs to update in the record + * @return The result of the operation. + */ + public abstract Status update(String table, String key, Map values); + + /** + * Insert a record in the database. Any field/value pairs in the specified values HashMap will be written into the + * record with the specified record key. + * + * @param table The name of the table + * @param key The record key of the record to insert. + * @param values A HashMap of field/value pairs to insert in the record + * @return The result of the operation. + */ + // public abstract Status insert(String table, String key, Map values); + public abstract Status insert(String table, String key, List values); + + /** + * Delete a record from the database. + * + * @param table The name of the table + * @param key The record key of the record to delete. + * @return The result of the operation. + */ + public abstract Status delete(String table, String key); + + public static Map fieldListAsIteratorMap(List list) { + Map values = new HashMap<>(); + for(DatabaseField f : list) { + values.put(f.getFieldname(), f.getContent().asIterator()); + } + return values; + } +} diff --git a/core/src/main/java/site/ycsb/DBException.java b/core/src/main/java/site/ycsb/DBException.java new file mode 100644 index 0000000..09ce9ec --- /dev/null +++ b/core/src/main/java/site/ycsb/DBException.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +/** + * Something bad happened while interacting with the database. + */ +public class DBException extends Exception { + /** + * + */ + private static final long serialVersionUID = 6646883591588721475L; + + public DBException(String message) { + super(message); + } + + public DBException() { + super(); + } + + public DBException(String message, Throwable cause) { + super(message, cause); + } + + public DBException(Throwable cause) { + super(cause); + } + +} diff --git a/core/src/main/java/site/ycsb/DBFactory.java b/core/src/main/java/site/ycsb/DBFactory.java new file mode 100644 index 0000000..4358633 --- /dev/null +++ b/core/src/main/java/site/ycsb/DBFactory.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 YCSB contributors All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import org.apache.htrace.core.Tracer; + +import java.util.Properties; + +/** + * Creates a DB layer by dynamically classloading the specified DB class. + */ +public final class DBFactory { + private DBFactory() { + // not used + } + + public static DB newDB(String dbname, Properties properties, final Tracer tracer) throws UnknownDBException { + ClassLoader classLoader = DBFactory.class.getClassLoader(); + + DB ret; + + try { + Class dbclass = classLoader.loadClass(dbname); + + ret = (DB) dbclass.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + + ret.setProperties(properties); + + return DBWrapper.createWrapper(ret, tracer); + } + +} diff --git a/core/src/main/java/site/ycsb/DBWrapper.java b/core/src/main/java/site/ycsb/DBWrapper.java new file mode 100644 index 0000000..fca435a --- /dev/null +++ b/core/src/main/java/site/ycsb/DBWrapper.java @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2010 Yahoo! Inc., 2016-2020 YCSB contributors. All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import site.ycsb.measurements.Measurements; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +import org.apache.htrace.core.TraceScope; +import org.apache.htrace.core.Tracer; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Wrapper around a "real" DB that measures latencies and counts return codes. + * Also reports latency separately between OK and failed operations. + */ +public class DBWrapper extends DB { + protected final DB db; + protected final Measurements measurements; + protected final Tracer tracer; + + private boolean reportLatencyForEachError = false; + private Set latencyTrackedErrors = new HashSet(); + + private static final String REPORT_LATENCY_FOR_EACH_ERROR_PROPERTY = "reportlatencyforeacherror"; + private static final String REPORT_LATENCY_FOR_EACH_ERROR_PROPERTY_DEFAULT = "false"; + + private static final String LATENCY_TRACKED_ERRORS_PROPERTY = "latencytrackederrors"; + + private static final AtomicBoolean LOG_REPORT_CONFIG = new AtomicBoolean(false); + + private final String scopeStringCleanup; + private final String scopeStringDelete; + private final String scopeStringInit; + private final String scopeStringInsert; + private final String scopeStringRead; + private final String scopeStringScan; + private final String scopeStringUpdate; + + public static DBWrapper createWrapper(final DB db, final Tracer tracer) { + if(db instanceof IndexableDB) { + return new IndexableDbWrapper(db, tracer); + } + return new DBWrapper(db, tracer); + } + + protected DBWrapper(final DB db, final Tracer tracer) { + this.db = db; + measurements = Measurements.getMeasurements(); + this.tracer = tracer; + final String simple = db.getClass().getSimpleName(); + scopeStringCleanup = simple + "#cleanup"; + scopeStringDelete = simple + "#delete"; + scopeStringInit = simple + "#init"; + scopeStringInsert = simple + "#insert"; + scopeStringRead = simple + "#read"; + scopeStringScan = simple + "#scan"; + scopeStringUpdate = simple + "#update"; + } + + /** + * Set the properties for this DB. + */ + public void setProperties(Properties p) { + db.setProperties(p); + } + + /** + * Get the set of properties for this DB. + */ + public Properties getProperties() { + return db.getProperties(); + } + + /** + * Initialize any state for this DB. + * Called once per DB instance; there is one DB instance per client thread. + */ + public void init() throws DBException { + try (final TraceScope span = tracer.newScope(scopeStringInit)) { + db.init(); + + this.reportLatencyForEachError = Boolean.parseBoolean(getProperties(). + getProperty(REPORT_LATENCY_FOR_EACH_ERROR_PROPERTY, + REPORT_LATENCY_FOR_EACH_ERROR_PROPERTY_DEFAULT)); + + if (!reportLatencyForEachError) { + String latencyTrackedErrorsProperty = getProperties().getProperty(LATENCY_TRACKED_ERRORS_PROPERTY, null); + if (latencyTrackedErrorsProperty != null) { + this.latencyTrackedErrors = new HashSet(Arrays.asList( + latencyTrackedErrorsProperty.split(","))); + } + } + + if (LOG_REPORT_CONFIG.compareAndSet(false, true)) { + System.err.println("DBWrapper: report latency for each error is " + + this.reportLatencyForEachError + " and specific error codes to track" + + " for latency are: " + this.latencyTrackedErrors.toString()); + } + } + } + + /** + * Cleanup any state for this DB. + * Called once per DB instance; there is one DB instance per client thread. + */ + public void cleanup() throws DBException { + try (final TraceScope span = tracer.newScope(scopeStringCleanup)) { + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + db.cleanup(); + long en = System.nanoTime(); + measure("CLEANUP", Status.OK, ist, st, en); + } + } + + /** + * Read a record from the database. Each field/value pair from the result + * will be stored in a HashMap. + * + * @param table The name of the table + * @param key The record key of the record to read. + * @param fields The list of fields to read, or null for all of them + * @param result A HashMap of field/value pairs for the result + * @return The result of the operation. + */ + public Status read(String table, String key, Set fields, + Map result) { + try (final TraceScope span = tracer.newScope(scopeStringRead)) { + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + Status res = db.read(table, key, fields, result); + long en = System.nanoTime(); + measure("READ", res, ist, st, en); + measurements.reportStatus("READ", res); + return res; + } + } + + /** + * Perform a range scan for a set of records in the database. + * Each field/value pair from the result will be stored in a HashMap. + * + * @param table The name of the table + * @param startkey The record key of the first record to read. + * @param recordcount The number of records to read + * @param fields The list of fields to read, or null for all of them + * @param result A Vector of HashMaps, where each HashMap is a set field/value pairs for one record + * @return The result of the operation. + */ + public Status scan(String table, String startkey, int recordcount, + Set fields, Vector> result) { + try (final TraceScope span = tracer.newScope(scopeStringScan)) { + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + Status res = db.scan(table, startkey, recordcount, fields, result); + long en = System.nanoTime(); + measure("SCAN", res, ist, st, en); + measurements.reportStatus("SCAN", res); + return res; + } + } + + protected final void measure(String op, Status result, long intendedStartTimeNanos, + long startTimeNanos, long endTimeNanos) { + String measurementName = op; + if (result == null || !result.isOk()) { + if (this.reportLatencyForEachError || + this.latencyTrackedErrors.contains(result.getName())) { + measurementName = op + "-" + result.getName(); + } else { + measurementName = op + "-FAILED"; + } + } + measurements.measure(measurementName, + (int) ((endTimeNanos - startTimeNanos) / 1000)); + measurements.measureIntended(measurementName, + (int) ((endTimeNanos - intendedStartTimeNanos) / 1000)); + } + + /** + * Update a record in the database. Any field/value pairs in the specified values HashMap will be written into the + * record with the specified record key, overwriting any existing values with the same field name. + * + * @param table The name of the table + * @param key The record key of the record to write. + * @param values A HashMap of field/value pairs to update in the record + * @return The result of the operation. + */ + public Status update(String table, String key, + Map values) { + try (final TraceScope span = tracer.newScope(scopeStringUpdate)) { + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + Status res = db.update(table, key, values); + long en = System.nanoTime(); + measure("UPDATE", res, ist, st, en); + measurements.reportStatus("UPDATE", res); + return res; + } + } + + /** + * Insert a record in the database. Any field/value pairs in the specified + * values HashMap will be written into the record with the specified + * record key. + * + * @param table The name of the table + * @param key The record key of the record to insert. + * @param values A HashMap of field/value pairs to insert in the record + * @return The result of the operation. + */ + public Status insert(String table, String key, List values) { + try (final TraceScope span = tracer.newScope(scopeStringInsert)) { + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + Status res = db.insert(table, key, values); + long en = System.nanoTime(); + measure("INSERT", res, ist, st, en); + measurements.reportStatus("INSERT", res); + return res; + } + } + + /** + * Delete a record from the database. + * + * @param table The name of the table + * @param key The record key of the record to delete. + * @return The result of the operation. + */ + public Status delete(String table, String key) { + try (final TraceScope span = tracer.newScope(scopeStringDelete)) { + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + Status res = db.delete(table, key); + long en = System.nanoTime(); + measure("DELETE", res, ist, st, en); + measurements.reportStatus("DELETE", res); + return res; + } + } +} + +final class IndexableDbWrapper extends DBWrapper implements IndexableDB { + private final String scopeStringFindOne; + private final String scopeStringUpdateOne; + + IndexableDbWrapper(final DB db, final Tracer tracer) { + super(db, tracer); + final String simple = db.getClass().getSimpleName(); + scopeStringFindOne = simple + "#findone"; + scopeStringUpdateOne = simple + "#updateone"; + } + @Override + public Status findOne(String table, List filters, Set fields, Map result) { + try (final TraceScope span = tracer.newScope(scopeStringFindOne)) { + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + Status res = ((IndexableDB) db).findOne(table, filters, fields, result); + long en = System.nanoTime(); + measure("FINDONE", res, ist, st, en); + measurements.reportStatus("FINDONE", res); + return res; + } + } + @Override + public Status updateOne(String table, List filters, List fields) { + try (final TraceScope span = tracer.newScope(scopeStringUpdateOne)) { + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + Status res = ((IndexableDB) db).updateOne(table, filters, fields); + long en = System.nanoTime(); + measure("UPDATEONE", res, ist, st, en); + measurements.reportStatus("UPDATEONE", res); + return res; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/site/ycsb/GoodBadUglyDB.java b/core/src/main/java/site/ycsb/GoodBadUglyDB.java new file mode 100644 index 0000000..ba3f8fc --- /dev/null +++ b/core/src/main/java/site/ycsb/GoodBadUglyDB.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import site.ycsb.wrappers.DatabaseField; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; + +/** + * Basic DB that just prints out the requested operations, instead of doing them against a database. + */ +public class GoodBadUglyDB extends DB { + public static final String SIMULATE_DELAY = "gbudb.delays"; + public static final String SIMULATE_DELAY_DEFAULT = "200,1000,10000,50000,100000"; + private static final ReadWriteLock DB_ACCESS = new ReentrantReadWriteLock(); + private long[] delays; + + public GoodBadUglyDB() { + delays = new long[]{200, 1000, 10000, 50000, 200000}; + } + + private void delay() { + final Random random = ThreadLocalRandom.current(); + double p = random.nextDouble(); + int mod; + if (p < 0.9) { + mod = 0; + } else if (p < 0.99) { + mod = 1; + } else if (p < 0.9999) { + mod = 2; + } else { + mod = 3; + } + // this will make mod 3 pauses global + Lock lock = mod == 3 ? DB_ACCESS.writeLock() : DB_ACCESS.readLock(); + if (mod == 3) { + System.out.println("OUCH"); + } + lock.lock(); + try { + final long baseDelayNs = MICROSECONDS.toNanos(delays[mod]); + final int delayRangeNs = (int) (MICROSECONDS.toNanos(delays[mod + 1]) - baseDelayNs); + final long delayNs = baseDelayNs + random.nextInt(delayRangeNs); + final long deadline = System.nanoTime() + delayNs; + do { + LockSupport.parkNanos(deadline - System.nanoTime()); + } while (System.nanoTime() < deadline && !Thread.interrupted()); + } finally { + lock.unlock(); + } + + } + + /** + * Initialize any state for this DB. Called once per DB instance; there is one DB instance per client thread. + */ + public void init() { + int i = 0; + for (String delay : getProperties().getProperty(SIMULATE_DELAY, SIMULATE_DELAY_DEFAULT).split(",")) { + delays[i++] = Long.parseLong(delay); + } + } + + /** + * Read a record from the database. Each field/value pair from the result will be stored in a HashMap. + * + * @param table The name of the table + * @param key The record key of the record to read. + * @param fields The list of fields to read, or null for all of them + * @param result A HashMap of field/value pairs for the result + * @return Zero on success, a non-zero error code on error + */ + public Status read(String table, String key, Set fields, Map result) { + delay(); + return Status.OK; + } + + /** + * Perform a range scan for a set of records in the database. Each field/value pair from the result will be stored + * in a HashMap. + * + * @param table The name of the table + * @param startkey The record key of the first record to read. + * @param recordcount The number of records to read + * @param fields The list of fields to read, or null for all of them + * @param result A Vector of HashMaps, where each HashMap is a set field/value pairs for one record + * @return Zero on success, a non-zero error code on error + */ + public Status scan(String table, String startkey, int recordcount, Set fields, + Vector> result) { + delay(); + + return Status.OK; + } + + /** + * Update a record in the database. Any field/value pairs in the specified values HashMap will be written into the + * record with the specified record key, overwriting any existing values with the same field name. + * + * @param table The name of the table + * @param key The record key of the record to write. + * @param values A HashMap of field/value pairs to update in the record + * @return Zero on success, a non-zero error code on error + */ + public Status update(String table, String key, Map values) { + delay(); + + return Status.OK; + } + + /** + * Insert a record in the database. Any field/value pairs in the specified values HashMap will be written into the + * record with the specified record key. + * + * @param table The name of the table + * @param key The record key of the record to insert. + * @param values A HashMap of field/value pairs to insert in the record + * @return Zero on success, a non-zero error code on error + */ + public Status insert(String table, String key, List values) { + delay(); + return Status.OK; + } + + /** + * Delete a record from the database. + * + * @param table The name of the table + * @param key The record key of the record to delete. + * @return Zero on success, a non-zero error code on error + */ + public Status delete(String table, String key) { + delay(); + return Status.OK; + } +} diff --git a/core/src/main/java/site/ycsb/IndexableDB.java b/core/src/main/java/site/ycsb/IndexableDB.java new file mode 100644 index 0000000..f650103 --- /dev/null +++ b/core/src/main/java/site/ycsb/IndexableDB.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +public interface IndexableDB { + + public static final String TYPED_FIELDS_PROPERTY = "typedfields"; + public static final String TYPED_FIELDS_DEFAULT = "false"; + + public Status findOne(String table, List filters, + Set fields, Map result); + public Status updateOne(String table, List filters, List fields); +} diff --git a/core/src/main/java/site/ycsb/InputStreamByteIterator.java b/core/src/main/java/site/ycsb/InputStreamByteIterator.java new file mode 100644 index 0000000..e803798 --- /dev/null +++ b/core/src/main/java/site/ycsb/InputStreamByteIterator.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A ByteIterator that iterates through an inputstream of bytes. + */ +public class InputStreamByteIterator extends ByteIterator { + private final long len; + private final InputStream ins; + private long off; + private final boolean resetable; + + public InputStreamByteIterator(InputStream ins, long len) { + this.len = len; + this.ins = ins; + off = 0; + resetable = ins.markSupported(); + if (resetable) { + ins.mark((int) len); + } + } + + @Override + public boolean hasNext() { + return off < len; + } + + @Override + public byte nextByte() { + int ret; + try { + ret = ins.read(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + if (ret == -1) { + throw new IllegalStateException("Past EOF!"); + } + off++; + return (byte) ret; + } + + @Override + public long bytesLeft() { + return len - off; + } + + @Override + public byte[] toArray() { + int size = (int) bytesLeft(); + byte[] bytes = new byte[size]; + try { + if (ins.read(bytes) < size) { + throw new IllegalStateException("Past EOF!"); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + off = len; + return bytes; + } + + @Override + public void reset() { + if (resetable) { + try { + ins.reset(); + ins.mark((int) len); + off = 0; + } catch (IOException e) { + throw new IllegalStateException("Failed to reset the input stream", e); + } + } else { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/core/src/main/java/site/ycsb/NumericByteIterator.java b/core/src/main/java/site/ycsb/NumericByteIterator.java new file mode 100644 index 0000000..42e82b0 --- /dev/null +++ b/core/src/main/java/site/ycsb/NumericByteIterator.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2017 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 site.ycsb; + +/** + * A byte iterator that handles encoding and decoding numeric values. + * Currently this iterator can handle 64 bit signed values and double precision + * floating point values. + */ +public class NumericByteIterator extends ByteIterator { + private final byte[] payload; + private final boolean floatingPoint; + private int off; + + public NumericByteIterator(final long value) { + floatingPoint = false; + payload = Utils.longToBytes(value); + off = 0; + } + + public NumericByteIterator(final double value) { + floatingPoint = true; + payload = Utils.doubleToBytes(value); + off = 0; + } + + @Override + public boolean hasNext() { + return off < payload.length; + } + + @Override + public byte nextByte() { + return payload[off++]; + } + + @Override + public long bytesLeft() { + return payload.length - off; + } + + @Override + public void reset() { + off = 0; + } + + public long getLong() { + if (floatingPoint) { + throw new IllegalStateException("Byte iterator is of the type double"); + } + return Utils.bytesToLong(payload); + } + + public double getDouble() { + if (!floatingPoint) { + throw new IllegalStateException("Byte iterator is of the type long"); + } + return Utils.bytesToDouble(payload); + } + + public boolean isFloatingPoint() { + return floatingPoint; + } + +} \ No newline at end of file diff --git a/core/src/main/java/site/ycsb/RandomByteIterator.java b/core/src/main/java/site/ycsb/RandomByteIterator.java new file mode 100644 index 0000000..4851c57 --- /dev/null +++ b/core/src/main/java/site/ycsb/RandomByteIterator.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * A ByteIterator that generates a random sequence of bytes. + */ +public class RandomByteIterator extends ByteIterator { + private final long len; + private long off; + private int bufOff; + private final byte[] buf; + + @Override + public boolean hasNext() { + return (off + bufOff) < len; + } + + private void fillBytesImpl(byte[] buffer, int base) { + int bytes = ThreadLocalRandom.current().nextInt(); + + switch (buffer.length - base) { + default: + buffer[base + 5] = (byte) (((bytes >> 25) & 95) + ' '); + case 5: + buffer[base + 4] = (byte) (((bytes >> 20) & 63) + ' '); + case 4: + buffer[base + 3] = (byte) (((bytes >> 15) & 31) + ' '); + case 3: + buffer[base + 2] = (byte) (((bytes >> 10) & 95) + ' '); + case 2: + buffer[base + 1] = (byte) (((bytes >> 5) & 63) + ' '); + case 1: + buffer[base + 0] = (byte) (((bytes) & 31) + ' '); + case 0: + break; + } + } + + private void fillBytes() { + if (bufOff == buf.length) { + fillBytesImpl(buf, 0); + bufOff = 0; + off += buf.length; + } + } + + public RandomByteIterator(long len) { + this.len = len; + this.buf = new byte[6]; + this.bufOff = buf.length; + fillBytes(); + this.off = 0; + } + + public byte nextByte() { + fillBytes(); + bufOff++; + return buf[bufOff - 1]; + } + + @Override + public int nextBuf(byte[] buffer, int bufOffset) { + int ret; + if (len - off < buffer.length - bufOffset) { + ret = (int) (len - off); + } else { + ret = buffer.length - bufOffset; + } + int i; + for (i = 0; i < ret; i += 6) { + fillBytesImpl(buffer, i + bufOffset); + } + off += ret; + return ret + bufOffset; + } + + @Override + public long bytesLeft() { + return len - off - bufOff; + } + + @Override + public void reset() { + off = 0; + } + + /** Consumes remaining contents of this object, and returns them as a byte array. */ + public byte[] toArray() { + long left = bytesLeft(); + if (left != (int) left) { + throw new ArrayIndexOutOfBoundsException("Too much data to fit in one array!"); + } + byte[] ret = new byte[(int) left]; + int bufOffset = 0; + while (bufOffset < ret.length) { + bufOffset = nextBuf(ret, bufOffset); + } + return ret; + } + +} diff --git a/core/src/main/java/site/ycsb/Status.java b/core/src/main/java/site/ycsb/Status.java new file mode 100644 index 0000000..8a27cc9 --- /dev/null +++ b/core/src/main/java/site/ycsb/Status.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +/** + * The result of an operation. + */ +public class Status { + private final String name; + private final String description; + + /** + * @param name A short name for the status. + * @param description A description of the status. + */ + public Status(String name, String description) { + super(); + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return "Status [name=" + name + ", description=" + description + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Status other = (Status) obj; + if (description == null) { + if (other.description != null) { + return false; + } + } else if (!description.equals(other.description)) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + /** + * Is {@code this} a passing state for the operation: {@link Status#OK} or {@link Status#BATCHED_OK}. + * @return true if the operation is successful, false otherwise + */ + public boolean isOk() { + return this == OK || this == BATCHED_OK; + } + + public static final Status OK = new Status("OK", "The operation completed successfully."); + public static final Status ERROR = new Status("ERROR", "The operation failed."); + public static final Status NOT_FOUND = new Status("NOT_FOUND", "The requested record was not found."); + public static final Status NOT_IMPLEMENTED = new Status("NOT_IMPLEMENTED", "The operation is not " + + "implemented for the current binding."); + public static final Status UNEXPECTED_STATE = new Status("UNEXPECTED_STATE", "The operation reported" + + " success, but the result was not as expected."); + public static final Status BAD_REQUEST = new Status("BAD_REQUEST", "The request was not valid."); + public static final Status FORBIDDEN = new Status("FORBIDDEN", "The operation is forbidden."); + public static final Status SERVICE_UNAVAILABLE = new Status("SERVICE_UNAVAILABLE", "Dependant " + + "service for the current binding is not available."); + public static final Status BATCHED_OK = new Status("BATCHED_OK", "The operation has been batched by " + + "the binding to be executed later."); +} + diff --git a/core/src/main/java/site/ycsb/StatusThread.java b/core/src/main/java/site/ycsb/StatusThread.java new file mode 100644 index 0000000..a45caeb --- /dev/null +++ b/core/src/main/java/site/ycsb/StatusThread.java @@ -0,0 +1,308 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +import site.ycsb.measurements.Measurements; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * A thread to periodically show the status of the experiment to reassure you that progress is being made. + */ +public class StatusThread extends Thread { + // Counts down each of the clients completing + private final CountDownLatch completeLatch; + + // Stores the measurements for the run + private final Measurements measurements; + + // Whether or not to track the JVM stats per run + private final boolean trackJVMStats; + + // The clients that are running. + private final List clients; + + private final String label; + private final boolean standardstatus; + + // The interval for reporting status. + private long sleeptimeNs; + + // JVM max/mins + private int maxThreads; + private int minThreads = Integer.MAX_VALUE; + private long maxUsedMem; + private long minUsedMem = Long.MAX_VALUE; + private double maxLoadAvg; + private double minLoadAvg = Double.MAX_VALUE; + private long lastGCCount = 0; + private long lastGCTime = 0; + + /** + * Creates a new StatusThread without JVM stat tracking. + * + * @param completeLatch The latch that each client thread will {@link CountDownLatch#countDown()} + * as they complete. + * @param clients The clients to collect metrics from. + * @param label The label for the status. + * @param standardstatus If true the status is printed to stdout in addition to stderr. + * @param statusIntervalSeconds The number of seconds between status updates. + */ + public StatusThread(CountDownLatch completeLatch, List clients, + String label, boolean standardstatus, int statusIntervalSeconds) { + this(completeLatch, clients, label, standardstatus, statusIntervalSeconds, false); + } + + /** + * Creates a new StatusThread. + * + * @param completeLatch The latch that each client thread will {@link CountDownLatch#countDown()} + * as they complete. + * @param clients The clients to collect metrics from. + * @param label The label for the status. + * @param standardstatus If true the status is printed to stdout in addition to stderr. + * @param statusIntervalSeconds The number of seconds between status updates. + * @param trackJVMStats Whether or not to track JVM stats. + */ + public StatusThread(CountDownLatch completeLatch, List clients, + String label, boolean standardstatus, int statusIntervalSeconds, + boolean trackJVMStats) { + this.completeLatch = completeLatch; + this.clients = clients; + this.label = label; + this.standardstatus = standardstatus; + sleeptimeNs = TimeUnit.SECONDS.toNanos(statusIntervalSeconds); + measurements = Measurements.getMeasurements(); + this.trackJVMStats = trackJVMStats; + } + + /** + * Run and periodically report status. + */ + @Override + public void run() { + final long startTimeMs = System.currentTimeMillis(); + final long startTimeNanos = System.nanoTime(); + long deadline = startTimeNanos + sleeptimeNs; + long startIntervalMs = startTimeMs; + long lastTotalOps = 0; + + boolean alldone; + + do { + long nowMs = System.currentTimeMillis(); + + lastTotalOps = computeStats(startTimeMs, startIntervalMs, nowMs, lastTotalOps); + + if (trackJVMStats) { + measureJVM(); + } + + alldone = waitForClientsUntil(deadline); + + startIntervalMs = nowMs; + deadline += sleeptimeNs; + } + while (!alldone); + + if (trackJVMStats) { + measureJVM(); + } + // Print the final stats. + computeStats(startTimeMs, startIntervalMs, System.currentTimeMillis(), lastTotalOps); + } + + /** + * Computes and prints the stats. + * + * @param startTimeMs The start time of the test. + * @param startIntervalMs The start time of this interval. + * @param endIntervalMs The end time (now) for the interval. + * @param lastTotalOps The last total operations count. + * @return The current operation count. + */ + private long computeStats(final long startTimeMs, long startIntervalMs, long endIntervalMs, + long lastTotalOps) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); + + long totalops = 0; + long todoops = 0; + + // Calculate the total number of operations completed. + for (ClientThread t : clients) { + totalops += t.getOpsDone(); + todoops += t.getOpsTodo(); + } + + + long interval = endIntervalMs - startTimeMs; + double throughput = 1000.0 * (((double) totalops) / (double) interval); + double curthroughput = 1000.0 * (((double) (totalops - lastTotalOps)) / + ((double) (endIntervalMs - startIntervalMs))); + long estremaining = (long) Math.ceil(todoops / throughput); + + + DecimalFormat d = new DecimalFormat("#.##"); + String labelString = this.label + format.format(new Date()); + + StringBuilder msg = new StringBuilder(labelString).append(" ").append(interval / 1000).append(" sec: "); + msg.append(totalops).append(" operations; "); + + if (totalops != 0) { + msg.append(d.format(curthroughput)).append(" current ops/sec; "); + } + if (todoops != 0) { + msg.append("est completion in ").append(RemainingFormatter.format(estremaining)); + } + + msg.append(Measurements.getMeasurements().getSummary()); + + System.err.println(msg); + + if (standardstatus) { + System.out.println(msg); + } + return totalops; + } + + /** + * Waits for all of the client to finish or the deadline to expire. + * + * @param deadline The current deadline. + * @return True if all of the clients completed. + */ + private boolean waitForClientsUntil(long deadline) { + boolean alldone = false; + long now = System.nanoTime(); + + while (!alldone && now < deadline) { + try { + alldone = completeLatch.await(deadline - now, TimeUnit.NANOSECONDS); + } catch (InterruptedException ie) { + // If we are interrupted the thread is being asked to shutdown. + // Return true to indicate that and reset the interrupt state + // of the thread. + Thread.currentThread().interrupt(); + alldone = true; + } + now = System.nanoTime(); + } + + return alldone; + } + + /** + * Executes the JVM measurements. + */ + private void measureJVM() { + final int threads = Utils.getActiveThreadCount(); + if (threads < minThreads) { + minThreads = threads; + } + if (threads > maxThreads) { + maxThreads = threads; + } + measurements.measure("THREAD_COUNT", threads); + + // TODO - once measurements allow for other number types, switch to using + // the raw bytes. Otherwise we can track in MB to avoid negative values + // when faced with huge heaps. + final int usedMem = Utils.getUsedMemoryMegaBytes(); + if (usedMem < minUsedMem) { + minUsedMem = usedMem; + } + if (usedMem > maxUsedMem) { + maxUsedMem = usedMem; + } + measurements.measure("USED_MEM_MB", usedMem); + + // Some JVMs may not implement this feature so if the value is less than + // zero, just ommit it. + final double systemLoad = Utils.getSystemLoadAverage(); + if (systemLoad >= 0) { + // TODO - store the double if measurements allows for them + measurements.measure("SYS_LOAD_AVG", (int) systemLoad); + if (systemLoad > maxLoadAvg) { + maxLoadAvg = systemLoad; + } + if (systemLoad < minLoadAvg) { + minLoadAvg = systemLoad; + } + } + + final long gcs = Utils.getGCTotalCollectionCount(); + measurements.measure("GCS", (int) (gcs - lastGCCount)); + final long gcTime = Utils.getGCTotalTime(); + measurements.measure("GCS_TIME", (int) (gcTime - lastGCTime)); + lastGCCount = gcs; + lastGCTime = gcTime; + } + + /** + * @return The maximum threads running during the test. + */ + public int getMaxThreads() { + return maxThreads; + } + + /** + * @return The minimum threads running during the test. + */ + public int getMinThreads() { + return minThreads; + } + + /** + * @return The maximum memory used during the test. + */ + public long getMaxUsedMem() { + return maxUsedMem; + } + + /** + * @return The minimum memory used during the test. + */ + public long getMinUsedMem() { + return minUsedMem; + } + + /** + * @return The maximum load average during the test. + */ + public double getMaxLoadAvg() { + return maxLoadAvg; + } + + /** + * @return The minimum load average during the test. + */ + public double getMinLoadAvg() { + return minLoadAvg; + } + + /** + * @return Whether or not the thread is tracking JVM stats. + */ + public boolean trackJVMStats() { + return trackJVMStats; + } +} diff --git a/core/src/main/java/site/ycsb/StringByteIterator.java b/core/src/main/java/site/ycsb/StringByteIterator.java new file mode 100644 index 0000000..5c899ce --- /dev/null +++ b/core/src/main/java/site/ycsb/StringByteIterator.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +import java.util.HashMap; +import java.util.Map; + +/** + * A ByteIterator that iterates through a string. + */ +public class StringByteIterator extends ByteIterator { + private String str; + private int off; + + /** + * Put all of the entries of one map into the other, converting + * String values into ByteIterators. + */ + public static void putAllAsByteIterators(Map out, Map in) { + for (Map.Entry entry : in.entrySet()) { + out.put(entry.getKey(), new StringByteIterator(entry.getValue())); + } + } + + /** + * Put all of the entries of one map into the other, converting + * ByteIterator values into Strings. + */ + public static void putAllAsStrings(Map out, Map in) { + for (Map.Entry entry : in.entrySet()) { + out.put(entry.getKey(), entry.getValue().toString()); + } + } + + /** + * Create a copy of a map, converting the values from Strings to + * StringByteIterators. + */ + public static Map getByteIteratorMap(Map m) { + HashMap ret = + new HashMap(); + + for (Map.Entry entry : m.entrySet()) { + ret.put(entry.getKey(), new StringByteIterator(entry.getValue())); + } + return ret; + } + + /** + * Create a copy of a map, converting the values from + * StringByteIterators to Strings. + */ + public static Map getStringMap(Map m) { + HashMap ret = new HashMap(); + + for (Map.Entry entry : m.entrySet()) { + ret.put(entry.getKey(), entry.getValue().toString()); + } + return ret; + } + + public StringByteIterator(String s) { + this.str = s; + this.off = 0; + } + + @Override + public boolean hasNext() { + return off < str.length(); + } + + @Override + public byte nextByte() { + byte ret = (byte) str.charAt(off); + off++; + return ret; + } + + @Override + public long bytesLeft() { + return str.length() - off; + } + + @Override + public void reset() { + off = 0; + } + + @Override + public byte[] toArray() { + byte[] bytes = new byte[(int) bytesLeft()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) str.charAt(off + i); + } + off = str.length(); + return bytes; + } + + /** + * Specialization of general purpose toString() to avoid unnecessary + * copies. + *

+ * Creating a new StringByteIterator, then calling toString() + * yields the original String object, and does not perform any copies + * or String conversion operations. + *

+ */ + @Override + public String toString() { + if (off > 0) { + return super.toString(); + } else { + return str; + } + } +} diff --git a/core/src/main/java/site/ycsb/TerminatorThread.java b/core/src/main/java/site/ycsb/TerminatorThread.java new file mode 100644 index 0000000..56bd49b --- /dev/null +++ b/core/src/main/java/site/ycsb/TerminatorThread.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +import java.util.Collection; + +/** + * A thread that waits for the maximum specified time and then interrupts all the client + * threads passed at initialization of this thread. + * + * The maximum execution time passed is assumed to be in seconds. + * + */ +public class TerminatorThread extends Thread { + + private final Collection threads; + private long maxExecutionTime; + private Workload workload; + private long waitTimeOutInMS; + + public TerminatorThread(long maxExecutionTime, Collection threads, + Workload workload) { + this.maxExecutionTime = maxExecutionTime; + this.threads = threads; + this.workload = workload; + waitTimeOutInMS = 2000; + System.err.println("Maximum execution time specified as: " + maxExecutionTime + " secs"); + } + + public void run() { + try { + Thread.sleep(maxExecutionTime * 1000); + } catch (InterruptedException e) { + System.err.println("Could not wait until max specified time, TerminatorThread interrupted."); + return; + } + System.err.println("Maximum time elapsed. Requesting stop for the workload."); + workload.requestStop(); + System.err.println("Stop requested for workload. Now Joining!"); + for (Thread t : threads) { + while (t.isAlive()) { + try { + t.join(waitTimeOutInMS); + if (t.isAlive()) { + System.out.println("Still waiting for thread " + t.getName() + " to complete. " + + "Workload status: " + workload.isStopRequested()); + } + } catch (InterruptedException e) { + // Do nothing. Don't know why I was interrupted. + } + } + } + } +} diff --git a/core/src/main/java/site/ycsb/TimeseriesDB.java b/core/src/main/java/site/ycsb/TimeseriesDB.java new file mode 100644 index 0000000..eda6997 --- /dev/null +++ b/core/src/main/java/site/ycsb/TimeseriesDB.java @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2018 YCSB Contributors All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb; + +import site.ycsb.generator.Generator; +import site.ycsb.generator.IncrementingPrintableStringGenerator; +import site.ycsb.workloads.TimeSeriesWorkload; +import site.ycsb.wrappers.DatabaseField; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * Abstract class to adapt the default ycsb DB interface to Timeseries databases. + * This class is mostly here to be extended by Timeseries dataabases + * originally developed by Andreas Bader in YCSB-TS. + *

+ * This class is mostly parsing the workload information passed through the default ycsb interface + * according to the information outlined in {@link TimeSeriesWorkload}. + * It also contains some minor utility methods relevant to Timeseries databases. + *

+ * + * @implSpec It's vital to call super.init() when overwriting the init method + * to correctly initialize the workload-parsing. + */ +public abstract class TimeseriesDB extends DB { + + // defaults for downsampling. Basically we ignore it + private static final String DOWNSAMPLING_FUNCTION_PROPERTY_DEFAULT = "NONE"; + private static final String DOWNSAMPLING_INTERVAL_PROPERTY_DEFAULT = "0"; + + // debug property loading + private static final String DEBUG_PROPERTY = "debug"; + private static final String DEBUG_PROPERTY_DEFAULT = "false"; + + // test property loading + private static final String TEST_PROPERTY = "test"; + private static final String TEST_PROPERTY_DEFAULT = "false"; + + // Workload parameters that we need to parse this + protected String timestampKey; + protected String valueKey; + protected String tagPairDelimiter; + protected String queryTimeSpanDelimiter; + protected String deleteDelimiter; + protected TimeUnit timestampUnit; + protected String groupByKey; + protected String downsamplingKey; + protected Integer downsamplingInterval; + protected AggregationOperation downsamplingFunction; + + // YCSB-parameters + protected boolean debug; + protected boolean test; + + /** + * Initialize any state for this DB. + * Called once per DB instance; there is one DB instance per client thread. + */ + @Override + public void init() throws DBException { + // taken from BasicTSDB + timestampKey = getProperties().getProperty( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY, + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT); + valueKey = getProperties().getProperty( + TimeSeriesWorkload.VALUE_KEY_PROPERTY, + TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT); + tagPairDelimiter = getProperties().getProperty( + TimeSeriesWorkload.PAIR_DELIMITER_PROPERTY, + TimeSeriesWorkload.PAIR_DELIMITER_PROPERTY_DEFAULT); + queryTimeSpanDelimiter = getProperties().getProperty( + TimeSeriesWorkload.QUERY_TIMESPAN_DELIMITER_PROPERTY, + TimeSeriesWorkload.QUERY_TIMESPAN_DELIMITER_PROPERTY_DEFAULT); + deleteDelimiter = getProperties().getProperty( + TimeSeriesWorkload.DELETE_DELIMITER_PROPERTY, + TimeSeriesWorkload.DELETE_DELIMITER_PROPERTY_DEFAULT); + timestampUnit = TimeUnit.valueOf(getProperties().getProperty( + TimeSeriesWorkload.TIMESTAMP_UNITS_PROPERTY, + TimeSeriesWorkload.TIMESTAMP_UNITS_PROPERTY_DEFAULT)); + groupByKey = getProperties().getProperty( + TimeSeriesWorkload.GROUPBY_KEY_PROPERTY, + TimeSeriesWorkload.GROUPBY_KEY_PROPERTY_DEFAULT); + downsamplingKey = getProperties().getProperty( + TimeSeriesWorkload.DOWNSAMPLING_KEY_PROPERTY, + TimeSeriesWorkload.DOWNSAMPLING_KEY_PROPERTY_DEFAULT); + downsamplingFunction = TimeseriesDB.AggregationOperation.valueOf(getProperties() + .getProperty(TimeSeriesWorkload.DOWNSAMPLING_FUNCTION_PROPERTY, DOWNSAMPLING_FUNCTION_PROPERTY_DEFAULT)); + downsamplingInterval = Integer.valueOf(getProperties() + .getProperty(TimeSeriesWorkload.DOWNSAMPLING_INTERVAL_PROPERTY, DOWNSAMPLING_INTERVAL_PROPERTY_DEFAULT)); + + test = Boolean.parseBoolean(getProperties().getProperty(TEST_PROPERTY, TEST_PROPERTY_DEFAULT)); + debug = Boolean.parseBoolean(getProperties().getProperty(DEBUG_PROPERTY, DEBUG_PROPERTY_DEFAULT)); + } + + @Override + public final Status read(String table, String key, Set fields, Map result) { + Map> tagQueries = new HashMap<>(); + Long timestamp = null; + for (String field : fields) { + if (field.startsWith(timestampKey)) { + String[] timestampParts = field.split(tagPairDelimiter); + if (timestampParts[1].contains(queryTimeSpanDelimiter)) { + // Since we're looking for a single datapoint, a range of timestamps makes no sense. + // As we cannot throw an exception to bail out here, we return `BAD_REQUEST` instead. + return Status.BAD_REQUEST; + } + timestamp = Long.valueOf(timestampParts[1]); + } else { + String[] queryParts = field.split(tagPairDelimiter); + tagQueries.computeIfAbsent(queryParts[0], k -> new ArrayList<>()).add(queryParts[1]); + } + } + if (timestamp == null) { + return Status.BAD_REQUEST; + } + + return read(table, timestamp, tagQueries); + } + + /** + * Read a record from the database. Each value from the result will be stored in a HashMap + * + * @param metric The name of the metric + * @param timestamp The timestamp of the record to read. + * @param tags actual tags that were want to receive (can be empty) + * @return Zero on success, a non-zero error code on error or "not found". + */ + protected abstract Status read(String metric, long timestamp, Map> tags); + + /** + * @inheritDoc + * @implNote this method parses the information passed to it and subsequently passes it to the modified + * interface at {@link #scan(String, long, long, Map, AggregationOperation, int, TimeUnit)} + */ + @Override + public final Status scan(String table, String startkey, int recordcount, Set fields, + Vector> result) { + Map> tagQueries = new HashMap<>(); + TimeseriesDB.AggregationOperation aggregationOperation = TimeseriesDB.AggregationOperation.NONE; + Set groupByFields = new HashSet<>(); + + boolean rangeSet = false; + long start = 0; + long end = 0; + for (String field : fields) { + if (field.startsWith(timestampKey)) { + String[] timestampParts = field.split(tagPairDelimiter); + if (!timestampParts[1].contains(queryTimeSpanDelimiter)) { + // seems like this should be a more elaborate query. + // for now we don't support scanning single timestamps + // TODO: Support Timestamp range queries + return Status.NOT_IMPLEMENTED; + } + String[] rangeParts = timestampParts[1].split(queryTimeSpanDelimiter); + rangeSet = true; + start = Long.valueOf(rangeParts[0]); + end = Long.valueOf(rangeParts[1]); + } else if (field.startsWith(groupByKey)) { + String groupBySpecifier = field.split(tagPairDelimiter)[1]; + aggregationOperation = TimeseriesDB.AggregationOperation.valueOf(groupBySpecifier); + } else if (field.startsWith(downsamplingKey)) { + String downsamplingSpec = field.split(tagPairDelimiter)[1]; + // apparently that needs to always hold true: + if (!downsamplingSpec.equals(downsamplingFunction.toString() + downsamplingInterval.toString())) { + System.err.print("Downsampling specification for Scan did not match configured downsampling"); + return Status.BAD_REQUEST; + } + } else { + String[] queryParts = field.split(tagPairDelimiter); + if (queryParts.length == 1) { + // we should probably warn about this being ignored... + System.err.println("Grouping by arbitrary series is currently not supported"); + groupByFields.add(field); + } else { + tagQueries.computeIfAbsent(queryParts[0], k -> new ArrayList<>()).add(queryParts[1]); + } + } + } + if (!rangeSet) { + return Status.BAD_REQUEST; + } + return scan(table, start, end, tagQueries, downsamplingFunction, downsamplingInterval, timestampUnit); + } + + /** + * Perform a range scan for a set of records in the database. Each value from the result will be stored in a + * HashMap. + * + * @param metric The name of the metric + * @param startTs The timestamp of the first record to read. + * @param endTs The timestamp of the last record to read. + * @param tags actual tags that were want to receive (can be empty). + * @param aggreg The aggregation operation to perform. + * @param timeValue value for timeUnit for aggregation + * @param timeUnit timeUnit for aggregation + * @return A {@link Status} detailing the outcome of the scan operation. + */ + protected abstract Status scan(String metric, long startTs, long endTs, Map> tags, + AggregationOperation aggreg, int timeValue, TimeUnit timeUnit); + + @Override + public Status update(String table, String key, Map values) { + return Status.NOT_IMPLEMENTED; + // not supportable for general TSDBs + // can be explicitly overwritten in inheriting classes + } + + @Override + public final Status insert(String table, String key, List fields) { + NumericByteIterator tsContainer = null; + NumericByteIterator valueContainer = null; + Map values = new HashMap(); + for(DatabaseField f : fields) { + String fieldname = f.getFieldname(); + if(timestampKey.equals(fieldname)) { + tsContainer = (NumericByteIterator) f.getContent().asIterator(); + continue; + } + if(valueKey.equals(f.getFieldname())) { + valueContainer = (NumericByteIterator) f.getContent().asIterator(); + continue; + } + values.put(fieldname, f.getContent().asIterator()); + } + if (valueContainer.isFloatingPoint()) { + return insert(table, tsContainer.getLong(), valueContainer.getDouble(), values); + } else { + return insert(table, tsContainer.getLong(), valueContainer.getLong(), values); + } + } + + /** + * Insert a record into the database. Any tags/tagvalue pairs in the specified tagmap and the given value will be + * written into the record with the specified timestamp. + * + * @param metric The name of the metric + * @param timestamp The timestamp of the record to insert. + * @param value The actual value to insert. + * @param tags A Map of tag/tagvalue pairs to insert as tags + * @return A {@link Status} detailing the outcome of the insert + */ + protected abstract Status insert(String metric, long timestamp, long value, Map tags); + + /** + * Insert a record in the database. Any tags/tagvalue pairs in the specified tagmap and the given value will be + * written into the record with the specified timestamp. + * + * @param metric The name of the metric + * @param timestamp The timestamp of the record to insert. + * @param value actual value to insert + * @param tags A HashMap of tag/tagvalue pairs to insert as tags + * @return A {@link Status} detailing the outcome of the insert + */ + protected abstract Status insert(String metric, long timestamp, double value, Map tags); + + /** + * NOTE: This operation is usually not supported for Time-Series databases. + * Deletion of data is often instead regulated through automatic cleanup and "retention policies" or similar. + * + * @return Status.NOT_IMPLEMENTED or a {@link Status} specifying the outcome of deletion + * in case the operation is supported. + */ + public Status delete(String table, String key) { + return Status.NOT_IMPLEMENTED; + } + + /** + * Examines the given {@link Properties} and returns an array containing the Tag Keys + * (basically matching column names for traditional Relational DBs) that are detailed in the workload specification. + * See {@link TimeSeriesWorkload} for how these are generated. + *

+ * This method is intended to be called during the initialization phase to create a table schema + * for DBMS that require such a schema before values can be inserted (or queried) + * + * @param properties The properties detailing the workload configuration. + * @return An array of strings specifying all allowed TagKeys (or column names) + * except for the "value" and the "timestamp" column name. + * @implSpec WARNING this method must exactly match how tagKeys are generated by the {@link TimeSeriesWorkload}, + * otherwise databases requiring this information will most likely break! + */ + protected static String[] getPossibleTagKeys(Properties properties) { + final int tagCount = Integer.parseInt(properties.getProperty(TimeSeriesWorkload.TAG_COUNT_PROPERTY, + TimeSeriesWorkload.TAG_COUNT_PROPERTY_DEFAULT)); + final int tagKeylength = Integer.parseInt(properties.getProperty(TimeSeriesWorkload.TAG_KEY_LENGTH_PROPERTY, + TimeSeriesWorkload.TAG_KEY_LENGTH_PROPERTY_DEFAULT)); + + Generator tagKeyGenerator = new IncrementingPrintableStringGenerator(tagKeylength); + String[] tagNames = new String[tagCount]; + for (int i = 0; i < tagCount; i++) { + tagNames[i] = tagKeyGenerator.nextValue(); + } + return tagNames; + } + + + /** + * An enum containing the possible aggregation operations. + * Not all of these operations are required to be supported by implementing classes. + *

+ * Aggregations are applied when using the SCAN operation on a range of timestamps. + * That way the result set is reduced from multiple records into + * a single one or one record for each group specified through GROUP BY clauses. + */ + public enum AggregationOperation { + /** + * No aggregation whatsoever. Return the results as a full table + */ + NONE, + /** + * Sum the values of the matching records when calculating the value. + * GroupBy criteria apply where relevant for sub-summing. + */ + SUM, + /** + * Calculate the arithmetic mean over the value across matching records when calculating the value. + * GroupBy criteria apply where relevant for group-targeted averages + */ + AVERAGE, + /** + * Count the number of matching records and return that as value. + * GroupBy criteria apply where relevant. + */ + COUNT, + /** + * Return only the maximum of the matching record values. + * GroupBy criteria apply and result in group-based maxima. + */ + MAX, + /** + * Return only the minimum of the matching record values. + * GroupBy criteria apply and result in group-based minima. + */ + MIN; + } +} diff --git a/core/src/main/java/site/ycsb/UnknownDBException.java b/core/src/main/java/site/ycsb/UnknownDBException.java new file mode 100644 index 0000000..1660ea7 --- /dev/null +++ b/core/src/main/java/site/ycsb/UnknownDBException.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +/** + * Could not create the specified DB. + */ +public class UnknownDBException extends Exception { + /** + * + */ + private static final long serialVersionUID = 459099842269616836L; + + public UnknownDBException(String message) { + super(message); + } + + public UnknownDBException() { + super(); + } + + public UnknownDBException(String message, Throwable cause) { + super(message, cause); + } + + public UnknownDBException(Throwable cause) { + super(cause); + } + +} diff --git a/core/src/main/java/site/ycsb/Utils.java b/core/src/main/java/site/ycsb/Utils.java new file mode 100644 index 0000000..6bfb4fa --- /dev/null +++ b/core/src/main/java/site/ycsb/Utils.java @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2010 Yahoo! Inc., 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 site.ycsb; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Utility functions. + */ +public final class Utils { + private Utils() { + // not used + } + + /** + * Hash an integer value. + */ + public static long hash(long val) { + return fnvhash64(val); + } + + public static final long FNV_OFFSET_BASIS_64 = 0xCBF29CE484222325L; + public static final long FNV_PRIME_64 = 1099511628211L; + + /** + * 64 bit FNV hash. Produces more "random" hashes than (say) String.hashCode(). + * + * @param val The value to hash. + * @return The hash value + */ + public static long fnvhash64(long val) { + //from http://en.wikipedia.org/wiki/Fowler_Noll_Vo_hash + long hashval = FNV_OFFSET_BASIS_64; + + for (int i = 0; i < 8; i++) { + long octet = val & 0x00ff; + val = val >> 8; + + hashval = hashval ^ octet; + hashval = hashval * FNV_PRIME_64; + //hashval = hashval ^ octet; + } + return Math.abs(hashval); + } + + /** + * Reads a big-endian 8-byte long from an offset in the given array. + * @param bytes The array to read from. + * @return A long integer. + * @throws IndexOutOfBoundsException if the byte array is too small. + * @throws NullPointerException if the byte array is null. + */ + public static long bytesToLong(final byte[] bytes) { + return (bytes[0] & 0xFFL) << 56 + | (bytes[1] & 0xFFL) << 48 + | (bytes[2] & 0xFFL) << 40 + | (bytes[3] & 0xFFL) << 32 + | (bytes[4] & 0xFFL) << 24 + | (bytes[5] & 0xFFL) << 16 + | (bytes[6] & 0xFFL) << 8 + | (bytes[7] & 0xFFL) << 0; + } + + /** + * Writes a big-endian 8-byte long at an offset in the given array. + * @param val The value to encode. + * @throws IndexOutOfBoundsException if the byte array is too small. + */ + public static byte[] longToBytes(final long val) { + final byte[] bytes = new byte[8]; + bytes[0] = (byte) (val >>> 56); + bytes[1] = (byte) (val >>> 48); + bytes[2] = (byte) (val >>> 40); + bytes[3] = (byte) (val >>> 32); + bytes[4] = (byte) (val >>> 24); + bytes[5] = (byte) (val >>> 16); + bytes[6] = (byte) (val >>> 8); + bytes[7] = (byte) (val >>> 0); + return bytes; + } + + /** + * Parses the byte array into a double. + * The byte array must be at least 8 bytes long and have been encoded using + * {@link #doubleToBytes}. If the array is longer than 8 bytes, only the + * first 8 bytes are parsed. + * @param bytes The byte array to parse, at least 8 bytes. + * @return A double value read from the byte array. + * @throws IllegalArgumentException if the byte array is not 8 bytes wide. + */ + public static double bytesToDouble(final byte[] bytes) { + if (bytes.length < 8) { + throw new IllegalArgumentException("Byte array must be 8 bytes wide."); + } + return Double.longBitsToDouble(bytesToLong(bytes)); + } + + /** + * Encodes the double value as an 8 byte array. + * @param val The double value to encode. + * @return A byte array of length 8. + */ + public static byte[] doubleToBytes(final double val) { + return longToBytes(Double.doubleToRawLongBits(val)); + } + + /** + * Measure the estimated active thread count in the current thread group. + * Since this calls {@link Thread.activeCount} it should be called from the + * main thread or one started by the main thread. Threads included in the + * count can be in any state. + * For a more accurate count we could use {@link Thread.getAllStackTraces().size()} + * but that freezes the JVM and incurs a high overhead. + * @return An estimated thread count, good for showing the thread count + * over time. + */ + public static int getActiveThreadCount() { + return Thread.activeCount(); + } + + /** @return The currently used memory in bytes */ + public static long getUsedMemoryBytes() { + final Runtime runtime = Runtime.getRuntime(); + return runtime.totalMemory() - runtime.freeMemory(); + } + + /** @return The currently used memory in megabytes. */ + public static int getUsedMemoryMegaBytes() { + return (int) (getUsedMemoryBytes() / 1024 / 1024); + } + + /** @return The current system load average if supported by the JDK. + * If it's not supported, the value will be negative. */ + public static double getSystemLoadAverage() { + final OperatingSystemMXBean osBean = + ManagementFactory.getOperatingSystemMXBean(); + return osBean.getSystemLoadAverage(); + } + + /** @return The total number of garbage collections executed for all + * memory pools. */ + public static long getGCTotalCollectionCount() { + final List gcBeans = + ManagementFactory.getGarbageCollectorMXBeans(); + long count = 0; + for (final GarbageCollectorMXBean bean : gcBeans) { + if (bean.getCollectionCount() < 0) { + continue; + } + count += bean.getCollectionCount(); + } + return count; + } + + /** @return The total time, in milliseconds, spent in GC. */ + public static long getGCTotalTime() { + final List gcBeans = + ManagementFactory.getGarbageCollectorMXBeans(); + long time = 0; + for (final GarbageCollectorMXBean bean : gcBeans) { + if (bean.getCollectionTime() < 0) { + continue; + } + time += bean.getCollectionTime(); + } + return time; + } + + /** + * Returns a map of garbage collectors and their stats. + * The first object in the array is the total count since JVM start and the + * second is the total time (ms) since JVM start. + * If a garbage collectors does not support the collector MXBean, then it + * will not be represented in the map. + * @return A non-null map of garbage collectors and their metrics. The map + * may be empty. + */ + public static Map getGCStatst() { + final List gcBeans = + ManagementFactory.getGarbageCollectorMXBeans(); + final Map map = new HashMap(gcBeans.size()); + for (final GarbageCollectorMXBean bean : gcBeans) { + if (!bean.isValid() || bean.getCollectionCount() < 0 || + bean.getCollectionTime() < 0) { + continue; + } + + final Long[] measurements = new Long[]{ + bean.getCollectionCount(), + bean.getCollectionTime() + }; + map.put(bean.getName().replace(" ", "_"), measurements); + } + return map; + } + + /** + * Simple Fisher-Yates array shuffle to randomize discrete sets. + * @param array The array to randomly shuffle. + * @return The shuffled array. + */ + public static T [] shuffleArray(final T[] array) { + for (int i = array.length -1; i > 0; i--) { + final int idx = ThreadLocalRandom.current().nextInt(i + 1); + final T temp = array[idx]; + array[idx] = array[i]; + array[i] = temp; + } + return array; + } +} diff --git a/core/src/main/java/site/ycsb/Workload.java b/core/src/main/java/site/ycsb/Workload.java new file mode 100644 index 0000000..1d7ae24 --- /dev/null +++ b/core/src/main/java/site/ycsb/Workload.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Properties; + + +/** + * One experiment scenario. One object of this type will + * be instantiated and shared among all client threads. This class + * should be constructed using a no-argument constructor, so we can + * load it dynamically. Any argument-based initialization should be + * done by init(). + * + * If you extend this class, you should support the "insertstart" property. This + * allows the Client to proceed from multiple clients on different machines, in case + * the client is the bottleneck. For example, if we want to load 1 million records from + * 2 machines, the first machine should have insertstart=0 and the second insertstart=500000. Additionally, + * the "insertcount" property, which is interpreted by Client, can be used to tell each instance of the + * client how many inserts to do. In the example above, both clients should have insertcount=500000. + */ +public abstract class Workload { + public static final String INSERT_START_PROPERTY = "insertstart"; + public static final String INSERT_COUNT_PROPERTY = "insertcount"; + + public static final String INSERT_START_PROPERTY_DEFAULT = "0"; + + private volatile AtomicBoolean stopRequested = new AtomicBoolean(false); + + /** Operations available for a database. */ + public enum Operation { + READ, + UPDATE, + INSERT, + SCAN, + DELETE + } + + /** + * Initialize the scenario. Create any generators and other shared objects here. + * Called once, in the main client thread, before any operations are started. + */ + public void init(Properties p) throws WorkloadException { + } + + /** + * Initialize any state for a particular client thread. Since the scenario object + * will be shared among all threads, this is the place to create any state that is specific + * to one thread. To be clear, this means the returned object should be created anew on each + * call to initThread(); do not return the same object multiple times. + * The returned object will be passed to invocations of doInsert() and doTransaction() + * for this thread. There should be no side effects from this call; all state should be encapsulated + * in the returned object. If you have no state to retain for this thread, return null. (But if you have + * no state to retain for this thread, probably you don't need to override initThread().) + * + * @return false if the workload knows it is done for this thread. Client will terminate the thread. + * Return true otherwise. Return true for workloads that rely on operationcount. For workloads that read + * traces from a file, return true when there are more to do, false when you are done. + */ + public Object initThread(Properties p, int mythreadid, int threadcount) throws WorkloadException { + return null; + } + + /** + * Cleanup the scenario. Called once, in the main client thread, after all operations have completed. + */ + public void cleanup() throws WorkloadException { + } + + /** + * Do one insert operation. Because it will be called concurrently from multiple client threads, this + * function must be thread safe. However, avoid synchronized, or the threads will block waiting for each + * other, and it will be difficult to reach the target throughput. Ideally, this function would have no side + * effects other than DB operations and mutations on threadstate. Mutations to threadstate do not need to be + * synchronized, since each thread has its own threadstate instance. + */ + public abstract boolean doInsert(DB db, Object threadstate); + + /** + * Do one transaction operation. Because it will be called concurrently from multiple client threads, this + * function must be thread safe. However, avoid synchronized, or the threads will block waiting for each + * other, and it will be difficult to reach the target throughput. Ideally, this function would have no side + * effects other than DB operations and mutations on threadstate. Mutations to threadstate do not need to be + * synchronized, since each thread has its own threadstate instance. + * + * @return false if the workload knows it is done for this thread. Client will terminate the thread. + * Return true otherwise. Return true for workloads that rely on operationcount. For workloads that read + * traces from a file, return true when there are more to do, false when you are done. + */ + public abstract boolean doTransaction(DB db, Object threadstate); + + /** + * Allows scheduling a request to stop the workload. + */ + public void requestStop() { + stopRequested.set(true); + } + + /** + * Check the status of the stop request flag. + * @return true if stop was requested, false otherwise. + */ + public boolean isStopRequested() { + return stopRequested.get(); + } +} diff --git a/core/src/main/java/site/ycsb/WorkloadException.java b/core/src/main/java/site/ycsb/WorkloadException.java new file mode 100644 index 0000000..61c9986 --- /dev/null +++ b/core/src/main/java/site/ycsb/WorkloadException.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb; + +/** + * The workload tried to do something bad. + */ +public class WorkloadException extends Exception { + /** + * + */ + private static final long serialVersionUID = 8844396756042772132L; + + public WorkloadException(String message) { + super(message); + } + + public WorkloadException() { + super(); + } + + public WorkloadException(String message, Throwable cause) { + super(message, cause); + } + + public WorkloadException(Throwable cause) { + super(cause); + } + +} diff --git a/core/src/main/java/site/ycsb/generator/ConstantIntegerGenerator.java b/core/src/main/java/site/ycsb/generator/ConstantIntegerGenerator.java new file mode 100644 index 0000000..34ea04d --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/ConstantIntegerGenerator.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +/** + * A trivial integer generator that always returns the same value. + * + */ +public class ConstantIntegerGenerator extends NumberGenerator { + private final int i; + + /** + * @param i The integer that this generator will always return. + */ + public ConstantIntegerGenerator(int i) { + this.i = i; + } + + @Override + public Integer nextValue() { + return i; + } + + @Override + public double mean() { + return i; + } + +} diff --git a/core/src/main/java/site/ycsb/generator/CounterGenerator.java b/core/src/main/java/site/ycsb/generator/CounterGenerator.java new file mode 100644 index 0000000..01f05fa --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/CounterGenerator.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010 Yahoo! Inc., Copyright (c) 2017 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 site.ycsb.generator; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Generates a sequence of integers. + * (0, 1, ...) + */ +public class CounterGenerator extends NumberGenerator { + private final AtomicLong counter; + + /** + * Create a counter that starts at countstart. + */ + public CounterGenerator(long countstart) { + counter=new AtomicLong(countstart); + } + + @Override + public Long nextValue() { + return counter.getAndIncrement(); + } + + @Override + public Long lastValue() { + return counter.get() - 1; + } + + @Override + public double mean() { + throw new UnsupportedOperationException("Can't compute mean of non-stationary distribution!"); + } +} diff --git a/core/src/main/java/site/ycsb/generator/DiscreteGenerator.java b/core/src/main/java/site/ycsb/generator/DiscreteGenerator.java new file mode 100644 index 0000000..8d397f3 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/DiscreteGenerator.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ThreadLocalRandom; + +import static java.util.Objects.requireNonNull; + +/** + * Generates a distribution by choosing from a discrete set of values. + */ +public class DiscreteGenerator extends Generator { + private static class Pair { + private double weight; + private String value; + + Pair(double weight, String value) { + this.weight = weight; + this.value = requireNonNull(value); + } + } + + private final Collection values = new ArrayList<>(); + private String lastvalue; + + public DiscreteGenerator() { + lastvalue = null; + } + + /** + * Generate the next string in the distribution. + */ + @Override + public String nextValue() { + double sum = 0; + + for (Pair p : values) { + sum += p.weight; + } + + double val = ThreadLocalRandom.current().nextDouble(); + + for (Pair p : values) { + double pw = p.weight / sum; + if (val < pw) { + return p.value; + } + + val -= pw; + } + + throw new AssertionError("oops. should not get here."); + + } + + /** + * Return the previous string generated by the distribution; e.g., returned from the last nextString() call. + * Calling lastString() should not advance the distribution or have any side effects. If nextString() has not yet + * been called, lastString() should return something reasonable. + */ + @Override + public String lastValue() { + if (lastvalue == null) { + lastvalue = nextValue(); + } + return lastvalue; + } + + public void addValue(double weight, String value) { + values.add(new Pair(weight, value)); + } + +} diff --git a/core/src/main/java/site/ycsb/generator/ExponentialGenerator.java b/core/src/main/java/site/ycsb/generator/ExponentialGenerator.java new file mode 100644 index 0000000..39ead69 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/ExponentialGenerator.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2011-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * A generator of an exponential distribution. It produces a sequence + * of time intervals according to an exponential + * distribution. Smaller intervals are more frequent than larger + * ones, and there is no bound on the length of an interval. When you + * construct an instance of this class, you specify a parameter gamma, + * which corresponds to the rate at which events occur. + * Alternatively, 1/gamma is the average length of an interval. + */ +public class ExponentialGenerator extends NumberGenerator { + // What percentage of the readings should be within the most recent exponential.frac portion of the dataset? + public static final String EXPONENTIAL_PERCENTILE_PROPERTY = "exponential.percentile"; + public static final String EXPONENTIAL_PERCENTILE_DEFAULT = "95"; + + // What fraction of the dataset should be accessed exponential.percentile of the time? + public static final String EXPONENTIAL_FRAC_PROPERTY = "exponential.frac"; + public static final String EXPONENTIAL_FRAC_DEFAULT = "0.8571428571"; // 1/7 + + /** + * The exponential constant to use. + */ + private double gamma; + + /******************************* Constructors **************************************/ + + /** + * Create an exponential generator with a mean arrival rate of + * gamma. (And half life of 1/gamma). + */ + public ExponentialGenerator(double mean) { + gamma = 1.0 / mean; + } + + public ExponentialGenerator(double percentile, double range) { + gamma = -Math.log(1.0 - percentile / 100.0) / range; //1.0/mean; + } + + /****************************************************************************************/ + + + /** + * Generate the next item as a long. This distribution will be skewed toward lower values; e.g. 0 will + * be the most popular, 1 the next most popular, etc. + * @return The next item in the sequence. + */ + @Override + public Double nextValue() { + return -Math.log(ThreadLocalRandom.current().nextDouble()) / gamma; + } + + @Override + public double mean() { + return 1.0 / gamma; + } + + public static void main(String[] args) { + ExponentialGenerator e = new ExponentialGenerator(90, 100); + int j = 0; + for (int i = 0; i < 1000; i++) { + if (e.nextValue() < 100) { + j++; + } + } + System.out.println("Got " + j + " hits. Expect 900"); + } +} diff --git a/core/src/main/java/site/ycsb/generator/FileGenerator.java b/core/src/main/java/site/ycsb/generator/FileGenerator.java new file mode 100644 index 0000000..9542356 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/FileGenerator.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; + +/** + * A generator, whose sequence is the lines of a file. + */ +public class FileGenerator extends Generator { + private final String filename; + private String current; + private BufferedReader reader; + + /** + * Create a FileGenerator with the given file. + * @param filename The file to read lines from. + */ + public FileGenerator(String filename) { + this.filename = filename; + reloadFile(); + } + + /** + * Return the next string of the sequence, ie the next line of the file. + */ + @Override + public synchronized String nextValue() { + try { + current = reader.readLine(); + return current; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Return the previous read line. + */ + @Override + public String lastValue() { + return current; + } + + /** + * Reopen the file to reuse values. + */ + public synchronized void reloadFile() { + try (Reader r = reader) { + System.err.println("Reload " + filename); + reader = new BufferedReader(new FileReader(filename)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/site/ycsb/generator/Generator.java b/core/src/main/java/site/ycsb/generator/Generator.java new file mode 100644 index 0000000..9b880f5 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/Generator.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +/** + * An expression that generates a sequence of values, following some distribution (Uniform, Zipfian, Sequential, etc.). + */ +public abstract class Generator { + /** + * Generate the next value in the distribution. + */ + public abstract V nextValue(); + + /** + * Return the previous value generated by the distribution; e.g., returned from the last {@link Generator#nextValue()} + * call. + * Calling {@link #lastValue()} should not advance the distribution or have any side effects. If {@link #nextValue()} + * has not yet been called, {@link #lastValue()} should return something reasonable. + */ + public abstract V lastValue(); + + public final String nextString() { + V ret = nextValue(); + return ret == null ? null : ret.toString(); + } + + public final String lastString() { + V ret = lastValue(); + return ret == null ? null : ret.toString(); + } +} + diff --git a/core/src/main/java/site/ycsb/generator/HistogramGenerator.java b/core/src/main/java/site/ycsb/generator/HistogramGenerator.java new file mode 100644 index 0000000..d63631f --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/HistogramGenerator.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Generate integers according to a histogram distribution. The histogram + * buckets are of width one, but the values are multiplied by a block size. + * Therefore, instead of drawing sizes uniformly at random within each + * bucket, we always draw the largest value in the current bucket, so the value + * drawn is always a multiple of blockSize. + * + * The minimum value this distribution returns is blockSize (not zero). + * + */ +public class HistogramGenerator extends NumberGenerator { + + private final long blockSize; + private final long[] buckets; + private long area; + private long weightedArea = 0; + private double meanSize = 0; + + public HistogramGenerator(String histogramfile) throws IOException { + try (BufferedReader in = new BufferedReader(new FileReader(histogramfile))) { + String str; + String[] line; + + ArrayList a = new ArrayList<>(); + + str = in.readLine(); + if (str == null) { + throw new IOException("Empty input file!\n"); + } + line = str.split("\t"); + if (line[0].compareTo("BlockSize") != 0) { + throw new IOException("First line of histogram is not the BlockSize!\n"); + } + blockSize = Integer.parseInt(line[1]); + + while ((str = in.readLine()) != null) { + // [0] is the bucket, [1] is the value + line = str.split("\t"); + + a.add(Integer.parseInt(line[0]), Integer.parseInt(line[1])); + } + buckets = new long[a.size()]; + for (int i = 0; i < a.size(); i++) { + buckets[i] = a.get(i); + } + } + init(); + } + + public HistogramGenerator(long[] buckets, int blockSize) { + this.blockSize = blockSize; + this.buckets = buckets; + init(); + } + + private void init() { + for (int i = 0; i < buckets.length; i++) { + area += buckets[i]; + weightedArea += i * buckets[i]; + } + // calculate average file size + meanSize = ((double) blockSize) * ((double) weightedArea) / (area); + } + + @Override + public Long nextValue() { + int number = ThreadLocalRandom.current().nextInt((int) area); + int i; + + for (i = 0; i < (buckets.length - 1); i++) { + number -= buckets[i]; + if (number <= 0) { + return (i + 1) * blockSize; + } + } + + return i * blockSize; + } + + @Override + public double mean() { + return meanSize; + } +} diff --git a/core/src/main/java/site/ycsb/generator/HotspotIntegerGenerator.java b/core/src/main/java/site/ycsb/generator/HotspotIntegerGenerator.java new file mode 100644 index 0000000..d4b9828 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/HotspotIntegerGenerator.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. Copyright (c) 2017 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 site.ycsb.generator; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Generate integers resembling a hotspot distribution where x% of operations + * access y% of data items. The parameters specify the bounds for the numbers, + * the percentage of the of the interval which comprises the hot set and + * the percentage of operations that access the hot set. Numbers of the hot set are + * always smaller than any number in the cold set. Elements from the hot set and + * the cold set are chose using a uniform distribution. + * + */ +public class HotspotIntegerGenerator extends NumberGenerator { + + private final long lowerBound; + private final long upperBound; + private final long hotInterval; + private final long coldInterval; + private final double hotsetFraction; + private final double hotOpnFraction; + + /** + * Create a generator for Hotspot distributions. + * + * @param lowerBound lower bound of the distribution. + * @param upperBound upper bound of the distribution. + * @param hotsetFraction percentage of data item + * @param hotOpnFraction percentage of operations accessing the hot set. + */ + public HotspotIntegerGenerator(long lowerBound, long upperBound, + double hotsetFraction, double hotOpnFraction) { + if (hotsetFraction < 0.0 || hotsetFraction > 1.0) { + System.err.println("Hotset fraction out of range. Setting to 0.0"); + hotsetFraction = 0.0; + } + if (hotOpnFraction < 0.0 || hotOpnFraction > 1.0) { + System.err.println("Hot operation fraction out of range. Setting to 0.0"); + hotOpnFraction = 0.0; + } + if (lowerBound > upperBound) { + System.err.println("Upper bound of Hotspot generator smaller than the lower bound. " + + "Swapping the values."); + long temp = lowerBound; + lowerBound = upperBound; + upperBound = temp; + } + this.lowerBound = lowerBound; + this.upperBound = upperBound; + this.hotsetFraction = hotsetFraction; + long interval = upperBound - lowerBound + 1; + this.hotInterval = (int) (interval * hotsetFraction); + this.coldInterval = interval - hotInterval; + this.hotOpnFraction = hotOpnFraction; + } + + @Override + public Long nextValue() { + long value = 0; + Random random = ThreadLocalRandom.current(); + if (random.nextDouble() < hotOpnFraction) { + // Choose a value from the hot set. + value = lowerBound + Math.abs(random.nextLong()) % hotInterval; + } else { + // Choose a value from the cold set. + value = lowerBound + hotInterval + Math.abs(random.nextLong()) % coldInterval; + } + setLastValue(value); + return value; + } + + /** + * @return the lowerBound + */ + public long getLowerBound() { + return lowerBound; + } + + /** + * @return the upperBound + */ + public long getUpperBound() { + return upperBound; + } + + /** + * @return the hotsetFraction + */ + public double getHotsetFraction() { + return hotsetFraction; + } + + /** + * @return the hotOpnFraction + */ + public double getHotOpnFraction() { + return hotOpnFraction; + } + + @Override + public double mean() { + return hotOpnFraction * (lowerBound + hotInterval / 2.0) + + (1 - hotOpnFraction) * (lowerBound + hotInterval + coldInterval / 2.0); + } +} diff --git a/core/src/main/java/site/ycsb/generator/IncrementingPrintableStringGenerator.java b/core/src/main/java/site/ycsb/generator/IncrementingPrintableStringGenerator.java new file mode 100644 index 0000000..3c178d8 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/IncrementingPrintableStringGenerator.java @@ -0,0 +1,389 @@ +/** + * Copyright (c) 2016-2017 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 site.ycsb.generator; + +import java.util.*; + +/** + * A generator that produces strings of {@link #length} using a set of code points + * from {@link #characterSet}. Each time {@link #nextValue()} is executed, the string + * is incremented by one character. Eventually the string may rollover to the beginning + * and the user may choose to have the generator throw a NoSuchElementException at that + * point or continue incrementing. (By default the generator will continue incrementing). + *

+ * For example, if we set a length of 2 characters and the character set includes + * [A, B] then the generator output will be: + *

    + *
  • AA
  • + *
  • AB
  • + *
  • BA
  • + *
  • BB
  • + *
  • AA <-- rolled over
  • + *
+ *

+ * This class includes some default character sets to choose from including ASCII + * and plane 0 UTF. + */ +public class IncrementingPrintableStringGenerator extends Generator { + + /** Default string length for the generator. */ + public static final int DEFAULTSTRINGLENGTH = 8; + + /** + * Set of all character types that include every symbol other than non-printable + * control characters. + */ + public static final Set CHAR_TYPES_ALL_BUT_CONTROL; + + static { + CHAR_TYPES_ALL_BUT_CONTROL = new HashSet(24); + // numbers + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.DECIMAL_DIGIT_NUMBER); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.LETTER_NUMBER); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.OTHER_NUMBER); + + // letters + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.UPPERCASE_LETTER); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.LOWERCASE_LETTER); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.TITLECASE_LETTER); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.OTHER_LETTER); + + // marks + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.COMBINING_SPACING_MARK); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.NON_SPACING_MARK); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.ENCLOSING_MARK); + + // punctuation + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.CONNECTOR_PUNCTUATION); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.DASH_PUNCTUATION); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.START_PUNCTUATION); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.END_PUNCTUATION); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.INITIAL_QUOTE_PUNCTUATION); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.FINAL_QUOTE_PUNCTUATION); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.OTHER_PUNCTUATION); + + // symbols + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.MATH_SYMBOL); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.CURRENCY_SYMBOL); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.MODIFIER_SYMBOL); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.OTHER_SYMBOL); + + // separators + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.SPACE_SEPARATOR); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.LINE_SEPARATOR); + CHAR_TYPES_ALL_BUT_CONTROL.add((int) Character.PARAGRAPH_SEPARATOR); + } + + /** + * Set of character types including only decimals, upper and lower case letters. + */ + public static final Set CHAR_TYPES_BASIC_ALPHA; + + static { + CHAR_TYPES_BASIC_ALPHA = new HashSet(2); + CHAR_TYPES_BASIC_ALPHA.add((int) Character.UPPERCASE_LETTER); + CHAR_TYPES_BASIC_ALPHA.add((int) Character.LOWERCASE_LETTER); + } + + /** + * Set of character types including only decimals, upper and lower case letters. + */ + public static final Set CHAR_TYPES_BASIC_ALPHANUMERICS; + + static { + CHAR_TYPES_BASIC_ALPHANUMERICS = new HashSet(3); + CHAR_TYPES_BASIC_ALPHANUMERICS.add((int) Character.DECIMAL_DIGIT_NUMBER); + CHAR_TYPES_BASIC_ALPHANUMERICS.add((int) Character.UPPERCASE_LETTER); + CHAR_TYPES_BASIC_ALPHANUMERICS.add((int) Character.LOWERCASE_LETTER); + } + + /** + * Set of character types including only decimals, letter numbers, + * other numbers, upper, lower, title case as well as letter modifiers + * and other letters. + */ + public static final Set CHAR_TYPE_EXTENDED_ALPHANUMERICS; + + static { + CHAR_TYPE_EXTENDED_ALPHANUMERICS = new HashSet(8); + CHAR_TYPE_EXTENDED_ALPHANUMERICS.add((int) Character.DECIMAL_DIGIT_NUMBER); + CHAR_TYPE_EXTENDED_ALPHANUMERICS.add((int) Character.LETTER_NUMBER); + CHAR_TYPE_EXTENDED_ALPHANUMERICS.add((int) Character.OTHER_NUMBER); + CHAR_TYPE_EXTENDED_ALPHANUMERICS.add((int) Character.UPPERCASE_LETTER); + CHAR_TYPE_EXTENDED_ALPHANUMERICS.add((int) Character.LOWERCASE_LETTER); + CHAR_TYPE_EXTENDED_ALPHANUMERICS.add((int) Character.TITLECASE_LETTER); + CHAR_TYPE_EXTENDED_ALPHANUMERICS.add((int) Character.MODIFIER_LETTER); + CHAR_TYPE_EXTENDED_ALPHANUMERICS.add((int) Character.OTHER_LETTER); + } + + /** The character set to iterate over. */ + private final int[] characterSet; + + /** An array indices matching a position in the output string. */ + private int[] indices; + + /** The length of the output string in characters. */ + private final int length; + + /** The last value returned by the generator. Should be null if {@link #nextValue()} + * has not been called.*/ + private String lastValue; + + /** Whether or not to throw an exception when the string rolls over. */ + private boolean throwExceptionOnRollover; + + /** Whether or not the generator has rolled over. */ + private boolean hasRolledOver; + + /** + * Generates strings of 8 characters using only the upper and lower case alphabetical + * characters from the ASCII set. + */ + public IncrementingPrintableStringGenerator() { + this(DEFAULTSTRINGLENGTH, printableBasicAlphaASCIISet()); + } + + /** + * Generates strings of {@link #length} characters using only the upper and lower + * case alphabetical characters from the ASCII set. + * @param length The length of string to return from the generator. + * @throws IllegalArgumentException if the length is less than one. + */ + public IncrementingPrintableStringGenerator(final int length) { + this(length, printableBasicAlphaASCIISet()); + } + + /** + * Generates strings of {@link #length} characters using the code points in + * {@link #characterSet}. + * @param length The length of string to return from the generator. + * @param characterSet A set of code points to choose from. Code points in the + * set can be in any order, not necessarily lexical. + * @throws IllegalArgumentException if the length is less than one or the character + * set has fewer than one code points. + */ + public IncrementingPrintableStringGenerator(final int length, final int[] characterSet) { + if (length < 1) { + throw new IllegalArgumentException("Length must be greater than or equal to 1"); + } + if (characterSet == null || characterSet.length < 1) { + throw new IllegalArgumentException("Character set must have at least one character"); + } + this.length = length; + this.characterSet = characterSet; + indices = new int[length]; + } + + @Override + public String nextValue() { + if (hasRolledOver && throwExceptionOnRollover) { + throw new NoSuchElementException("The generator has rolled over to the beginning"); + } + + final StringBuilder buffer = new StringBuilder(length); + for (int i = 0; i < length; i++) { + buffer.append(Character.toChars(characterSet[indices[i]])); + } + + // increment the indices; + for (int i = length - 1; i >= 0; --i) { + if (indices[i] >= characterSet.length - 1) { + indices[i] = 0; + if (i == 0 || characterSet.length == 1 && lastValue != null) { + hasRolledOver = true; + } + } else { + ++indices[i]; + break; + } + } + + lastValue = buffer.toString(); + return lastValue; + } + + @Override + public String lastValue() { + return lastValue; + } + + /** @param exceptionOnRollover Whether or not to throw an exception on rollover. */ + public void setThrowExceptionOnRollover(final boolean exceptionOnRollover) { + this.throwExceptionOnRollover = exceptionOnRollover; + } + + /** @return Whether or not to throw an exception on rollover. */ + public boolean getThrowExceptionOnRollover() { + return throwExceptionOnRollover; + } + + /** + * Returns an array of printable code points with only the upper and lower + * case alphabetical characters from the basic ASCII set. + * @return An array of code points + */ + public static int[] printableBasicAlphaASCIISet() { + final List validCharacters = + generatePrintableCharacterSet(0, 127, null, false, CHAR_TYPES_BASIC_ALPHA); + final int[] characterSet = new int[validCharacters.size()]; + for (int i = 0; i < validCharacters.size(); i++) { + characterSet[i] = validCharacters.get(i); + } + return characterSet; + } + + /** + * Returns an array of printable code points with the upper and lower case + * alphabetical characters as well as the numeric values from the basic + * ASCII set. + * @return An array of code points + */ + public static int[] printableBasicAlphaNumericASCIISet() { + final List validCharacters = + generatePrintableCharacterSet(0, 127, null, false, CHAR_TYPES_BASIC_ALPHANUMERICS); + final int[] characterSet = new int[validCharacters.size()]; + for (int i = 0; i < validCharacters.size(); i++) { + characterSet[i] = validCharacters.get(i); + } + return characterSet; + } + + /** + * Returns an array of printable code points with the entire basic ASCII table, + * including spaces. Excludes new lines. + * @return An array of code points + */ + public static int[] fullPrintableBasicASCIISet() { + final List validCharacters = + generatePrintableCharacterSet(32, 127, null, false, null); + final int[] characterSet = new int[validCharacters.size()]; + for (int i = 0; i < validCharacters.size(); i++) { + characterSet[i] = validCharacters.get(i); + } + return characterSet; + } + + /** + * Returns an array of printable code points with the entire basic ASCII table, + * including spaces and new lines. + * @return An array of code points + */ + public static int[] fullPrintableBasicASCIISetWithNewlines() { + final List validCharacters = new ArrayList(); + validCharacters.add(10); // newline + validCharacters.addAll(generatePrintableCharacterSet(32, 127, null, false, null)); + final int[] characterSet = new int[validCharacters.size()]; + for (int i = 0; i < validCharacters.size(); i++) { + characterSet[i] = validCharacters.get(i); + } + return characterSet; + } + + /** + * Returns an array of printable code points the first plane of Unicode characters + * including only the alpha-numeric values. + * @return An array of code points + */ + public static int[] printableAlphaNumericPlaneZeroSet() { + final List validCharacters = + generatePrintableCharacterSet(0, 65535, null, false, CHAR_TYPES_BASIC_ALPHANUMERICS); + final int[] characterSet = new int[validCharacters.size()]; + for (int i = 0; i < validCharacters.size(); i++) { + characterSet[i] = validCharacters.get(i); + } + return characterSet; + } + + /** + * Returns an array of printable code points the first plane of Unicode characters + * including all printable characters. + * @return An array of code points + */ + public static int[] fullPrintablePlaneZeroSet() { + final List validCharacters = + generatePrintableCharacterSet(0, 65535, null, false, CHAR_TYPES_ALL_BUT_CONTROL); + final int[] characterSet = new int[validCharacters.size()]; + for (int i = 0; i < validCharacters.size(); i++) { + characterSet[i] = validCharacters.get(i); + } + return characterSet; + } + + /** + * Generates a list of code points based on a range and filters. + * These can be used for generating strings with various ASCII and/or + * Unicode printable character sets for use with DBs that may have + * character limitations. + *

+ * Note that control, surrogate, format, private use and unassigned + * code points are skipped. + * @param startCodePoint The starting code point, inclusive. + * @param lastCodePoint The final code point, inclusive. + * @param characterTypesFilter An optional set of allowable character + * types. See {@link Character} for types. + * @param isFilterAllowableList Determines whether the {@code allowableTypes} + * set is inclusive or exclusive. When true, only those code points that + * appear in the list will be included in the resulting set. Otherwise + * matching code points are excluded. + * @param allowableTypes An optional list of code points for inclusion or + * exclusion. + * @return A list of code points matching the given range and filters. The + * list may be empty but is guaranteed not to be null. + */ + public static List generatePrintableCharacterSet( + final int startCodePoint, + final int lastCodePoint, + final Set characterTypesFilter, + final boolean isFilterAllowableList, + final Set allowableTypes) { + + // since we don't know the final size of the allowable character list we + // start with a list then we'll flatten it to an array. + final List validCharacters = new ArrayList(lastCodePoint); + + for (int codePoint = startCodePoint; codePoint <= lastCodePoint; ++codePoint) { + if (allowableTypes != null && + !allowableTypes.contains(Character.getType(codePoint))) { + continue; + } else { + // skip control points, formats, surrogates, etc + final int type = Character.getType(codePoint); + if (type == Character.CONTROL || + type == Character.SURROGATE || + type == Character.FORMAT || + type == Character.PRIVATE_USE || + type == Character.UNASSIGNED) { + continue; + } + } + + if (characterTypesFilter != null) { + // if the filter is enabled then we need to make sure the code point + // is in the allowable list if it's a whitelist or that the code point + // is NOT in the list if it's a blacklist. + if ((isFilterAllowableList && !characterTypesFilter.contains(codePoint)) || + (characterTypesFilter.contains(codePoint))) { + continue; + } + } + + validCharacters.add(codePoint); + } + return validCharacters; + } + +} diff --git a/core/src/main/java/site/ycsb/generator/NumberGenerator.java b/core/src/main/java/site/ycsb/generator/NumberGenerator.java new file mode 100644 index 0000000..6b42641 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/NumberGenerator.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +/** + * A generator that is capable of generating numeric values. + * + */ +public abstract class NumberGenerator extends Generator { + private Number lastVal; + + /** + * Set the last value generated. NumberGenerator subclasses must use this call + * to properly set the last value, or the {@link #lastValue()} calls won't work. + */ + protected void setLastValue(Number last) { + lastVal = last; + } + + + @Override + public Number lastValue() { + return lastVal; + } + + /** + * Return the expected value (mean) of the values this generator will return. + */ + public abstract double mean(); +} diff --git a/core/src/main/java/site/ycsb/generator/RandomDiscreteTimestampGenerator.java b/core/src/main/java/site/ycsb/generator/RandomDiscreteTimestampGenerator.java new file mode 100644 index 0000000..da3b44a --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/RandomDiscreteTimestampGenerator.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2017 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 site.ycsb.generator; + +import java.util.concurrent.TimeUnit; + +import site.ycsb.Utils; + +/** + * A generator that picks from a discrete set of offsets from a base Unix Epoch + * timestamp that returns timestamps in a random order with the guarantee that + * each timestamp is only returned once. + *

+ * TODO - It would be best to implement some kind of psuedo non-repeating random + * generator for this as it's likely OK that some small percentage of values are + * repeated. For now we just generate all of the offsets in an array, shuffle + * it and then iterate over the array. + *

+ * Note that {@link #MAX_INTERVALS} defines a hard limit on the size of the + * offset array so that we don't completely blow out the heap. + *

+ * The constructor parameter {@code intervals} determines how many values will be + * returned by the generator. For example, if the {@code interval} is 60 and the + * {@code timeUnits} are set to {@link TimeUnit#SECONDS} and {@code intervals} + * is set to 60, then the consumer can call {@link #nextValue()} 60 times for + * timestamps within an hour. + */ +public class RandomDiscreteTimestampGenerator extends UnixEpochTimestampGenerator { + + /** A hard limit on the size of the offsets array to a void using too much heap. */ + public static final int MAX_INTERVALS = 16777216; + + /** The total number of intervals for this generator. */ + private final int intervals; + + // can't be primitives due to the generic params on the sort function :( + /** The array of generated offsets from the base time. */ + private final Integer[] offsets; + + /** The current index into the offsets array. */ + private int offsetIndex; + + /** + * Ctor that uses the current system time as current. + * @param interval The interval between timestamps. + * @param timeUnits The time units of the returned Unix Epoch timestamp (as well + * as the units for the interval). + * @param intervals The total number of intervals for the generator. + * @throws IllegalArgumentException if the intervals is larger than {@link #MAX_INTERVALS} + */ + public RandomDiscreteTimestampGenerator(final long interval, final TimeUnit timeUnits, + final int intervals) { + super(interval, timeUnits); + this.intervals = intervals; + offsets = new Integer[intervals]; + setup(); + } + + /** + * Ctor for supplying a starting timestamp. + * The interval between timestamps. + * @param timeUnits The time units of the returned Unix Epoch timestamp (as well + * as the units for the interval). + * @param startTimestamp The start timestamp to use. + * NOTE that this must match the time units used for the interval. + * If the units are in nanoseconds, provide a nanosecond timestamp {@code System.nanoTime()} + * or in microseconds, {@code System.nanoTime() / 1000} + * or in millis, {@code System.currentTimeMillis()} + * @param intervals The total number of intervals for the generator. + * @throws IllegalArgumentException if the intervals is larger than {@link #MAX_INTERVALS} + */ + public RandomDiscreteTimestampGenerator(final long interval, final TimeUnit timeUnits, + final long startTimestamp, final int intervals) { + super(interval, timeUnits, startTimestamp); + this.intervals = intervals; + offsets = new Integer[intervals]; + setup(); + } + + /** + * Generates the offsets and shuffles the array. + */ + private void setup() { + if (intervals > MAX_INTERVALS) { + throw new IllegalArgumentException("Too many intervals for the in-memory " + + "array. The limit is " + MAX_INTERVALS + "."); + } + offsetIndex = 0; + for (int i = 0; i < intervals; i++) { + offsets[i] = i; + } + Utils.shuffleArray(offsets); + } + + @Override + public Long nextValue() { + if (offsetIndex >= offsets.length) { + throw new IllegalStateException("Reached the end of the random timestamp " + + "intervals: " + offsetIndex); + } + lastTimestamp = currentTimestamp; + currentTimestamp = startTimestamp + (offsets[offsetIndex++] * getOffset(1)); + return currentTimestamp; + } +} \ No newline at end of file diff --git a/core/src/main/java/site/ycsb/generator/ScrambledZipfianGenerator.java b/core/src/main/java/site/ycsb/generator/ScrambledZipfianGenerator.java new file mode 100644 index 0000000..155a6cf --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/ScrambledZipfianGenerator.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +import site.ycsb.Utils; + +/** + * A generator of a zipfian distribution. It produces a sequence of items, such that some items are more popular than + * others, according to a zipfian distribution. When you construct an instance of this class, you specify the number + * of items in the set to draw from, either by specifying an itemcount (so that the sequence is of items from 0 to + * itemcount-1) or by specifying a min and a max (so that the sequence is of items from min to max inclusive). After + * you construct the instance, you can change the number of items by calling nextInt(itemcount) or nextLong(itemcount). + *

+ * Unlike @ZipfianGenerator, this class scatters the "popular" items across the itemspace. Use this, instead of + * @ZipfianGenerator, if you don't want the head of the distribution (the popular items) clustered together. + */ +public class ScrambledZipfianGenerator extends NumberGenerator { + public static final double ZETAN = 26.46902820178302; + public static final double USED_ZIPFIAN_CONSTANT = 0.99; + public static final long ITEM_COUNT = 10000000000L; + + private ZipfianGenerator gen; + private final long min, max, itemcount; + + /******************************* Constructors **************************************/ + + /** + * Create a zipfian generator for the specified number of items. + * + * @param items The number of items in the distribution. + */ + public ScrambledZipfianGenerator(long items) { + this(0, items - 1); + } + + /** + * Create a zipfian generator for items between min and max. + * + * @param min The smallest integer to generate in the sequence. + * @param max The largest integer to generate in the sequence. + */ + public ScrambledZipfianGenerator(long min, long max) { + this(min, max, ZipfianGenerator.ZIPFIAN_CONSTANT); + } + + /** + * Create a zipfian generator for the specified number of items using the specified zipfian constant. + * + * @param _items The number of items in the distribution. + * @param _zipfianconstant The zipfian constant to use. + */ + /* +// not supported, as the value of zeta depends on the zipfian constant, and we have only precomputed zeta for one +zipfian constant + public ScrambledZipfianGenerator(long _items, double _zipfianconstant) + { + this(0,_items-1,_zipfianconstant); + } +*/ + + /** + * Create a zipfian generator for items between min and max (inclusive) for the specified zipfian constant. If you + * use a zipfian constant other than 0.99, this will take a long time to complete because we need to recompute zeta. + * + * @param min The smallest integer to generate in the sequence. + * @param max The largest integer to generate in the sequence. + * @param zipfianconstant The zipfian constant to use. + */ + public ScrambledZipfianGenerator(long min, long max, double zipfianconstant) { + this.min = min; + this.max = max; + itemcount = this.max - this.min + 1; + if (zipfianconstant == USED_ZIPFIAN_CONSTANT) { + gen = new ZipfianGenerator(0, ITEM_COUNT, zipfianconstant, ZETAN); + } else { + gen = new ZipfianGenerator(0, ITEM_COUNT, zipfianconstant); + } + } + + /**************************************************************************************************/ + + /** + * Return the next long in the sequence. + */ + @Override + public Long nextValue() { + long ret = gen.nextValue(); + ret = min + Utils.fnvhash64(ret) % itemcount; + setLastValue(ret); + return ret; + } + + public static void main(String[] args) { + double newzetan = ZipfianGenerator.zetastatic(ITEM_COUNT, ZipfianGenerator.ZIPFIAN_CONSTANT); + System.out.println("zetan: " + newzetan); + System.exit(0); + + ScrambledZipfianGenerator gen = new ScrambledZipfianGenerator(10000); + + for (int i = 0; i < 1000000; i++) { + System.out.println("" + gen.nextValue()); + } + } + + /** + * since the values are scrambled (hopefully uniformly), the mean is simply the middle of the range. + */ + @Override + public double mean() { + return ((min) + max) / 2.0; + } +} diff --git a/core/src/main/java/site/ycsb/generator/SequentialGenerator.java b/core/src/main/java/site/ycsb/generator/SequentialGenerator.java new file mode 100644 index 0000000..39ed82e --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/SequentialGenerator.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2016-2017 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 site.ycsb.generator; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Generates a sequence of integers 0, 1, ... + */ +public class SequentialGenerator extends NumberGenerator { + private final AtomicLong counter; + private long interval; + private long countstart; + + /** + * Create a counter that starts at countstart. + */ + public SequentialGenerator(long countstart, long countend) { + counter = new AtomicLong(); + setLastValue(counter.get()); + this.countstart = countstart; + interval = countend - countstart + 1; + } + + /** + * If the generator returns numeric (long) values, return the next value as an long. + * Default is to return -1, which is appropriate for generators that do not return numeric values. + */ + public long nextLong() { + long ret = countstart + counter.getAndIncrement() % interval; + setLastValue(ret); + return ret; + } + + @Override + public Number nextValue() { + long ret = countstart + counter.getAndIncrement() % interval; + setLastValue(ret); + return ret; + } + + @Override + public Number lastValue() { + return counter.get() + 1; + } + + @Override + public double mean() { + throw new UnsupportedOperationException("Can't compute mean of non-stationary distribution!"); + } +} diff --git a/core/src/main/java/site/ycsb/generator/SkewedLatestGenerator.java b/core/src/main/java/site/ycsb/generator/SkewedLatestGenerator.java new file mode 100644 index 0000000..ec18efe --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/SkewedLatestGenerator.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +/** + * Generate a popularity distribution of items, skewed to favor recent items significantly more than older items. + */ +public class SkewedLatestGenerator extends NumberGenerator { + private CounterGenerator basis; + private final ZipfianGenerator zipfian; + + public SkewedLatestGenerator(CounterGenerator basis) { + this.basis = basis; + zipfian = new ZipfianGenerator(this.basis.lastValue()); + nextValue(); + } + + /** + * Generate the next string in the distribution, skewed Zipfian favoring the items most recently returned by + * the basis generator. + */ + @Override + public Long nextValue() { + long max = basis.lastValue(); + long next = max - zipfian.nextLong(max); + setLastValue(next); + return next; + } + + public static void main(String[] args) { + SkewedLatestGenerator gen = new SkewedLatestGenerator(new CounterGenerator(1000)); + for (int i = 0; i < Integer.parseInt(args[0]); i++) { + System.out.println(gen.nextString()); + } + } + + @Override + public double mean() { + throw new UnsupportedOperationException("Can't compute mean of non-stationary distribution!"); + } +} diff --git a/core/src/main/java/site/ycsb/generator/UniformGenerator.java b/core/src/main/java/site/ycsb/generator/UniformGenerator.java new file mode 100644 index 0000000..b8731f6 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/UniformGenerator.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. Copyright (c) 2017 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 site.ycsb.generator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * An expression that generates a random value in the specified range. + */ +public class UniformGenerator extends Generator { + private final List values; + private String laststring; + private final UniformLongGenerator gen; + + /** + * Creates a generator that will return strings from the specified set uniformly randomly. + */ + public UniformGenerator(Collection values) { + this.values = new ArrayList<>(values); + laststring = null; + gen = new UniformLongGenerator(0, values.size() - 1); + } + + /** + * Generate the next string in the distribution. + */ + @Override + public String nextValue() { + laststring = values.get(gen.nextValue().intValue()); + return laststring; + } + + /** + * Return the previous string generated by the distribution; e.g., returned from the last nextString() call. + * Calling lastString() should not advance the distribution or have any side effects. If nextString() has not yet + * been called, lastString() should return something reasonable. + */ + @Override + public String lastValue() { + if (laststring == null) { + nextValue(); + } + return laststring; + } +} + diff --git a/core/src/main/java/site/ycsb/generator/UniformLongGenerator.java b/core/src/main/java/site/ycsb/generator/UniformLongGenerator.java new file mode 100644 index 0000000..d369538 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/UniformLongGenerator.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. Copyright (c) 2017 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 site.ycsb.generator; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Generates longs randomly uniform from an interval. + */ +public class UniformLongGenerator extends NumberGenerator { + private final long lb, ub, interval; + + /** + * Creates a generator that will return longs uniformly randomly from the + * interval [lb,ub] inclusive (that is, lb and ub are possible values) + * (lb and ub are possible values). + * + * @param lb the lower bound (inclusive) of generated values + * @param ub the upper bound (inclusive) of generated values + */ + public UniformLongGenerator(long lb, long ub) { + this.lb = lb; + this.ub = ub; + interval = this.ub - this.lb + 1; + } + + @Override + public Long nextValue() { + long ret = Math.abs(ThreadLocalRandom.current().nextLong()) % interval + lb; + setLastValue(ret); + + return ret; + } + + @Override + public double mean() { + return ((lb + (long) ub)) / 2.0; + } +} diff --git a/core/src/main/java/site/ycsb/generator/UnixEpochTimestampGenerator.java b/core/src/main/java/site/ycsb/generator/UnixEpochTimestampGenerator.java new file mode 100644 index 0000000..47a6101 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/UnixEpochTimestampGenerator.java @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2016-2017 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 site.ycsb.generator; + +import java.util.concurrent.TimeUnit; + +/** + * A generator that produces Unix epoch timestamps in seconds, milli, micro or + * nanoseconds and increments the stamp a given interval each time + * {@link #nextValue()} is called. The result is emitted as a long in the same + * way calls to {@code System.currentTimeMillis()} and + * {@code System.nanoTime()} behave. + *

+ * By default, the current system time of the host is used as the starting + * timestamp. Calling {@link #initalizeTimestamp(long)} can adjust the timestamp + * back or forward in time. For example, if a workload will generate an hour of + * data at 1 minute intervals, then to set the start timestamp an hour in the past + * from the current run, use: + *

{@code
+ * UnixEpochTimestampGenerator generator = new UnixEpochTimestampGenerator();
+ * generator.initalizeTimestamp(-60);
+ * }
+ * A constructor is also present for setting an explicit start time. + * Negative intervals are supported as well for iterating back in time. + *

+ * WARNING: This generator is not thread safe and should not called from multiple + * threads. + */ +public class UnixEpochTimestampGenerator extends Generator { + + /** The base timestamp used as a starting reference. */ + protected long startTimestamp; + + /** The current timestamp that will be incremented. */ + protected long currentTimestamp; + + /** The last used timestamp. Should always be one interval behind current. */ + protected long lastTimestamp; + + /** The interval to increment by. Multiplied by {@link #timeUnits}. */ + protected long interval; + + /** The units of time the interval represents. */ + protected TimeUnit timeUnits; + + /** + * Default ctor with the current system time and a 60 second interval. + */ + public UnixEpochTimestampGenerator() { + this(60, TimeUnit.SECONDS); + } + + /** + * Ctor that uses the current system time as current. + * @param interval The interval for incrementing the timestamp. + * @param timeUnits The units of time the increment represents. + */ + public UnixEpochTimestampGenerator(final long interval, final TimeUnit timeUnits) { + this.interval = interval; + this.timeUnits = timeUnits; + // move the first timestamp by 1 interval so that the first call to nextValue + // returns this timestamp + initalizeTimestamp(-1); + currentTimestamp -= getOffset(1); + lastTimestamp = currentTimestamp; + } + + /** + * Ctor for supplying a starting timestamp. + * @param interval The interval for incrementing the timestamp. + * @param timeUnits The units of time the increment represents. + * @param startTimestamp The start timestamp to use. + * NOTE that this must match the time units used for the interval. + * If the units are in nanoseconds, provide a nanosecond timestamp {@code System.nanoTime()} + * or in microseconds, {@code System.nanoTime() / 1000} + * or in millis, {@code System.currentTimeMillis()} + * or seconds and any interval above, {@code System.currentTimeMillis() / 1000} + */ + public UnixEpochTimestampGenerator(final long interval, final TimeUnit timeUnits, + final long startTimestamp) { + this.interval = interval; + this.timeUnits = timeUnits; + // move the first timestamp by 1 interval so that the first call to nextValue + // returns this timestamp + currentTimestamp = startTimestamp - getOffset(1); + this.startTimestamp = currentTimestamp; + lastTimestamp = currentTimestamp - getOffset(1); + } + + /** + * Sets the starting timestamp to the current system time plus the interval offset. + * E.g. to set the time an hour in the past, supply a value of {@code -60}. + * @param intervalOffset The interval to increment or decrement by. + */ + public void initalizeTimestamp(final long intervalOffset) { + switch (timeUnits) { + case NANOSECONDS: + currentTimestamp = System.nanoTime() + getOffset(intervalOffset); + break; + case MICROSECONDS: + currentTimestamp = (System.nanoTime() / 1000) + getOffset(intervalOffset); + break; + case MILLISECONDS: + currentTimestamp = System.currentTimeMillis() + getOffset(intervalOffset); + break; + case SECONDS: + currentTimestamp = (System.currentTimeMillis() / 1000) + + getOffset(intervalOffset); + break; + case MINUTES: + currentTimestamp = (System.currentTimeMillis() / 1000) + + getOffset(intervalOffset); + break; + case HOURS: + currentTimestamp = (System.currentTimeMillis() / 1000) + + getOffset(intervalOffset); + break; + case DAYS: + currentTimestamp = (System.currentTimeMillis() / 1000) + + getOffset(intervalOffset); + break; + default: + throw new IllegalArgumentException("Unhandled time unit type: " + timeUnits); + } + startTimestamp = currentTimestamp; + } + + @Override + public Long nextValue() { + lastTimestamp = currentTimestamp; + currentTimestamp += getOffset(1); + return currentTimestamp; + } + + /** + * Returns the proper increment offset to use given the interval and timeunits. + * @param intervalOffset The amount of offset to multiply by. + * @return An offset value to adjust the timestamp by. + */ + public long getOffset(final long intervalOffset) { + switch (timeUnits) { + case NANOSECONDS: + case MICROSECONDS: + case MILLISECONDS: + case SECONDS: + return intervalOffset * interval; + case MINUTES: + return intervalOffset * interval * (long) 60; + case HOURS: + return intervalOffset * interval * (long) (60 * 60); + case DAYS: + return intervalOffset * interval * (long) (60 * 60 * 24); + default: + throw new IllegalArgumentException("Unhandled time unit type: " + timeUnits); + } + } + + @Override + public Long lastValue() { + return lastTimestamp; + } + + /** @return The current timestamp as set by the last call to {@link #nextValue()} */ + public long currentValue() { + return currentTimestamp; + } + +} \ No newline at end of file diff --git a/core/src/main/java/site/ycsb/generator/ZipfianGenerator.java b/core/src/main/java/site/ycsb/generator/ZipfianGenerator.java new file mode 100644 index 0000000..5e45eaa --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/ZipfianGenerator.java @@ -0,0 +1,288 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.generator; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * A generator of a zipfian distribution. It produces a sequence of items, such that some items are more popular than + * others, according to a zipfian distribution. When you construct an instance of this class, you specify the number + * of items in the set to draw from, either by specifying an itemcount (so that the sequence is of items from 0 to + * itemcount-1) or by specifying a min and a max (so that the sequence is of items from min to max inclusive). After + * you construct the instance, you can change the number of items by calling nextInt(itemcount) or nextLong(itemcount). + * + * Note that the popular items will be clustered together, e.g. item 0 is the most popular, item 1 the second most + * popular, and so on (or min is the most popular, min+1 the next most popular, etc.) If you don't want this clustering, + * and instead want the popular items scattered throughout the item space, then use ScrambledZipfianGenerator instead. + * + * Be aware: initializing this generator may take a long time if there are lots of items to choose from (e.g. over a + * minute for 100 million objects). This is because certain mathematical values need to be computed to properly + * generate a zipfian skew, and one of those values (zeta) is a sum sequence from 1 to n, where n is the itemcount. + * Note that if you increase the number of items in the set, we can compute a new zeta incrementally, so it should be + * fast unless you have added millions of items. However, if you decrease the number of items, we recompute zeta from + * scratch, so this can take a long time. + * + * The algorithm used here is from "Quickly Generating Billion-Record Synthetic Databases", Jim Gray et al, SIGMOD 1994. + */ +public class ZipfianGenerator extends NumberGenerator { + public static final double ZIPFIAN_CONSTANT = 0.99; + + /** + * Number of items. + */ + private final long items; + + /** + * Min item to generate. + */ + private final long base; + + /** + * The zipfian constant to use. + */ + private final double zipfianconstant; + + /** + * Computed parameters for generating the distribution. + */ + private double alpha, zetan, eta, theta, zeta2theta; + + /** + * The number of items used to compute zetan the last time. + */ + private long countforzeta; + + /** + * Flag to prevent problems. If you increase the number of items the zipfian generator is allowed to choose from, + * this code will incrementally compute a new zeta value for the larger itemcount. However, if you decrease the + * number of items, the code computes zeta from scratch; this is expensive for large itemsets. + * Usually this is not intentional; e.g. one thread thinks the number of items is 1001 and calls "nextLong()" with + * that item count; then another thread who thinks the number of items is 1000 calls nextLong() with itemcount=1000 + * triggering the expensive recomputation. (It is expensive for 100 million items, not really for 1000 items.) Why + * did the second thread think there were only 1000 items? maybe it read the item count before the first thread + * incremented it. So this flag allows you to say if you really do want that recomputation. If true, then the code + * will recompute zeta if the itemcount goes down. If false, the code will assume itemcount only goes up, and never + * recompute. + */ + private boolean allowitemcountdecrease = false; + + /******************************* Constructors **************************************/ + + /** + * Create a zipfian generator for the specified number of items. + * @param items The number of items in the distribution. + */ + public ZipfianGenerator(long items) { + this(0, items - 1); + } + + /** + * Create a zipfian generator for items between min and max. + * @param min The smallest integer to generate in the sequence. + * @param max The largest integer to generate in the sequence. + */ + public ZipfianGenerator(long min, long max) { + this(min, max, ZIPFIAN_CONSTANT); + } + + /** + * Create a zipfian generator for the specified number of items using the specified zipfian constant. + * + * @param items The number of items in the distribution. + * @param zipfianconstant The zipfian constant to use. + */ + public ZipfianGenerator(long items, double zipfianconstant) { + this(0, items - 1, zipfianconstant); + } + + /** + * Create a zipfian generator for items between min and max (inclusive) for the specified zipfian constant. + * @param min The smallest integer to generate in the sequence. + * @param max The largest integer to generate in the sequence. + * @param zipfianconstant The zipfian constant to use. + */ + public ZipfianGenerator(long min, long max, double zipfianconstant) { + this(min, max, zipfianconstant, zetastatic(max - min + 1, zipfianconstant)); + } + + /** + * Create a zipfian generator for items between min and max (inclusive) for the specified zipfian constant, using + * the precomputed value of zeta. + * + * @param min The smallest integer to generate in the sequence. + * @param max The largest integer to generate in the sequence. + * @param zipfianconstant The zipfian constant to use. + * @param zetan The precomputed zeta constant. + */ + public ZipfianGenerator(long min, long max, double zipfianconstant, double zetan) { + + items = max - min + 1; + base = min; + this.zipfianconstant = zipfianconstant; + + theta = this.zipfianconstant; + + zeta2theta = zeta(2, theta); + + alpha = 1.0 / (1.0 - theta); + this.zetan = zetan; + countforzeta = items; + eta = (1 - Math.pow(2.0 / items, 1 - theta)) / (1 - zeta2theta / this.zetan); + + nextValue(); + } + + /**************************************************************************/ + + /** + * Compute the zeta constant needed for the distribution. Do this from scratch for a distribution with n items, + * using the zipfian constant thetaVal. Remember the value of n, so if we change the itemcount, we can recompute zeta. + * + * @param n The number of items to compute zeta over. + * @param thetaVal The zipfian constant. + */ + double zeta(long n, double thetaVal) { + countforzeta = n; + return zetastatic(n, thetaVal); + } + + /** + * Compute the zeta constant needed for the distribution. Do this from scratch for a distribution with n items, + * using the zipfian constant theta. This is a static version of the function which will not remember n. + * @param n The number of items to compute zeta over. + * @param theta The zipfian constant. + */ + static double zetastatic(long n, double theta) { + return zetastatic(0, n, theta, 0); + } + + /** + * Compute the zeta constant needed for the distribution. Do this incrementally for a distribution that + * has n items now but used to have st items. Use the zipfian constant thetaVal. Remember the new value of + * n so that if we change the itemcount, we'll know to recompute zeta. + * + * @param st The number of items used to compute the last initialsum + * @param n The number of items to compute zeta over. + * @param thetaVal The zipfian constant. + * @param initialsum The value of zeta we are computing incrementally from. + */ + double zeta(long st, long n, double thetaVal, double initialsum) { + countforzeta = n; + return zetastatic(st, n, thetaVal, initialsum); + } + + /** + * Compute the zeta constant needed for the distribution. Do this incrementally for a distribution that + * has n items now but used to have st items. Use the zipfian constant theta. Remember the new value of + * n so that if we change the itemcount, we'll know to recompute zeta. + * @param st The number of items used to compute the last initialsum + * @param n The number of items to compute zeta over. + * @param theta The zipfian constant. + * @param initialsum The value of zeta we are computing incrementally from. + */ + static double zetastatic(long st, long n, double theta, double initialsum) { + double sum = initialsum; + for (long i = st; i < n; i++) { + + sum += 1 / (Math.pow(i + 1, theta)); + } + + //System.out.println("countforzeta="+countforzeta); + + return sum; + } + + /****************************************************************************************/ + + + /** + * Generate the next item as a long. + * + * @param itemcount The number of items in the distribution. + * @return The next item in the sequence. + */ + long nextLong(long itemcount) { + //from "Quickly Generating Billion-Record Synthetic Databases", Jim Gray et al, SIGMOD 1994 + + if (itemcount != countforzeta) { + + //have to recompute zetan and eta, since they depend on itemcount + synchronized (this) { + if (itemcount > countforzeta) { + //System.err.println("WARNING: Incrementally recomputing Zipfian distribtion. (itemcount="+itemcount+" + // countforzeta="+countforzeta+")"); + + //we have added more items. can compute zetan incrementally, which is cheaper + zetan = zeta(countforzeta, itemcount, theta, zetan); + eta = (1 - Math.pow(2.0 / items, 1 - theta)) / (1 - zeta2theta / zetan); + } else if ((itemcount < countforzeta) && (allowitemcountdecrease)) { + //have to start over with zetan + //note : for large itemsets, this is very slow. so don't do it! + + //TODO: can also have a negative incremental computation, e.g. if you decrease the number of items, + // then just subtract the zeta sequence terms for the items that went away. This would be faster than + // recomputing from scratch when the number of items decreases + + System.err.println("WARNING: Recomputing Zipfian distribtion. This is slow and should be avoided. " + + "(itemcount=" + itemcount + " countforzeta=" + countforzeta + ")"); + + zetan = zeta(itemcount, theta); + eta = (1 - Math.pow(2.0 / items, 1 - theta)) / (1 - zeta2theta / zetan); + } + } + } + + double u = ThreadLocalRandom.current().nextDouble(); + double uz = u * zetan; + + if (uz < 1.0) { + return base; + } + + if (uz < 1.0 + Math.pow(0.5, theta)) { + return base + 1; + } + + long ret = base + (long) ((itemcount) * Math.pow(eta * u - eta + 1, alpha)); + setLastValue(ret); + return ret; + } + + /** + * Return the next value, skewed by the Zipfian distribution. The 0th item will be the most popular, followed by + * the 1st, followed by the 2nd, etc. (Or, if min != 0, the min-th item is the most popular, the min+1th item the + * next most popular, etc.) If you want the popular items scattered throughout the item space, use + * ScrambledZipfianGenerator instead. + */ + @Override + public Long nextValue() { + return nextLong(items); + } + + public static void main(String[] args) { + new ZipfianGenerator(ScrambledZipfianGenerator.ITEM_COUNT); + } + + /** + * @todo Implement ZipfianGenerator.mean() + */ + @Override + public double mean() { + throw new UnsupportedOperationException("@todo implement ZipfianGenerator.mean()"); + } +} diff --git a/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgeGeneratorFactory.java b/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgeGeneratorFactory.java new file mode 100644 index 0000000..dee8245 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgeGeneratorFactory.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.generator.acknowledge; + +public final class AcknowledgeGeneratorFactory { + + private AcknowledgeGeneratorFactory() { + // private constructor + } +} diff --git a/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgeTester.java b/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgeTester.java new file mode 100644 index 0000000..85d1298 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgeTester.java @@ -0,0 +1,31 @@ +package site.ycsb.generator.acknowledge; + +public class AcknowledgeTester { + + public static void main(String[] args) { + System.out.println("starting"); + DefaultAcknowledgedCounterGenerator am = new DefaultAcknowledgedCounterGenerator(10500000000L); + long l = am.lastValue(); + System.out.println("getting values"); + for(long i = 0; i < (1 << 40); i++) { + am.nextValue(); + } + System.out.println("acknowledging pt1"); + for(long i = 0; i < (1 << 10); i++) { + am.acknowledge(l + i); + } + System.out.println("acknowledging pt2"); + for(long i = (1 << 30); i < (1 << 40); i++) { + am.acknowledge(l + i); + } + System.out.println("acknowledging pt3"); + for(long i = (1 << 20); i < (1 << 30); i++) { + am.acknowledge(l + i); + } + System.out.println("acknowledging pt4"); + for(long i = (1 << 10); i < (1 << 20); i++) { + am.acknowledge(l + i); + } + System.out.println("done"); + } +} diff --git a/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgedCounterGenerator.java b/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgedCounterGenerator.java new file mode 100644 index 0000000..daab31e --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/acknowledge/AcknowledgedCounterGenerator.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.generator.acknowledge; + +import site.ycsb.generator.CounterGenerator; + +public abstract class AcknowledgedCounterGenerator extends CounterGenerator { + protected AcknowledgedCounterGenerator(long countstart) { + super(countstart); + } + public abstract void acknowledge(long value); +} diff --git a/core/src/main/java/site/ycsb/generator/acknowledge/DefaultAcknowledgedCounterGenerator.java b/core/src/main/java/site/ycsb/generator/acknowledge/DefaultAcknowledgedCounterGenerator.java new file mode 100644 index 0000000..f44ef15 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/acknowledge/DefaultAcknowledgedCounterGenerator.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015-2017 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 site.ycsb.generator.acknowledge; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A CounterGenerator that reports generated integers via lastInt() + * only after they have been acknowledged. + */ +public class DefaultAcknowledgedCounterGenerator extends AcknowledgedCounterGenerator { + /** The size of the window of pending id ack's. 2^20 = {@value} */ + static final int DEFAUL_WINDOW_SIZE = Integer.rotateLeft(1, 17); + + private final ConcurrentHashMap mapping = new ConcurrentHashMap<>(); + private final ReentrantLock lock; + private final boolean[] window; + private volatile long limit; + private final int windowSize; + /** The mask to use to turn an id into a slot in {@link #window}. */ + private final int windowMask; + + /** + * Create a counter that starts at countstart. + */ + public DefaultAcknowledgedCounterGenerator(long countstart, int windowSize) { + super(countstart); + if(windowSize < 1) throw new IllegalArgumentException("windo must be positive"); + this.windowSize = windowSize; + lock = new ReentrantLock(); + window = new boolean[this.windowSize]; + windowMask = this.windowSize - 1; + limit = countstart - 1; + System.err.println("starting AcknowledgedCounterGenerator with limit " + limit); + } + + /** + * Create a counter that starts at countstart. + */ + public DefaultAcknowledgedCounterGenerator(long countstart) { + this(countstart, DEFAUL_WINDOW_SIZE); + } + + /** + * In this generator, the highest acknowledged counter value + * (as opposed to the highest generated counter value). + */ + @Override + public Long lastValue() { + return limit; + } + @Override + public Long nextValue() { + Long l = super.nextValue(); + mapping.put(l, Thread.currentThread().getName()); + if(l - limit > this.windowSize) { + System.err.println("given out more elements than fit into window. Where did they go? Limit is " + limit); + } + return l; + } + + /** + * Make a generated counter value available via lastInt(). + */ + public void acknowledge(long value) { + final int currentSlot = (int)(value & this.windowMask); + if (window[currentSlot]) { + System.err.println(mapping); + System.err.println(lock.isLocked() + " --- " + lock.getHoldCount() + " --- " + lock.getQueueLength()); + Map map = Thread.getAllStackTraces(); + Exception e = new Exception(); + for(Thread t : map.keySet()) { + System.err.println("thread: " + t.getName()); + e.setStackTrace(map.get(t)); + e.printStackTrace(System.err); + } + throw new RuntimeException("Too many unacknowledged insertion keys. Limit is " + limit); + } + mapping.remove(value); + window[currentSlot] = true; + + if (lock.tryLock()) { + // long start = System.currentTimeMillis(); + // move a contiguous sequence from the window + // over to the "limit" variable + try { + // Only loop through the entire window at most once. + long beforeFirstSlot = (limit & this.windowMask); + long index; + for (index = limit + 1; index != beforeFirstSlot; ++index) { + int slot = (int)(index & this.windowMask); + if (!window[slot]) { + break; + } + window[slot] = false; + } + limit = index - 1; + } finally { + lock.unlock(); + /* long end = System.currentTimeMillis(); + if(end - start > 0) { + System.out.println("cleanup took: " + (end -start) + " milliseconds "); + } + */ + } + } + } +} diff --git a/core/src/main/java/site/ycsb/generator/acknowledge/StupidAcknowledgedCounterGenerator.java b/core/src/main/java/site/ycsb/generator/acknowledge/StupidAcknowledgedCounterGenerator.java new file mode 100644 index 0000000..30d8f09 --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/acknowledge/StupidAcknowledgedCounterGenerator.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2023-2024 benchANT GmbH. 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 site.ycsb.generator.acknowledge; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A CounterGenerator that reports naively reports the + * highest acknowledged id without considering holes in + * the sequence. This is sufficient for bulk loads. + */ +public class StupidAcknowledgedCounterGenerator extends AcknowledgedCounterGenerator { + + private final AtomicLong limit; + + /** + * Create a counter that starts at countstart. + */ + public StupidAcknowledgedCounterGenerator(long countstart) { + super(countstart); + limit = new AtomicLong(countstart - 1); + } + + /** + * In this generator, the highest acknowledged counter value + * (as opposed to the highest generated counter value). + */ + @Override + public Long lastValue() { + return limit.get(); + } + + /** + * Make a generated counter value available via lastInt(). + */ + public void acknowledge(long value) { + while(true) { + long l = limit.get(); + if(l < value) { + boolean b = limit.compareAndSet(l, value); + if(!b) continue; + } + return; + } + } +} diff --git a/core/src/main/java/site/ycsb/generator/package-info.java b/core/src/main/java/site/ycsb/generator/package-info.java new file mode 100644 index 0000000..853437f --- /dev/null +++ b/core/src/main/java/site/ycsb/generator/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017 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. + */ + +/** + * The YCSB generator package. + */ +package site.ycsb.generator; + diff --git a/core/src/main/java/site/ycsb/measurements/Measurements.java b/core/src/main/java/site/ycsb/measurements/Measurements.java new file mode 100644 index 0000000..b6e68bb --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/Measurements.java @@ -0,0 +1,281 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2020 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 site.ycsb.measurements; + +import site.ycsb.Status; +import site.ycsb.measurements.exporter.MeasurementsExporter; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Collects latency measurements, and reports them when requested. + */ +public class Measurements { + /** + * All supported measurement types are defined in this enum. + */ + public enum MeasurementType { + HISTOGRAM, + HDRHISTOGRAM, + HDRHISTOGRAM_AND_HISTOGRAM, + HDRHISTOGRAM_AND_RAW, + TIMESERIES, + RAW + } + + public static final String MEASUREMENT_TYPE_PROPERTY = "measurementtype"; + private static final String MEASUREMENT_TYPE_PROPERTY_DEFAULT = "hdrhistogram"; + + public static final String MEASUREMENT_INTERVAL = "measurement.interval"; + private static final String MEASUREMENT_INTERVAL_DEFAULT = "op"; + + public static final String MEASUREMENT_TRACK_JVM_PROPERTY = "measurement.trackjvm"; + public static final String MEASUREMENT_TRACK_JVM_PROPERTY_DEFAULT = "false"; + + private static Measurements singleton = null; + private static Properties measurementproperties = null; + + public static void setProperties(Properties props) { + measurementproperties = props; + } + + /** + * Return the singleton Measurements object. + */ + public static synchronized Measurements getMeasurements() { + if (singleton == null) { + singleton = new Measurements(measurementproperties); + } + return singleton; + } + + private final ConcurrentHashMap opToMesurementMap; + private final ConcurrentHashMap opToIntendedMesurementMap; + private final MeasurementType measurementType; + private final int measurementInterval; + private final Properties props; + + /** + * Create a new object with the specified properties. + */ + public Measurements(Properties props) { + opToMesurementMap = new ConcurrentHashMap<>(); + opToIntendedMesurementMap = new ConcurrentHashMap<>(); + + this.props = props; + + String mTypeString = this.props.getProperty(MEASUREMENT_TYPE_PROPERTY, MEASUREMENT_TYPE_PROPERTY_DEFAULT); + switch (mTypeString) { + case "histogram": + measurementType = MeasurementType.HISTOGRAM; + break; + case "hdrhistogram": + measurementType = MeasurementType.HDRHISTOGRAM; + break; + case "hdrhistogram+histogram": + measurementType = MeasurementType.HDRHISTOGRAM_AND_HISTOGRAM; + break; + case "hdrhistogram+raw": + measurementType = MeasurementType.HDRHISTOGRAM_AND_RAW; + break; + case "timeseries": + measurementType = MeasurementType.TIMESERIES; + break; + case "raw": + measurementType = MeasurementType.RAW; + break; + default: + throw new IllegalArgumentException("unknown " + MEASUREMENT_TYPE_PROPERTY + "=" + mTypeString); + } + + String mIntervalString = this.props.getProperty(MEASUREMENT_INTERVAL, MEASUREMENT_INTERVAL_DEFAULT); + switch (mIntervalString) { + case "op": + measurementInterval = 0; + break; + case "intended": + measurementInterval = 1; + break; + case "both": + measurementInterval = 2; + break; + default: + throw new IllegalArgumentException("unknown " + MEASUREMENT_INTERVAL + "=" + mIntervalString); + } + } + + private OneMeasurement constructOneMeasurement(String name) { + switch (measurementType) { + case HISTOGRAM: + return new OneMeasurementHistogram(name, props); + case HDRHISTOGRAM: + return new OneMeasurementHdrHistogram(name, props); + case HDRHISTOGRAM_AND_HISTOGRAM: + return new TwoInOneMeasurement(name, + new OneMeasurementHdrHistogram("Hdr" + name, props), + new OneMeasurementHistogram("Bucket" + name, props)); + case HDRHISTOGRAM_AND_RAW: + return new TwoInOneMeasurement(name, + new OneMeasurementHdrHistogram("Hdr" + name, props), + new OneMeasurementRaw("Raw" + name, props)); + case TIMESERIES: + return new OneMeasurementTimeSeries(name, props); + case RAW: + return new OneMeasurementRaw(name, props); + default: + throw new AssertionError("Impossible to be here. Dead code reached. Bugs?"); + } + } + + static class StartTimeHolder { + protected long time; + + long startTime() { + if (time == 0) { + return System.nanoTime(); + } else { + return time; + } + } + } + + private final ThreadLocal tlIntendedStartTime = new ThreadLocal() { + protected StartTimeHolder initialValue() { + return new StartTimeHolder(); + } + }; + + public void setIntendedStartTimeNs(long time) { + if (measurementInterval == 0) { + return; + } + tlIntendedStartTime.get().time = time; + } + + public long getIntendedStartTimeNs() { + if (measurementInterval == 0) { + return 0L; + } + return tlIntendedStartTime.get().startTime(); + } + + /** + * Report a single value of a single metric. E.g. for read latency, operation="READ" and latency is the measured + * value. + */ + public void measure(String operation, int latency) { + if (measurementInterval == 1) { + return; + } + try { + OneMeasurement m = getOpMeasurement(operation); + m.measure(latency); + } catch (java.lang.ArrayIndexOutOfBoundsException e) { + // This seems like a terribly hacky way to cover up for a bug in the measurement code + System.out.println("ERROR: java.lang.ArrayIndexOutOfBoundsException - ignoring and continuing"); + e.printStackTrace(); + e.printStackTrace(System.out); + } + } + + /** + * Report a single value of a single metric. E.g. for read latency, operation="READ" and latency is the measured + * value. + */ + public void measureIntended(String operation, int latency) { + if (measurementInterval == 0) { + return; + } + try { + OneMeasurement m = getOpIntendedMeasurement(operation); + m.measure(latency); + } catch (java.lang.ArrayIndexOutOfBoundsException e) { + // This seems like a terribly hacky way to cover up for a bug in the measurement code + System.out.println("ERROR: java.lang.ArrayIndexOutOfBoundsException - ignoring and continuing"); + e.printStackTrace(); + e.printStackTrace(System.out); + } + } + + private OneMeasurement getOpMeasurement(String operation) { + OneMeasurement m = opToMesurementMap.get(operation); + if (m == null) { + m = constructOneMeasurement(operation); + OneMeasurement oldM = opToMesurementMap.putIfAbsent(operation, m); + if (oldM != null) { + m = oldM; + } + } + return m; + } + + private OneMeasurement getOpIntendedMeasurement(String operation) { + OneMeasurement m = opToIntendedMesurementMap.get(operation); + if (m == null) { + final String name = measurementInterval == 1 ? operation : "Intended-" + operation; + m = constructOneMeasurement(name); + OneMeasurement oldM = opToIntendedMesurementMap.putIfAbsent(operation, m); + if (oldM != null) { + m = oldM; + } + } + return m; + } + + /** + * Report a return code for a single DB operation. + */ + public void reportStatus(final String operation, final Status status) { + OneMeasurement m = measurementInterval == 1 ? + getOpIntendedMeasurement(operation) : + getOpMeasurement(operation); + m.reportStatus(status); + } + + /** + * Export the current measurements to a suitable format. + * + * @param exporter Exporter representing the type of format to write to. + * @throws IOException Thrown if the export failed. + */ + public void exportMeasurements(MeasurementsExporter exporter) throws IOException { + for (OneMeasurement measurement : opToMesurementMap.values()) { + measurement.exportMeasurements(exporter); + } + for (OneMeasurement measurement : opToIntendedMesurementMap.values()) { + measurement.exportMeasurements(exporter); + } + } + + /** + * Return a one line summary of the measurements. + */ + public synchronized String getSummary() { + String ret = ""; + for (OneMeasurement m : opToMesurementMap.values()) { + ret += m.getSummary() + " "; + } + for (OneMeasurement m : opToIntendedMesurementMap.values()) { + ret += m.getSummary() + " "; + } + return ret; + } + +} diff --git a/core/src/main/java/site/ycsb/measurements/OneMeasurement.java b/core/src/main/java/site/ycsb/measurements/OneMeasurement.java new file mode 100644 index 0000000..1bc7d51 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/OneMeasurement.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 YCSB contributors All rights reserved. + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.measurements; + +import site.ycsb.Status; +import site.ycsb.measurements.exporter.MeasurementsExporter; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A single measured metric (such as READ LATENCY). + */ +public abstract class OneMeasurement { + + private final String name; + private final ConcurrentHashMap returncodes; + + public String getName() { + return name; + } + + /** + * @param name measurement name + */ + public OneMeasurement(String name) { + this.name = name; + this.returncodes = new ConcurrentHashMap<>(); + } + + public abstract void measure(int latency); + + public abstract String getSummary(); + + /** + * No need for synchronization, using CHM to deal with that. + */ + public void reportStatus(Status status) { + AtomicLong counter = returncodes.get(status); + + if (counter == null) { + counter = new AtomicLong(); + AtomicLong other = returncodes.putIfAbsent(status, counter); + if (other != null) { + counter = other; + } + } + + counter.incrementAndGet(); + } + + /** + * Export the current measurements to a suitable format. + * + * @param exporter Exporter representing the type of format to write to. + * @throws IOException Thrown if the export failed. + */ + public abstract void exportMeasurements(MeasurementsExporter exporter) throws IOException; + + protected final void exportStatusCounts(MeasurementsExporter exporter) throws IOException { + for (Map.Entry entry : returncodes.entrySet()) { + exporter.write(getName(), "Return=" + entry.getKey().getName(), entry.getValue().get()); + } + } +} diff --git a/core/src/main/java/site/ycsb/measurements/OneMeasurementHdrHistogram.java b/core/src/main/java/site/ycsb/measurements/OneMeasurementHdrHistogram.java new file mode 100644 index 0000000..e8ab866 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/OneMeasurementHdrHistogram.java @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.measurements; + +import site.ycsb.measurements.exporter.MeasurementsExporter; +import org.HdrHistogram.Histogram; +import org.HdrHistogram.HistogramIterationValue; +import org.HdrHistogram.HistogramLogWriter; +import org.HdrHistogram.Recorder; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * Take measurements and maintain a HdrHistogram of a given metric, such as READ LATENCY. + * + */ +public class OneMeasurementHdrHistogram extends OneMeasurement { + + // we need one log per measurement histogram + private final PrintStream log; + private final HistogramLogWriter histogramLogWriter; + + private final Recorder histogram; + private Histogram totalHistogram; + + /** + * The name of the property for deciding what percentile values to output. + */ + public static final String PERCENTILES_PROPERTY = "hdrhistogram.percentiles"; + + /** + * The default value for the hdrhistogram.percentiles property. + */ + public static final String PERCENTILES_PROPERTY_DEFAULT = "95,99"; + + /** + * The name of the property for determining if we should print out the buckets. + */ + public static final String VERBOSE_PROPERTY = "measurement.histogram.verbose"; + + /** + * Whether or not to emit the histogram buckets. + */ + private final boolean verbose; + + private final List percentiles; + + public OneMeasurementHdrHistogram(String name, Properties props) { + super(name); + percentiles = getPercentileValues(props.getProperty(PERCENTILES_PROPERTY, PERCENTILES_PROPERTY_DEFAULT)); + verbose = Boolean.valueOf(props.getProperty(VERBOSE_PROPERTY, String.valueOf(false))); + boolean shouldLog = Boolean.parseBoolean(props.getProperty("hdrhistogram.fileoutput", "false")); + if (!shouldLog) { + log = null; + histogramLogWriter = null; + } else { + try { + final String hdrOutputFilename = props.getProperty("hdrhistogram.output.path", "") + name + ".hdr"; + log = new PrintStream(new FileOutputStream(hdrOutputFilename), false); + } catch (FileNotFoundException e) { + throw new RuntimeException("Failed to open hdr histogram output file", e); + } + histogramLogWriter = new HistogramLogWriter(log); + histogramLogWriter.outputComment("[Logging for: " + name + "]"); + histogramLogWriter.outputLogFormatVersion(); + long now = System.currentTimeMillis(); + histogramLogWriter.outputStartTime(now); + histogramLogWriter.setBaseTime(now); + histogramLogWriter.outputLegend(); + } + histogram = new Recorder(3); + } + + /** + * It appears latency is reported in micros. + * Using {@link Recorder} to support concurrent updates to histogram. + */ + public void measure(int latencyInMicros) { + histogram.recordValue(latencyInMicros); + } + + /** + * This is called from a main thread, on orderly termination. + */ + @Override + public void exportMeasurements(MeasurementsExporter exporter) throws IOException { + // accumulate the last interval which was not caught by status thread + Histogram intervalHistogram = getIntervalHistogramAndAccumulate(); + if (histogramLogWriter != null) { + histogramLogWriter.outputIntervalHistogram(intervalHistogram); + // we can close now + log.close(); + } + exporter.write(getName(), "Operations", totalHistogram.getTotalCount()); + exporter.write(getName(), "AverageLatency(us)", totalHistogram.getMean()); + exporter.write(getName(), "MinLatency(us)", totalHistogram.getMinValue()); + exporter.write(getName(), "MaxLatency(us)", totalHistogram.getMaxValue()); + + for (Double percentile : percentiles) { + exporter.write(getName(), ordinal(percentile) + "PercentileLatency(us)", + totalHistogram.getValueAtPercentile(percentile)); + } + + exportStatusCounts(exporter); + + // also export totalHistogram + if (verbose) { + for (HistogramIterationValue v : totalHistogram.recordedValues()) { + int value; + if (v.getValueIteratedTo() > (long)Integer.MAX_VALUE) { + value = Integer.MAX_VALUE; + } else { + value = (int)v.getValueIteratedTo(); + } + + exporter.write(getName(), Integer.toString(value), (double)v.getCountAtValueIteratedTo()); + } + } + } + + /** + * This is called periodically from the StatusThread. There's a single + * StatusThread per Client process. We optionally serialize the interval to + * log on this opportunity. + * + * @see site.ycsb.measurements.OneMeasurement#getSummary() + */ + @Override + public String getSummary() { + Histogram intervalHistogram = getIntervalHistogramAndAccumulate(); + // we use the summary interval as the histogram file interval. + if (histogramLogWriter != null) { + histogramLogWriter.outputIntervalHistogram(intervalHistogram); + } + + DecimalFormat d = new DecimalFormat("#.##"); + return "[" + getName() + ": Count=" + intervalHistogram.getTotalCount() + ", Max=" + + intervalHistogram.getMaxValue() + ", Min=" + intervalHistogram.getMinValue() + ", Avg=" + + d.format(intervalHistogram.getMean()) + ", 90=" + d.format(intervalHistogram.getValueAtPercentile(90)) + + ", 99=" + d.format(intervalHistogram.getValueAtPercentile(99)) + ", 99.9=" + + d.format(intervalHistogram.getValueAtPercentile(99.9)) + ", 99.99=" + + d.format(intervalHistogram.getValueAtPercentile(99.99)) + "]"; + } + + private Histogram getIntervalHistogramAndAccumulate() { + Histogram intervalHistogram = histogram.getIntervalHistogram(); + // add this to the total time histogram. + if (totalHistogram == null) { + totalHistogram = intervalHistogram; + } else { + totalHistogram.add(intervalHistogram); + } + return intervalHistogram; + } + + /** + * Helper method to parse the given percentile value string. + * + * @param percentileString - comma delimited string of Integer values + * @return An Integer List of percentile values + */ + private List getPercentileValues(String percentileString) { + List percentileValues = new ArrayList<>(); + + try { + for (String rawPercentile : percentileString.split(",")) { + percentileValues.add(Double.parseDouble(rawPercentile)); + } + } catch (Exception e) { + // If the given hdrhistogram.percentiles value is unreadable for whatever reason, + // then calculate and return the default set. + System.err.println("[WARN] Couldn't read " + PERCENTILES_PROPERTY + " value: '" + percentileString + + "', the default of '" + PERCENTILES_PROPERTY_DEFAULT + "' will be used."); + e.printStackTrace(); + return getPercentileValues(PERCENTILES_PROPERTY_DEFAULT); + } + + return percentileValues; + } + + /** + * Helper method to find the ordinal of any number. eg 1 -> 1st + * @param i number + * @return ordinal string + */ + private String ordinal(Double i) { + String[] suffixes = new String[]{"th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"}; + Integer j = i.intValue(); + if (i % 1 == 0) { + switch (j % 100) { + case 11: + case 12: + case 13: + return j + "th"; + default: + return j + suffixes[j % 10]; + } + } else { + return i.toString(); + } + } +} diff --git a/core/src/main/java/site/ycsb/measurements/OneMeasurementHistogram.java b/core/src/main/java/site/ycsb/measurements/OneMeasurementHistogram.java new file mode 100644 index 0000000..01ec402 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/OneMeasurementHistogram.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.measurements; + +import site.ycsb.measurements.exporter.MeasurementsExporter; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.Properties; + +/** + * Take measurements and maintain a histogram of a given metric, such as READ LATENCY. + * + */ +public class OneMeasurementHistogram extends OneMeasurement { + public static final String BUCKETS = "histogram.buckets"; + public static final String BUCKETS_DEFAULT = "1000"; + public static final String VERBOSE_PROPERTY = "measurement.histogram.verbose"; + + /** + * Specify the range of latencies to track in the histogram. + */ + private final int buckets; + + /** + * Groups operations in discrete blocks of 1ms width. + */ + private long[] histogram; + + /** + * Counts all operations outside the histogram's range. + */ + private long histogramoverflow; + + /** + * The total number of reported operations. + */ + private long operations; + + /** + * The sum of each latency measurement over all operations. + * Calculated in ms. + */ + private long totallatency; + + /** + * The sum of each latency measurement squared over all operations. + * Used to calculate variance of latency. + * Calculated in ms. + */ + private double totalsquaredlatency; + + /** + * Whether or not to emit the histogram buckets. + */ + private final boolean verbose; + + //keep a windowed version of these stats for printing status + private long windowoperations; + private long windowtotallatency; + + private int min; + private int max; + + public OneMeasurementHistogram(String name, Properties props) { + super(name); + buckets = Integer.parseInt(props.getProperty(BUCKETS, BUCKETS_DEFAULT)); + verbose = Boolean.valueOf(props.getProperty(VERBOSE_PROPERTY, String.valueOf(false))); + histogram = new long[buckets]; + histogramoverflow = 0; + operations = 0; + totallatency = 0; + totalsquaredlatency = 0; + windowoperations = 0; + windowtotallatency = 0; + min = -1; + max = -1; + } + + /* (non-Javadoc) + * @see site.ycsb.OneMeasurement#measure(int) + */ + public synchronized void measure(int latency) { + //latency reported in us and collected in bucket by ms. + if (latency / 1000 >= buckets) { + histogramoverflow++; + } else { + histogram[latency / 1000]++; + } + operations++; + totallatency += latency; + totalsquaredlatency += ((double) latency) * ((double) latency); + windowoperations++; + windowtotallatency += latency; + + if ((min < 0) || (latency < min)) { + min = latency; + } + + if ((max < 0) || (latency > max)) { + max = latency; + } + } + + @Override + public void exportMeasurements(MeasurementsExporter exporter) throws IOException { + double mean = totallatency / ((double) operations); + double variance = totalsquaredlatency / ((double) operations) - (mean * mean); + exporter.write(getName(), "Operations", operations); + exporter.write(getName(), "AverageLatency(us)", mean); + exporter.write(getName(), "LatencyVariance(us)", variance); + exporter.write(getName(), "MinLatency(us)", min); + exporter.write(getName(), "MaxLatency(us)", max); + + long opcounter=0; + boolean done95th = false; + for (int i = 0; i < buckets; i++) { + opcounter += histogram[i]; + if ((!done95th) && (((double) opcounter) / ((double) operations) >= 0.95)) { + exporter.write(getName(), "95thPercentileLatency(us)", i * 1000); + done95th = true; + } + if (((double) opcounter) / ((double) operations) >= 0.99) { + exporter.write(getName(), "99thPercentileLatency(us)", i * 1000); + break; + } + } + + exportStatusCounts(exporter); + + if (verbose) { + for (int i = 0; i < buckets; i++) { + exporter.write(getName(), Integer.toString(i), histogram[i]); + } + + exporter.write(getName(), ">" + buckets, histogramoverflow); + } + } + + @Override + public String getSummary() { + if (windowoperations == 0) { + return ""; + } + DecimalFormat d = new DecimalFormat("#.##"); + double report = ((double) windowtotallatency) / ((double) windowoperations); + windowtotallatency = 0; + windowoperations = 0; + return "[" + getName() + " AverageLatency(us)=" + d.format(report) + "]"; + } +} diff --git a/core/src/main/java/site/ycsb/measurements/OneMeasurementRaw.java b/core/src/main/java/site/ycsb/measurements/OneMeasurementRaw.java new file mode 100644 index 0000000..a3cc570 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/OneMeasurementRaw.java @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2015-2017 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 site.ycsb.measurements; + +import site.ycsb.measurements.exporter.MeasurementsExporter; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.Properties; + +/** + * Record a series of measurements as raw data points without down sampling, + * optionally write to an output file when configured. + * + */ +public class OneMeasurementRaw extends OneMeasurement { + /** + * One raw data point, two fields: timestamp (ms) when the datapoint is + * inserted, and the value. + */ + class RawDataPoint { + private final long timestamp; + private final int value; + + public RawDataPoint(int value) { + this.timestamp = System.currentTimeMillis(); + this.value = value; + } + + public long timeStamp() { + return timestamp; + } + + public int value() { + return value; + } + } + + class RawDataPointComparator implements Comparator { + @Override + public int compare(RawDataPoint p1, RawDataPoint p2) { + if (p1.value() < p2.value()) { + return -1; + } else if (p1.value() == p2.value()) { + return 0; + } else { + return 1; + } + } + } + + /** + * Optionally, user can configure an output file to save the raw data points. + * Default is none, raw results will be written to stdout. + * + */ + public static final String OUTPUT_FILE_PATH = "measurement.raw.output_file"; + public static final String OUTPUT_FILE_PATH_DEFAULT = ""; + + /** + * Optionally, user can request to not output summary stats. This is useful + * if the user chains the raw measurement type behind the HdrHistogram type + * which already outputs summary stats. But even in that case, the user may + * still want this class to compute summary stats for them, especially if + * they want accurate computation of percentiles (because percentils computed + * by histogram classes are still approximations). + */ + public static final String NO_SUMMARY_STATS = "measurement.raw.no_summary"; + public static final String NO_SUMMARY_STATS_DEFAULT = "false"; + + private final PrintStream outputStream; + + private boolean noSummaryStats = false; + + private LinkedList measurements; + private long totalLatency = 0; + + // A window of stats to print summary for at the next getSummary() call. + // It's supposed to be a one line summary, so we will just print count and + // average. + private int windowOperations = 0; + private long windowTotalLatency = 0; + + public OneMeasurementRaw(String name, Properties props) { + super(name); + + String outputFilePath = props.getProperty(OUTPUT_FILE_PATH, OUTPUT_FILE_PATH_DEFAULT); + if (!outputFilePath.isEmpty()) { + System.out.println("Raw data measurement: will output to result file: " + + outputFilePath); + + try { + outputStream = new PrintStream( + new FileOutputStream(outputFilePath, true), + true); + } catch (FileNotFoundException e) { + throw new RuntimeException("Failed to open raw data output file", e); + } + + } else { + System.out.println("Raw data measurement: will output to stdout."); + outputStream = System.out; + + } + + noSummaryStats = Boolean.parseBoolean(props.getProperty(NO_SUMMARY_STATS, + NO_SUMMARY_STATS_DEFAULT)); + + measurements = new LinkedList<>(); + } + + @Override + public synchronized void measure(int latency) { + totalLatency += latency; + windowTotalLatency += latency; + windowOperations++; + + measurements.add(new RawDataPoint(latency)); + } + + @Override + public void exportMeasurements(MeasurementsExporter exporter) + throws IOException { + // Output raw data points first then print out a summary of percentiles to + // stdout. + + outputStream.println(getName() + + " latency raw data: op, timestamp(ms), latency(us)"); + for (RawDataPoint point : measurements) { + outputStream.println( + String.format("%s,%d,%d", getName(), point.timeStamp(), + point.value())); + } + if (outputStream != System.out) { + outputStream.close(); + } + + int totalOps = measurements.size(); + exporter.write(getName(), "Total Operations", totalOps); + if (totalOps > 0 && !noSummaryStats) { + exporter.write(getName(), + "Below is a summary of latency in microseconds:", -1); + exporter.write(getName(), "Average", + (double) totalLatency / (double) totalOps); + + Collections.sort(measurements, new RawDataPointComparator()); + + exporter.write(getName(), "Min", measurements.get(0).value()); + exporter.write( + getName(), "Max", measurements.get(totalOps - 1).value()); + exporter.write( + getName(), "p1", measurements.get((int) (totalOps * 0.01)).value()); + exporter.write( + getName(), "p5", measurements.get((int) (totalOps * 0.05)).value()); + exporter.write( + getName(), "p50", measurements.get((int) (totalOps * 0.5)).value()); + exporter.write( + getName(), "p90", measurements.get((int) (totalOps * 0.9)).value()); + exporter.write( + getName(), "p95", measurements.get((int) (totalOps * 0.95)).value()); + exporter.write( + getName(), "p99", measurements.get((int) (totalOps * 0.99)).value()); + exporter.write(getName(), "p99.9", + measurements.get((int) (totalOps * 0.999)).value()); + exporter.write(getName(), "p99.99", + measurements.get((int) (totalOps * 0.9999)).value()); + } + + exportStatusCounts(exporter); + } + + @Override + public synchronized String getSummary() { + if (windowOperations == 0) { + return ""; + } + + String toReturn = String.format("%s count: %d, average latency(us): %.2f", + getName(), windowOperations, + (double) windowTotalLatency / (double) windowOperations); + + windowTotalLatency = 0; + windowOperations = 0; + + return toReturn; + } +} diff --git a/core/src/main/java/site/ycsb/measurements/OneMeasurementTimeSeries.java b/core/src/main/java/site/ycsb/measurements/OneMeasurementTimeSeries.java new file mode 100644 index 0000000..03c3cf2 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/OneMeasurementTimeSeries.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.measurements; + +import site.ycsb.measurements.exporter.MeasurementsExporter; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.Properties; +import java.util.Vector; + +class SeriesUnit { + /** + * @param time + * @param average + */ + public SeriesUnit(long time, double average) { + this.time = time; + this.average = average; + } + + protected final long time; + protected final double average; +} + +/** + * A time series measurement of a metric, such as READ LATENCY. + */ +public class OneMeasurementTimeSeries extends OneMeasurement { + + /** + * Granularity for time series; measurements will be averaged in chunks of this granularity. Units are milliseconds. + */ + public static final String GRANULARITY = "timeseries.granularity"; + public static final String GRANULARITY_DEFAULT = "1000"; + + private final int granularity; + private final Vector measurements; + + private long start = -1; + private long currentunit = -1; + private long count = 0; + private long sum = 0; + private long operations = 0; + private long totallatency = 0; + + //keep a windowed version of these stats for printing status + private int windowoperations = 0; + private long windowtotallatency = 0; + + private int min = -1; + private int max = -1; + + public OneMeasurementTimeSeries(String name, Properties props) { + super(name); + granularity = Integer.parseInt(props.getProperty(GRANULARITY, GRANULARITY_DEFAULT)); + measurements = new Vector<>(); + } + + private synchronized void checkEndOfUnit(boolean forceend) { + long now = System.currentTimeMillis(); + + if (start < 0) { + currentunit = 0; + start = now; + } + + long unit = ((now - start) / granularity) * granularity; + + if ((unit > currentunit) || (forceend)) { + double avg = ((double) sum) / ((double) count); + measurements.add(new SeriesUnit(currentunit, avg)); + + currentunit = unit; + + count = 0; + sum = 0; + } + } + + @Override + public void measure(int latency) { + checkEndOfUnit(false); + + count++; + sum += latency; + totallatency += latency; + operations++; + windowoperations++; + windowtotallatency += latency; + + if (latency > max) { + max = latency; + } + + if ((latency < min) || (min < 0)) { + min = latency; + } + } + + + @Override + public void exportMeasurements(MeasurementsExporter exporter) throws IOException { + checkEndOfUnit(true); + + exporter.write(getName(), "Operations", operations); + exporter.write(getName(), "AverageLatency(us)", (((double) totallatency) / ((double) operations))); + exporter.write(getName(), "MinLatency(us)", min); + exporter.write(getName(), "MaxLatency(us)", max); + + // TODO: 95th and 99th percentile latency + + exportStatusCounts(exporter); + for (SeriesUnit unit : measurements) { + exporter.write(getName(), Long.toString(unit.time), unit.average); + } + } + + @Override + public String getSummary() { + if (windowoperations == 0) { + return ""; + } + DecimalFormat d = new DecimalFormat("#.##"); + double report = ((double) windowtotallatency) / ((double) windowoperations); + windowtotallatency = 0; + windowoperations = 0; + return "[" + getName() + " AverageLatency(us)=" + d.format(report) + "]"; + } + +} diff --git a/core/src/main/java/site/ycsb/measurements/TwoInOneMeasurement.java b/core/src/main/java/site/ycsb/measurements/TwoInOneMeasurement.java new file mode 100644 index 0000000..9ba53c1 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/TwoInOneMeasurement.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.measurements; + +import site.ycsb.Status; +import site.ycsb.measurements.exporter.MeasurementsExporter; + +import java.io.IOException; + +/** + * delegates to 2 measurement instances. + */ +public class TwoInOneMeasurement extends OneMeasurement { + + private final OneMeasurement thing1, thing2; + + public TwoInOneMeasurement(String name, OneMeasurement thing1, OneMeasurement thing2) { + super(name); + this.thing1 = thing1; + this.thing2 = thing2; + } + + /** + * No need for synchronization, using CHM to deal with that. + */ + @Override + public void reportStatus(final Status status) { + thing1.reportStatus(status); + } + + /** + * It appears latency is reported in micros. + * Using {@link org.HdrHistogram.Recorder} to support concurrent updates to histogram. + */ + @Override + public void measure(int latencyInMicros) { + thing1.measure(latencyInMicros); + thing2.measure(latencyInMicros); + } + + /** + * This is called from a main thread, on orderly termination. + */ + @Override + public void exportMeasurements(MeasurementsExporter exporter) throws IOException { + thing1.exportMeasurements(exporter); + thing2.exportMeasurements(exporter); + } + + /** + * This is called periodically from the StatusThread. There's a single StatusThread per Client process. + * We optionally serialize the interval to log on this opportunity. + * + * @see site.ycsb.measurements.OneMeasurement#getSummary() + */ + @Override + public String getSummary() { + return thing1.getSummary() + "\n" + thing2.getSummary(); + } + +} diff --git a/core/src/main/java/site/ycsb/measurements/exporter/JSONArrayMeasurementsExporter.java b/core/src/main/java/site/ycsb/measurements/exporter/JSONArrayMeasurementsExporter.java new file mode 100644 index 0000000..6c70164 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/exporter/JSONArrayMeasurementsExporter.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-2016 Yahoo! Inc., 2017 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 site.ycsb.measurements.exporter; + +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.util.DefaultPrettyPrinter; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +/** + * Export measurements into a machine readable JSON Array of measurement objects. + */ +public class JSONArrayMeasurementsExporter implements MeasurementsExporter { + private final JsonFactory factory = new JsonFactory(); + private JsonGenerator g; + + public JSONArrayMeasurementsExporter(OutputStream os) throws IOException { + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); + g = factory.createJsonGenerator(bw); + g.setPrettyPrinter(new DefaultPrettyPrinter()); + g.writeStartArray(); + } + + public void write(String metric, String measurement, int i) throws IOException { + g.writeStartObject(); + g.writeStringField("metric", metric); + g.writeStringField("measurement", measurement); + g.writeNumberField("value", i); + g.writeEndObject(); + } + + public void write(String metric, String measurement, long i) throws IOException { + g.writeStartObject(); + g.writeStringField("metric", metric); + g.writeStringField("measurement", measurement); + g.writeNumberField("value", i); + g.writeEndObject(); + } + + public void write(String metric, String measurement, double d) throws IOException { + g.writeStartObject(); + g.writeStringField("metric", metric); + g.writeStringField("measurement", measurement); + g.writeNumberField("value", d); + g.writeEndObject(); + } + + public void close() throws IOException { + if (g != null) { + g.writeEndArray(); + g.close(); + } + } +} diff --git a/core/src/main/java/site/ycsb/measurements/exporter/JSONMeasurementsExporter.java b/core/src/main/java/site/ycsb/measurements/exporter/JSONMeasurementsExporter.java new file mode 100644 index 0000000..c85f56b --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/exporter/JSONMeasurementsExporter.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.measurements.exporter; + +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.util.DefaultPrettyPrinter; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +/** + * Export measurements into a machine readable JSON file. + */ +public class JSONMeasurementsExporter implements MeasurementsExporter { + + private final JsonFactory factory = new JsonFactory(); + private JsonGenerator g; + + public JSONMeasurementsExporter(OutputStream os) throws IOException { + + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); + g = factory.createJsonGenerator(bw); + g.setPrettyPrinter(new DefaultPrettyPrinter()); + } + + public void write(String metric, String measurement, int i) throws IOException { + g.writeStartObject(); + g.writeStringField("metric", metric); + g.writeStringField("measurement", measurement); + g.writeNumberField("value", i); + g.writeEndObject(); + } + + public void write(String metric, String measurement, long i) throws IOException { + g.writeStartObject(); + g.writeStringField("metric", metric); + g.writeStringField("measurement", measurement); + g.writeNumberField("value", i); + g.writeEndObject(); + } + + public void write(String metric, String measurement, double d) throws IOException { + g.writeStartObject(); + g.writeStringField("metric", metric); + g.writeStringField("measurement", measurement); + g.writeNumberField("value", d); + g.writeEndObject(); + } + + public void close() throws IOException { + if (g != null) { + g.close(); + } + } + +} diff --git a/core/src/main/java/site/ycsb/measurements/exporter/MeasurementsExporter.java b/core/src/main/java/site/ycsb/measurements/exporter/MeasurementsExporter.java new file mode 100644 index 0000000..f3626e1 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/exporter/MeasurementsExporter.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.measurements.exporter; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Used to export the collected measurements into a useful format, for example + * human readable text or machine readable JSON. + */ +public interface MeasurementsExporter extends Closeable { + /** + * Write a measurement to the exported format. + * + * @param metric Metric name, for example "READ LATENCY". + * @param measurement Measurement name, for example "Average latency". + * @param i Measurement to write. + * @throws IOException if writing failed + */ + void write(String metric, String measurement, int i) throws IOException; + + /** + * Write a measurement to the exported format. + * + * @param metric Metric name, for example "READ LATENCY". + * @param measurement Measurement name, for example "Average latency". + * @param i Measurement to write. + * @throws IOException if writing failed + */ + void write(String metric, String measurement, long i) throws IOException; + + /** + * Write a measurement to the exported format. + * + * @param metric Metric name, for example "READ LATENCY". + * @param measurement Measurement name, for example "Average latency". + * @param d Measurement to write. + * @throws IOException if writing failed + */ + void write(String metric, String measurement, double d) throws IOException; +} diff --git a/core/src/main/java/site/ycsb/measurements/exporter/TextMeasurementsExporter.java b/core/src/main/java/site/ycsb/measurements/exporter/TextMeasurementsExporter.java new file mode 100644 index 0000000..36acd92 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/exporter/TextMeasurementsExporter.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2016 Yahoo! Inc., 2017 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 site.ycsb.measurements.exporter; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +/** + * Write human readable text. Tries to emulate the previous print report method. + */ +public class TextMeasurementsExporter implements MeasurementsExporter { + private final BufferedWriter bw; + + public TextMeasurementsExporter(OutputStream os) { + this.bw = new BufferedWriter(new OutputStreamWriter(os)); + } + + public void write(String metric, String measurement, int i) throws IOException { + bw.write("[" + metric + "], " + measurement + ", " + i); + bw.newLine(); + } + + public void write(String metric, String measurement, long i) throws IOException { + bw.write("[" + metric + "], " + measurement + ", " + i); + bw.newLine(); + } + + public void write(String metric, String measurement, double d) throws IOException { + bw.write("[" + metric + "], " + measurement + ", " + d); + bw.newLine(); + } + + public void close() throws IOException { + this.bw.close(); + } +} diff --git a/core/src/main/java/site/ycsb/measurements/exporter/package-info.java b/core/src/main/java/site/ycsb/measurements/exporter/package-info.java new file mode 100644 index 0000000..fe74b68 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/exporter/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017 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. + */ + +/** + * The YCSB measurements.exporter package. + */ +package site.ycsb.measurements.exporter; + diff --git a/core/src/main/java/site/ycsb/measurements/package-info.java b/core/src/main/java/site/ycsb/measurements/package-info.java new file mode 100644 index 0000000..78fc129 --- /dev/null +++ b/core/src/main/java/site/ycsb/measurements/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017 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. + */ + +/** + * The YCSB measurements package. + */ +package site.ycsb.measurements; + diff --git a/core/src/main/java/site/ycsb/package-info.java b/core/src/main/java/site/ycsb/package-info.java new file mode 100644 index 0000000..276715d --- /dev/null +++ b/core/src/main/java/site/ycsb/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2015 - 2017 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. + */ + +/** + * The YCSB core package. + */ +package site.ycsb; + diff --git a/core/src/main/java/site/ycsb/workloads/ConstantOccupancyWorkload.java b/core/src/main/java/site/ycsb/workloads/ConstantOccupancyWorkload.java new file mode 100644 index 0000000..2403bb8 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/ConstantOccupancyWorkload.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010 Yahoo! Inc. All rights reserved. + * Copyrihght (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb.workloads; + +import site.ycsb.Client; +import site.ycsb.WorkloadException; +import site.ycsb.generator.NumberGenerator; +import site.ycsb.workloads.core.CoreHelper; + +import java.util.Properties; +import static site.ycsb.workloads.core.CoreConstants.*; +/** + * A disk-fragmenting workload. + *

+ * Properties to control the client: + *

+ *
    + *
  • disksize: how many bytes of storage can the disk store? (default 100,000,000) + *
  • occupancy: what fraction of the available storage should be used? (default 0.9) + *
  • requestdistribution: what distribution should be used to select the records to operate on - uniform, + * zipfian or latest (default: histogram) + *
+ *

+ *

+ *

See also: + * Russell Sears, Catharine van Ingen. + * Fragmentation in Large Object + * Repositories, + * CIDR 2006. [Presentation] + *

+ */ +public class ConstantOccupancyWorkload extends CoreWorkload { + private long disksize; + private long storageages; + private double occupancy; + + private long objectCount; + + public static final String STORAGE_AGE_PROPERTY = "storageages"; + public static final long STORAGE_AGE_PROPERTY_DEFAULT = 10; + + public static final String DISK_SIZE_PROPERTY = "disksize"; + public static final long DISK_SIZE_PROPERTY_DEFAULT = 100 * 1000 * 1000; + + public static final String OCCUPANCY_PROPERTY = "occupancy"; + public static final double OCCUPANCY_PROPERTY_DEFAULT = 0.9; + + @Override + public void init(Properties p) throws WorkloadException { + disksize = Long.parseLong(p.getProperty(DISK_SIZE_PROPERTY, String.valueOf(DISK_SIZE_PROPERTY_DEFAULT))); + storageages = Long.parseLong(p.getProperty(STORAGE_AGE_PROPERTY, String.valueOf(STORAGE_AGE_PROPERTY_DEFAULT))); + occupancy = Double.parseDouble(p.getProperty(OCCUPANCY_PROPERTY, String.valueOf(OCCUPANCY_PROPERTY_DEFAULT))); + + if (p.getProperty(Client.RECORD_COUNT_PROPERTY) != null || + p.getProperty(Client.INSERT_COUNT_PROPERTY) != null || + p.getProperty(Client.OPERATION_COUNT_PROPERTY) != null) { + System.err.println("Warning: record, insert or operation count was set prior to initting " + + "ConstantOccupancyWorkload. Overriding old values."); + } + NumberGenerator g = CoreHelper.getFieldLengthGenerator(p); + double fieldsize = g.mean(); + int fieldcount = Integer.parseInt(p.getProperty(FIELD_COUNT_PROPERTY, FIELD_COUNT_PROPERTY_DEFAULT)); + + objectCount = (long) (occupancy * (disksize / (fieldsize * fieldcount))); + if (objectCount == 0) { + throw new IllegalStateException("Object count was zero. Perhaps disksize is too low?"); + } + p.setProperty(Client.RECORD_COUNT_PROPERTY, String.valueOf(objectCount)); + p.setProperty(Client.OPERATION_COUNT_PROPERTY, String.valueOf(storageages * objectCount)); + p.setProperty(Client.INSERT_COUNT_PROPERTY, String.valueOf(objectCount)); + + super.init(p); + } + +} diff --git a/core/src/main/java/site/ycsb/workloads/CoreWorkload.java b/core/src/main/java/site/ycsb/workloads/CoreWorkload.java new file mode 100644 index 0000000..c36d4b3 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/CoreWorkload.java @@ -0,0 +1,574 @@ +/** + * Copyright (c) 2010 Yahoo! Inc., Copyright (c) 2016-2020 YCSB contributors. All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb.workloads; + +import site.ycsb.*; +import site.ycsb.generator.*; +import site.ycsb.generator.acknowledge.AcknowledgedCounterGenerator; +import site.ycsb.measurements.Measurements; +import site.ycsb.workloads.core.CoreHelper; +import site.ycsb.wrappers.DatabaseField; +import site.ycsb.wrappers.Wrappers; +import static site.ycsb.workloads.core.CoreConstants.*; + +import java.util.*; + +/** + * The core benchmark scenario. Represents a set of clients doing simple CRUD operations. The + * relative proportion of different kinds of operations, and other properties of the workload, + * are controlled by parameters specified at runtime. + *

+ * Properties to control the client: + *

    + *
  • fieldcount: the number of fields in a record (default: 10) + *
  • fieldlength: the size of each field (default: 100) + *
  • minfieldlength: the minimum size of each field (default: 1) + *
  • readallfields: should reads read all fields (true) or just one (false) (default: true) + *
  • writeallfields: should updates and read/modify/writes update all fields (true) or just + * one (false) (default: false) + *
  • readproportion: what proportion of operations should be reads (default: 0.95) + *
  • updateproportion: what proportion of operations should be updates (default: 0.05) + *
  • insertproportion: what proportion of operations should be inserts (default: 0) + *
  • scanproportion: what proportion of operations should be scans (default: 0) + *
  • readmodifywriteproportion: what proportion of operations should be read a record, + * modify it, write it back (default: 0) + *
  • requestdistribution: what distribution should be used to select the records to operate + * on - uniform, zipfian, hotspot, sequential, exponential or latest (default: uniform) + *
  • minscanlength: for scans, what is the minimum number of records to scan (default: 1) + *
  • maxscanlength: for scans, what is the maximum number of records to scan (default: 1000) + *
  • scanlengthdistribution: for scans, what distribution should be used to choose the + * number of records to scan, for each scan, between 1 and maxscanlength (default: uniform) + *
  • insertstart: for parallel loads and runs, defines the starting record for this + * YCSB instance (default: 0) + *
  • insertcount: for parallel loads and runs, defines the number of records for this + * YCSB instance (default: recordcount) + *
  • zeropadding: for generating a record sequence compatible with string sort order by + * 0 padding the record number. Controls the number of 0s to use for padding. (default: 1) + * For example for row 5, with zeropadding=1 you get 'user5' key and with zeropading=8 you get + * 'user00000005' key. In order to see its impact, zeropadding needs to be bigger than number of + * digits in the record number. + *
  • insertorder: should records be inserted in order by key ("ordered"), or in hashed + * order ("hashed") (default: hashed) + *
  • fieldnameprefix: what should be a prefix for field names, the shorter may decrease the + * required storage size (default: "field") + *
+ */ +public class CoreWorkload extends Workload { + + protected String table; + protected List fieldnames; + + /** + * Generator object that produces field lengths. The value of this depends on the properties that + * start with "FIELD_LENGTH_". + */ + protected NumberGenerator fieldlengthgenerator; + + protected boolean readallfields; + + protected boolean readallfieldsbyname; + + protected boolean writeallfields; + + /** + * Set to true if want to check correctness of reads. Must also + * be set to true during loading phase to function. + */ + private boolean dataintegrity; + + + protected NumberGenerator keysequence; + protected DiscreteGenerator operationchooser; + protected NumberGenerator keychooser; + protected NumberGenerator fieldchooser; + protected AcknowledgedCounterGenerator transactioninsertkeysequence; + protected NumberGenerator scanlength; + protected boolean orderedinserts; + protected long fieldcount; + protected long recordcount; + protected int zeropadding; + protected int insertionRetryLimit; + protected int insertionRetryInterval; + + private Measurements measurements = Measurements.getMeasurements(); + + public static String buildKeyName(long keynum, int zeropadding, boolean orderedinserts) { + if (!orderedinserts) { + keynum = Utils.hash(keynum); + } + String value = Long.toString(keynum); + int fill = zeropadding - value.length(); + String prekey = "user"; + for (int i = 0; i < fill; i++) { + prekey += '0'; + } + return prekey + value; + } + + + + /** + * Initialize the scenario. + * Called once, in the main client thread, before any operations are started. + */ + @Override + public void init(Properties p) throws WorkloadException { + table = p.getProperty(TABLENAME_PROPERTY, TABLENAME_PROPERTY_DEFAULT); + + fieldcount = + Long.parseLong(p.getProperty(FIELD_COUNT_PROPERTY, FIELD_COUNT_PROPERTY_DEFAULT)); + final String fieldnameprefix = p.getProperty(FIELD_NAME_PREFIX, FIELD_NAME_PREFIX_DEFAULT); + fieldnames = new ArrayList<>(); + for (int i = 0; i < fieldcount; i++) { + fieldnames.add(fieldnameprefix + i); + } + fieldlengthgenerator = CoreHelper.getFieldLengthGenerator(p); + + recordcount = + Long.parseLong(p.getProperty(Client.RECORD_COUNT_PROPERTY, Client.DEFAULT_RECORD_COUNT)); + if (recordcount == 0) { + recordcount = Integer.MAX_VALUE; + } + String requestdistrib = + p.getProperty(REQUEST_DISTRIBUTION_PROPERTY, REQUEST_DISTRIBUTION_PROPERTY_DEFAULT); + int minscanlength = + Integer.parseInt(p.getProperty(MIN_SCAN_LENGTH_PROPERTY, MIN_SCAN_LENGTH_PROPERTY_DEFAULT)); + int maxscanlength = + Integer.parseInt(p.getProperty(MAX_SCAN_LENGTH_PROPERTY, MAX_SCAN_LENGTH_PROPERTY_DEFAULT)); + String scanlengthdistrib = + p.getProperty(SCAN_LENGTH_DISTRIBUTION_PROPERTY, SCAN_LENGTH_DISTRIBUTION_PROPERTY_DEFAULT); + + long insertstart = + Long.parseLong(p.getProperty(INSERT_START_PROPERTY, INSERT_START_PROPERTY_DEFAULT)); + long insertcount= Long.parseLong(p.getProperty(INSERT_COUNT_PROPERTY, String.valueOf(recordcount - insertstart))); + // Confirm valid values for insertstart and insertcount in relation to recordcount + if (recordcount < (insertstart + insertcount)) { + System.err.println("Invalid combination of insertstart, insertcount and recordcount."); + System.err.println("recordcount must be bigger than insertstart + insertcount."); + System.exit(-1); + } + zeropadding = + Integer.parseInt(p.getProperty(ZERO_PADDING_PROPERTY, ZERO_PADDING_PROPERTY_DEFAULT)); + + readallfields = Boolean.parseBoolean( + p.getProperty(READ_ALL_FIELDS_PROPERTY, READ_ALL_FIELDS_PROPERTY_DEFAULT)); + readallfieldsbyname = Boolean.parseBoolean( + p.getProperty(READ_ALL_FIELDS_BY_NAME_PROPERTY, READ_ALL_FIELDS_BY_NAME_PROPERTY_DEFAULT)); + writeallfields = Boolean.parseBoolean( + p.getProperty(WRITE_ALL_FIELDS_PROPERTY, WRITE_ALL_FIELDS_PROPERTY_DEFAULT)); + + dataintegrity = Boolean.parseBoolean( + p.getProperty(DATA_INTEGRITY_PROPERTY, DATA_INTEGRITY_PROPERTY_DEFAULT)); + // Confirm that fieldlengthgenerator returns a constant if data + // integrity check requested. + if (dataintegrity && !(p.getProperty( + FIELD_LENGTH_DISTRIBUTION_PROPERTY, + FIELD_LENGTH_DISTRIBUTION_PROPERTY_DEFAULT)).equals("constant")) { + System.err.println("Must have constant field size to check data integrity."); + System.exit(-1); + } + if (dataintegrity) { + System.out.println("Data integrity is enabled."); + } + + if (p.getProperty(INSERT_ORDER_PROPERTY, INSERT_ORDER_PROPERTY_DEFAULT).compareTo("hashed") == 0) { + orderedinserts = false; + } else { + orderedinserts = true; + } + + keysequence = new CounterGenerator(insertstart); + operationchooser = CoreHelper.createOperationGenerator(p); + + transactioninsertkeysequence = CoreHelper.createTransactionInsertKeyGenerator(p, recordcount); + if (requestdistrib.compareTo("uniform") == 0) { + keychooser = new UniformLongGenerator(insertstart, insertstart + insertcount - 1); + } else if (requestdistrib.compareTo("exponential") == 0) { + 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); + } else if (requestdistrib.compareTo("sequential") == 0) { + keychooser = new SequentialGenerator(insertstart, insertstart + insertcount - 1); + } else if (requestdistrib.compareTo("zipfian") == 0) { + // it does this by generating a random "next key" in part by taking the modulus over the + // number of keys. + // If the number of keys changes, this would shift the modulus, and we don't want that to + // change which keys are popular so we'll actually construct the scrambled zipfian generator + // with a keyspace that is larger than exists at the beginning of the test. that is, we'll predict + // the number of inserts, and tell the scrambled zipfian generator the number of existing keys + // plus the number of predicted keys as the total keyspace. then, if the generator picks a key + // that hasn't been inserted yet, will just ignore it and pick another key. this way, the size of + // the keyspace doesn't change from the perspective of the scrambled zipfian generator + final double insertproportion = Double.parseDouble( + p.getProperty(INSERT_PROPORTION_PROPERTY, INSERT_PROPORTION_PROPERTY_DEFAULT)); + long opcount = Long.parseLong(p.getProperty(Client.OPERATION_COUNT_PROPERTY)); + long expectednewkeys = (long) ((opcount) * insertproportion * 2.0); // 2 is fudge factor + + keychooser = new ScrambledZipfianGenerator(insertstart, insertstart + insertcount + expectednewkeys); + } else if (requestdistrib.compareTo("latest") == 0) { + keychooser = new SkewedLatestGenerator(transactioninsertkeysequence); + } else if (requestdistrib.equals("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(insertstart, insertstart + insertcount - 1, + hotsetfraction, hotopnfraction); + } else { + throw new WorkloadException("Unknown request distribution \"" + requestdistrib + "\""); + } + + fieldchooser = new UniformLongGenerator(0, fieldcount - 1); + + if (scanlengthdistrib.compareTo("uniform") == 0) { + scanlength = new UniformLongGenerator(minscanlength, maxscanlength); + } else if (scanlengthdistrib.compareTo("zipfian") == 0) { + scanlength = new ZipfianGenerator(minscanlength, maxscanlength); + } else { + throw new WorkloadException( + "Distribution \"" + scanlengthdistrib + "\" not allowed for scan length"); + } + + insertionRetryLimit = Integer.parseInt(p.getProperty( + INSERTION_RETRY_LIMIT, INSERTION_RETRY_LIMIT_DEFAULT)); + insertionRetryInterval = Integer.parseInt(p.getProperty( + INSERTION_RETRY_INTERVAL, INSERTION_RETRY_INTERVAL_DEFAULT)); + } + + /** + * Builds a value for a randomly chosen field. + */ + protected HashMap buildSingleValue(String key) { + HashMap value = new HashMap<>(); + + String fieldkey = fieldnames.get(fieldchooser.nextValue().intValue()); + ByteIterator data; + if (dataintegrity) { + data = new StringByteIterator(buildDeterministicValue(key, fieldkey)); + } else { + // fill with random data + data = new RandomByteIterator(fieldlengthgenerator.nextValue().longValue()); + } + value.put(fieldkey, data); + + return value; + } + + protected HashMap oldBuildValues(String key) { + HashMap values = new HashMap<>(); + for (String fieldkey : fieldnames) { + ByteIterator data; + if (dataintegrity) { + data = new StringByteIterator(buildDeterministicValue(key, fieldkey)); + } else { + // fill with random data + data = new RandomByteIterator(fieldlengthgenerator.nextValue().longValue()); + } + values.put(fieldkey, data); + } + return values; + } + + /** + * Builds values for all fields. + */ + protected List buildValues(String key) { + List values = new ArrayList<>(); + // HashMap values = new HashMap<>(); + + for (String fieldkey : fieldnames) { + ByteIterator data; + if (dataintegrity) { + data = new StringByteIterator(buildDeterministicValue(key, fieldkey)); + } else { + // fill with random data + data = new RandomByteIterator(fieldlengthgenerator.nextValue().longValue()); + } + values.add(new DatabaseField(fieldkey, Wrappers.wrapIterator(data))); + } + return values; + } + + /** + * Build a deterministic value given the key information. + */ + private String buildDeterministicValue(String key, String fieldkey) { + int size = fieldlengthgenerator.nextValue().intValue(); + StringBuilder sb = new StringBuilder(size); + sb.append(key); + sb.append(':'); + sb.append(fieldkey); + while (sb.length() < size) { + sb.append(':'); + sb.append(sb.toString().hashCode()); + } + sb.setLength(size); + + return sb.toString(); + } + + /** + * Do one insert operation. Because it will be called concurrently from multiple client threads, + * this function must be thread safe. However, avoid synchronized, or the threads will block waiting + * for each other, and it will be difficult to reach the target throughput. Ideally, this function would + * have no side effects other than DB operations. + */ + @Override + public boolean doInsert(DB db, Object threadstate) { + long keynum = keysequence.nextValue().longValue(); + String dbkey = CoreWorkload.buildKeyName(keynum, zeropadding, orderedinserts); + List values = buildValues(dbkey); + + Status status; + int numOfRetries = 0; + do { + status = db.insert(table, dbkey, values); + if (null != status && status.isOk()) { + break; + } + // Retry if configured. Without retrying, the load process will fail + // even if one single insertion fails. User can optionally configure + // an insertion retry limit (default is 0) to enable retry. + if (++numOfRetries <= insertionRetryLimit) { + System.err.println("Retrying insertion, retry count: " + numOfRetries); + try { + // Sleep for a random number between [0.8, 1.2)*insertionRetryInterval. + int sleepTime = (int) (1000 * insertionRetryInterval * (0.8 + 0.4 * Math.random())); + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + break; + } + + } else { + System.err.println("Error inserting, not retrying any more. number of attempts: " + numOfRetries + + "Insertion Retry Limit: " + insertionRetryLimit); + break; + + } + } while (true); + + return null != status && status.isOk(); + } + + /** + * Do one transaction operation. Because it will be called concurrently from multiple client + * threads, this function must be thread safe. However, avoid synchronized, or the threads will block waiting + * for each other, and it will be difficult to reach the target throughput. Ideally, this function would + * have no side effects other than DB operations. + */ + @Override + public boolean doTransaction(DB db, Object threadstate) { + String operation = operationchooser.nextString(); + if(operation == null) { + return false; + } + + switch (operation) { + case "READ": + doTransactionRead(db); + break; + case "UPDATE": + doTransactionUpdate(db); + break; + case "INSERT": + doTransactionInsert(db); + break; + case "SCAN": + doTransactionScan(db); + break; + default: + doTransactionReadModifyWrite(db); + } + + return true; + } + + /** + * Results are reported in the first three buckets of the histogram under + * the label "VERIFY". + * Bucket 0 means the expected data was returned. + * Bucket 1 means incorrect data was returned. + * Bucket 2 means null data was returned when some data was expected. + */ + protected void verifyRow(String key, HashMap cells) { + Status verifyStatus = Status.OK; + long startTime = System.nanoTime(); + if (!cells.isEmpty()) { + for (Map.Entry entry : cells.entrySet()) { + if (!entry.getValue().toString().equals(buildDeterministicValue(key, entry.getKey()))) { + verifyStatus = Status.UNEXPECTED_STATE; + break; + } + } + } else { + // This assumes that null data is never valid + verifyStatus = Status.ERROR; + } + long endTime = System.nanoTime(); + measurements.measure("VERIFY", (int) (endTime - startTime) / 1000); + measurements.reportStatus("VERIFY", verifyStatus); + } + + protected long nextKeynum() { + long keynum; + if (keychooser instanceof ExponentialGenerator) { + do { + keynum = transactioninsertkeysequence.lastValue() - keychooser.nextValue().longValue(); + } while (keynum < 0); + } else { + do { + keynum = keychooser.nextValue().longValue(); + } while (keynum > transactioninsertkeysequence.lastValue()); + } + return keynum; + } + + public void doTransactionRead(DB db) { + // choose a random key + long keynum = nextKeynum(); + + String keyname = CoreWorkload.buildKeyName(keynum, zeropadding, orderedinserts); + + HashSet fields = null; + + if (!readallfields) { + // read a random field + String fieldname = fieldnames.get(fieldchooser.nextValue().intValue()); + + fields = new HashSet(); + fields.add(fieldname); + } else if (dataintegrity || readallfieldsbyname) { + // pass the full field list if dataintegrity is on for verification + fields = new HashSet(fieldnames); + } + + HashMap cells = new HashMap(); + db.read(table, keyname, fields, cells); + + if (dataintegrity) { + verifyRow(keyname, cells); + } + } + + public void doTransactionReadModifyWrite(DB db) { + // choose a random key + long keynum = nextKeynum(); + + String keyname = CoreWorkload.buildKeyName(keynum, zeropadding, orderedinserts); + + HashSet fields = null; + + if (!readallfields) { + // read a random field + String fieldname = fieldnames.get(fieldchooser.nextValue().intValue()); + + fields = new HashSet(); + fields.add(fieldname); + } + + HashMap values; + + if (writeallfields) { + // new data for all the fields + values = oldBuildValues(keyname); + } else { + // update a random field + values = buildSingleValue(keyname); + } + + // do the transaction + + HashMap cells = new HashMap(); + + + long ist = measurements.getIntendedStartTimeNs(); + long st = System.nanoTime(); + db.read(table, keyname, fields, cells); + + db.update(table, keyname, values); + + long en = System.nanoTime(); + + if (dataintegrity) { + verifyRow(keyname, cells); + } + + measurements.measure("READ-MODIFY-WRITE", (int) ((en - st) / 1000)); + measurements.measureIntended("READ-MODIFY-WRITE", (int) ((en - ist) / 1000)); + } + + public void doTransactionScan(DB db) { + // choose a random key + long keynum = nextKeynum(); + + String startkeyname = CoreWorkload.buildKeyName(keynum, zeropadding, orderedinserts); + + // choose a random scan length + int len = scanlength.nextValue().intValue(); + + HashSet fields = null; + + if (!readallfields) { + // read a random field + String fieldname = fieldnames.get(fieldchooser.nextValue().intValue()); + + fields = new HashSet(); + fields.add(fieldname); + } + + db.scan(table, startkeyname, len, fields, new Vector>()); + } + + public void doTransactionUpdate(DB db) { + // choose a random key + long keynum = nextKeynum(); + + String keyname = CoreWorkload.buildKeyName(keynum, zeropadding, orderedinserts); + + HashMap values; + + if (writeallfields) { + // new data for all the fields + values = oldBuildValues(keyname); + } else { + // update a random field + values = buildSingleValue(keyname); + } + + db.update(table, keyname, values); + } + + public void doTransactionInsert(DB db) { + // choose the next key + long keynum = transactioninsertkeysequence.nextValue(); + + try { + String dbkey = CoreWorkload.buildKeyName(keynum, zeropadding, orderedinserts); + + List values = buildValues(dbkey); + db.insert(table, dbkey, values); + } finally { + transactioninsertkeysequence.acknowledge(keynum); + } + } +} diff --git a/core/src/main/java/site/ycsb/workloads/RestWorkload.java b/core/src/main/java/site/ycsb/workloads/RestWorkload.java new file mode 100644 index 0000000..535f344 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/RestWorkload.java @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2016-2017 YCSB contributors. All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb.workloads; + +import site.ycsb.ByteIterator; +import site.ycsb.DB; +import site.ycsb.RandomByteIterator; +import site.ycsb.WorkloadException; +import site.ycsb.generator.*; +import site.ycsb.workloads.core.CoreHelper; +import site.ycsb.wrappers.ByteIteratorWrapper; +import site.ycsb.wrappers.DatabaseField; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static site.ycsb.workloads.core.CoreConstants.*; +/** + * 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; + private NumberGenerator fieldlengthgenerator; + private DiscreteGenerator operationchooser; + + @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 = CoreHelper.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; + + 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 UniformLongGenerator(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 = CoreHelper.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) { + String operation = operationchooser.nextString(); + if (operation == null) { + return false; + } + + switch (operation) { + 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) { + List values = new ArrayList<>(); + // Create random bytes of insert data with a specific size. + final ByteIterator data = new RandomByteIterator(fieldlengthgenerator.nextValue().longValue()); + values.add(new DatabaseField("data", ByteIteratorWrapper.create(data))); + db.insert(null, getNextURL(2), values); + } + + 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/core/src/main/java/site/ycsb/workloads/TimeSeriesWorkload.java b/core/src/main/java/site/ycsb/workloads/TimeSeriesWorkload.java new file mode 100644 index 0000000..147c0a6 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/TimeSeriesWorkload.java @@ -0,0 +1,1304 @@ +/** + * Copyright (c) 2017 YCSB contributors All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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 site.ycsb.workloads; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Random; +import java.util.Set; +import java.util.TreeMap; +import java.util.Vector; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import site.ycsb.BasicTSDB; +import site.ycsb.ByteIterator; +import site.ycsb.Client; +import site.ycsb.DB; +import site.ycsb.NumericByteIterator; +import site.ycsb.Status; +import site.ycsb.StringByteIterator; +import site.ycsb.Utils; +import site.ycsb.Workload; +import site.ycsb.WorkloadException; +import site.ycsb.generator.DiscreteGenerator; +import site.ycsb.generator.Generator; +import site.ycsb.generator.HotspotIntegerGenerator; +import site.ycsb.generator.IncrementingPrintableStringGenerator; +import site.ycsb.generator.NumberGenerator; +import site.ycsb.generator.RandomDiscreteTimestampGenerator; +import site.ycsb.generator.ScrambledZipfianGenerator; +import site.ycsb.generator.SequentialGenerator; +import site.ycsb.generator.UniformLongGenerator; +import site.ycsb.generator.UnixEpochTimestampGenerator; +import site.ycsb.generator.ZipfianGenerator; +import site.ycsb.measurements.Measurements; +import site.ycsb.workloads.core.CoreHelper; +import site.ycsb.wrappers.ByteIteratorWrapper; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +import static site.ycsb.workloads.core.CoreConstants.*; + +/** + * A specialized workload dealing with time series data, i.e. series of discreet + * events associated with timestamps and identifiers. For this workload, identities + * consist of a {@link String} key and a set of {@link String} tag key/value + * pairs. + *

+ * For example: + * + * + * + * + * + * + *
Time Series KeyTag Keys/Values148322880014832288601483228920
AAAA=AA, AB=AA42.51.085.9
AAAA=AA, AB=AB-9.476.90.18
ABAA=AA, AB=AA-93.057.1-63.8
ABAA=AA, AB=AB7.656.1-0.3
+ *

+ * This table shows four time series with 3 measurements at three different timestamps. + * Keys, tags, timestamps and values (numeric only at this time) are generated by + * this workload. For details on properties and behavior, see the + * {@code workloads/tsworkload_template} file. The Javadocs will focus on implementation + * and how {@link DB} clients can parse the workload. + *

+ * In order to avoid having existing DB implementations implement a brand new interface + * this workload uses the existing APIs to encode a few special values that can be parsed + * by the client. The special values include the timestamp, numeric value and some + * query (read or scan) parameters. As an example on how to parse the fields, see + * {@link BasicTSDB}. + *

+ * Timestamps + *

+ * Timestamps are presented as Unix Epoch values in units of {@link TimeUnit#SECONDS}, + * {@link TimeUnit#MILLISECONDS} or {@link TimeUnit#NANOSECONDS} based on the + * {@code timestampunits} property. For calls to {@link DB#insert(String, String, java.util.Map)} + * and {@link DB#update(String, String, java.util.Map)}, the timestamp is added to the + * {@code values} map encoded in a {@link NumericByteIterator} with the key defined + * in the {@code timestampkey} property (defaulting to "YCSBTS"). To pull out the timestamp + * when iterating over the values map, cast the {@link ByteIterator} to a + * {@link NumericByteIterator} and call {@link NumericByteIterator#getLong()}. + *

+ * Note that for calls to {@link DB#update(String, String, java.util.Map)}, timestamps + * earlier than the timestamp generator's timestamp will be choosen at random to + * mimic a lambda architecture or old job re-reporting some data. + *

+ * For calls to {@link DB#read(String, String, java.util.Set, java.util.Map)} and + * {@link DB#scan(String, String, int, java.util.Set, Vector)}, timestamps + * are encoded in a {@link StringByteIterator} in a key/value format with the + * {@code tagpairdelimiter} separator. E.g {@code YCSBTS=1483228800}. If {@code querytimespan} + * has been set to a positive value then the value will include a range with the + * starting (oldest) timestamp followed by the {@code querytimespandelimiter} separator + * and the ending (most recent) timestamp. E.g. {@code YCSBTS=1483228800-1483228920}. + *

+ * For calls to {@link DB#delete(String, String)}, encoding is the same as reads and + * scans but key/value pairs are separated by the {@code deletedelimiter} property value. + *

+ * By default, the starting timestamp is the current system time without any rounding. + * All timestamps are then offsets from that starting value. + *

+ * Values + *

+ * Similar to timestamps, values are encoded in {@link NumericByteIterator}s and stored + * in the values map with the key defined in {@code valuekey} (defaulting to "YCSBV"). + * Values can either be 64 bit signed {@link long}s or double precision {@link double}s + * depending on the {@code valuetype} or {@code dataintegrity} properties. When parsing + * out the value, always call {@link NumericByteIterator#isFloatingPoint()} to determine + * whether or not to call {@link NumericByteIterator#getDouble()} (true) or + * {@link NumericByteIterator#getLong()} (false). + *

+ * When {@code dataintegrity} is set to true, then the value is always set to a + * 64 bit signed integer which is the Java hash code of the concatenation of the + * key and map of values (sorted on the map keys and skipping the timestamp and value + * entries) OR'd with the timestamp of the data point. See + * {@link #validationFunction(String, long, TreeMap)} for the implementation. + *

+ * Keys and Tags + *

+ * As mentioned, the workload generates strings for the keys and tags. On initialization + * three string generators are created using the {@link IncrementingPrintableStringGenerator} + * implementation. Then the generators fill three arrays with values based on the + * number of keys, the number of tags and the cardinality of each tag key/value pair. + * This implementation gives us time series like the example table where every string + * starts at something like "AA" (depending on the length of keys, tag keys and tag values) + * and continuing to "ZZ" wherein they rollover back to "AA". + *

+ * Each time series must have a unique set of tag keys, i.e. the key "AA" cannot appear + * more than once per time series. If the workload is configured for four tags with a + * tag key length of 2, the keys would be "AA", "AB", "AC" and "AD". + *

+ * Each tag key is then associated with a tag value. Tag values may appear more than once + * in each time series. E.g. time series will usually start with the tags "AA=AA", + * "AB=AA", "AC=AA" and "AD=AA". The {@code tagcardinality} property determines how many + * unique values will be generated per tag key. In the example table above, the + * {@code tagcardinality} property would have been set to {@code 1,2} meaning tag + * key "AA" would always have the tag value "AA" given a cardinality of 1. However + * tag key "AB" would have values "AA" and "AB" due to a cardinality of 2. This + * cardinality map, along with the number of unique time series keys determines how + * many unique time series are generated for the workload. Tag values share a common + * array of generated strings to save on memory. + *

+ * Operation Order + *

+ * The default behavior of the workload (for inserts and updates) is to generate a + * value for each time series for a given timestamp before incrementing to the next + * timestamp and writing values. This is an ideal workload and some time series + * databases are designed for this behavior. However in the real-world events will + * arrive grouped close to the current system time with a number of events being + * delayed, hence their timestamps are further in the past. The {@code delayedseries} + * property determines the percentage of time series that are delayed by up to + * {@code delayedintervals} intervals. E.g. setting this value to 0.05 means that + * 5% of the time series will be written with timestamps earlier than the timestamp + * generator's current time. + *

+ * Reads and Scans + *

+ * For benchmarking queries, some common tasks implemented by almost every time series + * data base are available and are passed in the fields {@link Set}: + *

+ * GroupBy - A common operation is to aggregate multiple time series into a + * single time series via common parameters. For example, a user may want to see the + * total network traffic in a data center so they'll issue a SQL query like: + * SELECT value FROM timeseriesdb GROUP BY datacenter ORDER BY SUM(value); + * If the {@code groupbyfunction} has been set to a group by function, then the fields + * will contain a key/value pair with the key set in {@code groupbykey}. E.g. + * {@code YCSBGB=SUM}. + *

+ * Additionally with grouping enabled, fields on tag keys where group bys should + * occur will only have the key defined and will not have a value or delimiter. E.g. + * if grouping on tag key "AA", the field will contain {@code AA} instead of {@code AA=AB}. + *

+ * Downsampling - Another common operation is to reduce the resolution of the + * queried time series when fetching a wide time range of data so fewer data points + * are returned. For example, a user may fetch a week of data but if the data is + * recorded on a 1 second interval, that would be over 600k data points so they + * may ask for a 1 hour downsampling (also called bucketing) wherein every hour, all + * of the data points for a "bucket" are aggregated into a single value. + *

+ * To enable downsampling, the {@code downsamplingfunction} property must be set to + * a supported function such as "SUM" and the {@code downsamplinginterval} must be + * set to a valid time interval with the same units as {@code timestampunits}, e.g. + * "3600" which would create 1 hour buckets if the time units were set to seconds. + * With downsampling, query fields will include a key/value pair with + * {@code downsamplingkey} as the key (defaulting to "YCSBDS") and the value being + * a concatenation of {@code downsamplingfunction} and {@code downsamplinginterval}, + * for example {@code YCSBDS=SUM60}. + *

+ * Timestamps - For every read, a random timestamp is selected from the interval + * set. If {@code querytimespan} has been set to a positive value, then the configured + * query time interval is added to the selected timestamp so the read passes the DB + * a range of times. Note that during the run phase, if no data was previously loaded, + * or if there are more {@code recordcount}s set for the run phase, reads may be sent + * to the DB with timestamps that are beyond the written data time range (or even the + * system clock of the DB). + *

+ * Deletes + *

+ * Because the delete API only accepts a single key, a full key and tag key/value + * pair map is flattened into a single string for parsing by the database. Common + * workloads include deleting a single time series (wherein all tag key and values are + * defined), deleting all series containing a tag key and value or deleting all of the + * time series sharing a common time series key. + *

+ * Right now the workload supports deletes with a key and for time series tag key/value + * pairs or a key with tags and a group by on one or more tags (meaning, delete all of + * the series with any value for the given tag key). The parameters are collapsed into + * a single string delimited with the character in the {@code deletedelimiter} property. + * For example, a delete request may look like: {@code AA:AA=AA:AA=AB} to delete the + * first time series in the table above. + *

+ * Threads + *

+ * For a multi-threaded execution, the number of time series keys set via the + * {@code fieldcount} property, must be greater than or equal to the number of + * threads set via {@code threads}. This is due to each thread choosing a subset + * of the total number of time series keys and being responsible for writing values + * for each time series containing those keys at each timestamp. Thus each thread + * will have it's own timestamp generator, incrementing each time every time series + * it is responsible for has had a value written. + *

+ * Each thread may, however, issue reads and scans for any time series in the + * complete set. + *

+ * Sparsity + *

+ * By default, during loads, every time series will have a data point written at every + * time stamp in the interval set. This is common in workloads where a sensor writes + * a value at regular intervals. However some time series are only reported under + * certain conditions. + *

+ * For example, a counter may track the number of errors over a + * time period for a web service and only report when the value is greater than 1. + * Or a time series may include tags such as a user ID and IP address when a request + * arrives at the web service and only report values when that combination is seen. + * This means the timeseries will not have a value at every timestamp and in + * some cases there may be only a single value! + *

+ * This workload has a {@code sparsity} parameter that can choose how often a + * time series should record a value. The default value of 0.0 means every series + * will get a value at every timestamp. A value of 0.95 will mean that for each + * series, only 5% of the timestamps in the interval will have a value. The distribution + * of values is random. + *

+ * Notes/Warnings + *

+ *

    + *
  • Because time series keys and tag key/values are generated and stored in memory, + * be careful of setting the cardinality too high for the JVM's heap.
  • + *
  • When running for data integrity, a number of settings are incompatible and will + * throw errors. Check the error messages for details.
  • + *
  • Databases that support keys only and can't store tags should order and then + * collapse the tag values using a delimiter. For example the series in the example + * table at the top could be written as: + *
      + *
    • {@code AA.AA.AA}
    • + *
    • {@code AA.AA.AB}
    • + *
    • {@code AB.AA.AA}
    • + *
    • {@code AB.AA.AB}
    • + *
  • + *
+ *

+ * TODOs + *

+ *

    + *
  • Support random time intervals. E.g. some series write every second, others every + * 60 seconds.
  • + *
  • Support random time series cardinality. Right now every series has the same + * cardinality.
  • + *
  • Truly random timetamps per time series. We could use bitmaps to determine if + * a series has had a value written for a given timestamp. Right now all of the series + * are in sync time-wise.
  • + *
  • Possibly a real-time load where values are written with the current system time. + * It's more of a bulk-loading operation now.
  • + *
+ */ +public class TimeSeriesWorkload extends Workload { + + /** + * The types of values written to the timeseries store. + */ + public enum ValueType { + INTEGERS("integers"), + FLOATS("floats"), + MIXED("mixednumbers"); + + protected final String name; + + ValueType(final String name) { + this.name = name; + } + + public static ValueType fromString(final String name) { + for (final ValueType type : ValueType.values()) { + if (type.name.equalsIgnoreCase(name)) { + return type; + } + } + throw new IllegalArgumentException("Unrecognized type: " + name); + } + } + + /** Name and default value for the timestamp key property. */ + public static final String TIMESTAMP_KEY_PROPERTY = "timestampkey"; + public static final String TIMESTAMP_KEY_PROPERTY_DEFAULT = "YCSBTS"; + + /** Name and default value for the value key property. */ + public static final String VALUE_KEY_PROPERTY = "valuekey"; + public static final String VALUE_KEY_PROPERTY_DEFAULT = "YCSBV"; + + /** Name and default value for the timestamp interval property. */ + public static final String TIMESTAMP_INTERVAL_PROPERTY = "timestampinterval"; + public static final String TIMESTAMP_INTERVAL_PROPERTY_DEFAULT = "60"; + + /** Name and default value for the timestamp units property. */ + public static final String TIMESTAMP_UNITS_PROPERTY = "timestampunits"; + public static final String TIMESTAMP_UNITS_PROPERTY_DEFAULT = "SECONDS"; + + /** Name and default value for the number of tags property. */ + public static final String TAG_COUNT_PROPERTY = "tagcount"; + public static final String TAG_COUNT_PROPERTY_DEFAULT = "4"; + + /** Name and default value for the tag value cardinality map property. */ + public static final String TAG_CARDINALITY_PROPERTY = "tagcardinality"; + public static final String TAG_CARDINALITY_PROPERTY_DEFAULT = "1, 2, 4, 8"; + + /** Name and default value for the tag key length property. */ + public static final String TAG_KEY_LENGTH_PROPERTY = "tagkeylength"; + public static final String TAG_KEY_LENGTH_PROPERTY_DEFAULT = "8"; + + /** Name and default value for the tag value length property. */ + public static final String TAG_VALUE_LENGTH_PROPERTY = "tagvaluelength"; + public static final String TAG_VALUE_LENGTH_PROPERTY_DEFAULT = "8"; + + /** Name and default value for the tag pair delimiter property. */ + public static final String PAIR_DELIMITER_PROPERTY = "tagpairdelimiter"; + public static final String PAIR_DELIMITER_PROPERTY_DEFAULT = "="; + + /** Name and default value for the delete string delimiter property. */ + public static final String DELETE_DELIMITER_PROPERTY = "deletedelimiter"; + public static final String DELETE_DELIMITER_PROPERTY_DEFAULT = ":"; + + /** Name and default value for the random timestamp write order property. */ + public static final String RANDOMIZE_TIMESTAMP_ORDER_PROPERTY = "randomwritetimestamporder"; + public static final String RANDOMIZE_TIMESTAMP_ORDER_PROPERTY_DEFAULT = "false"; + + /** Name and default value for the random time series write order property. */ + public static final String RANDOMIZE_TIMESERIES_ORDER_PROPERTY = "randomtimeseriesorder"; + public static final String RANDOMIZE_TIMESERIES_ORDER_PROPERTY_DEFAULT = "true"; + + /** Name and default value for the value types property. */ + public static final String VALUE_TYPE_PROPERTY = "valuetype"; + public static final String VALUE_TYPE_PROPERTY_DEFAULT = "floats"; + + /** Name and default value for the sparsity property. */ + public static final String SPARSITY_PROPERTY = "sparsity"; + public static final String SPARSITY_PROPERTY_DEFAULT = "0.00"; + + /** Name and default value for the delayed series percentage property. */ + public static final String DELAYED_SERIES_PROPERTY = "delayedseries"; + public static final String DELAYED_SERIES_PROPERTY_DEFAULT = "0.10"; + + /** Name and default value for the delayed series intervals property. */ + public static final String DELAYED_INTERVALS_PROPERTY = "delayedintervals"; + public static final String DELAYED_INTERVALS_PROPERTY_DEFAULT = "5"; + + /** Name and default value for the query time span property. */ + public static final String QUERY_TIMESPAN_PROPERTY = "querytimespan"; + public static final String QUERY_TIMESPAN_PROPERTY_DEFAULT = "0"; + + /** Name and default value for the randomized query time span property. */ + public static final String QUERY_RANDOM_TIMESPAN_PROPERTY = "queryrandomtimespan"; + public static final String QUERY_RANDOM_TIMESPAN_PROPERTY_DEFAULT = "false"; + + /** Name and default value for the query time stamp delimiter property. */ + public static final String QUERY_TIMESPAN_DELIMITER_PROPERTY = "querytimespandelimiter"; + public static final String QUERY_TIMESPAN_DELIMITER_PROPERTY_DEFAULT = ","; + + /** Name and default value for the group-by key property. */ + public static final String GROUPBY_KEY_PROPERTY = "groupbykey"; + public static final String GROUPBY_KEY_PROPERTY_DEFAULT = "YCSBGB"; + + /** Name and default value for the group-by function property. */ + public static final String GROUPBY_PROPERTY = "groupbyfunction"; + + /** Name and default value for the group-by key map property. */ + public static final String GROUPBY_KEYS_PROPERTY = "groupbykeys"; + + /** Name and default value for the downsampling key property. */ + public static final String DOWNSAMPLING_KEY_PROPERTY = "downsamplingkey"; + public static final String DOWNSAMPLING_KEY_PROPERTY_DEFAULT = "YCSBDS"; + + /** Name and default value for the downsampling function property. */ + public static final String DOWNSAMPLING_FUNCTION_PROPERTY = "downsamplingfunction"; + + /** Name and default value for the downsampling interval property. */ + public static final String DOWNSAMPLING_INTERVAL_PROPERTY = "downsamplinginterval"; + + /** The properties to pull settings from. */ + protected Properties properties; + + /** Generators for keys, tag keys and tag values. */ + protected Generator keyGenerator; + protected Generator tagKeyGenerator; + protected Generator tagValueGenerator; + + /** The timestamp key, defaults to "YCSBTS". */ + protected String timestampKey; + + /** The value key, defaults to "YCSBDS". */ + protected String valueKey; + + /** The number of time units in between timestamps. */ + protected int timestampInterval; + + /** The units of time the timestamp and various intervals represent. */ + protected TimeUnit timeUnits; + + /** Whether or not to randomize the timestamp order when writing. */ + protected boolean randomizeTimestampOrder; + + /** Whether or not to randomize (shuffle) the time series order. NOT compatible + * with data integrity. */ + protected boolean randomizeTimeseriesOrder; + + /** The type of values to generate when writing data. */ + protected ValueType valueType; + + /** Used to calculate an offset for each time series. */ + protected int[] cumulativeCardinality; + + /** The calculated total cardinality based on the config. */ + protected int totalCardinality; + + /** The calculated per-time-series-key cardinality. I.e. the number of unique + * tag key and value combinations. */ + protected int perKeyCardinality; + + /** How much data to scan for in each call. */ + protected NumberGenerator scanlength; + + /** A generator used to select a random time series key per read/scan. */ + protected NumberGenerator keychooser; + + /** A generator to select what operation to perform during the run phase. */ + protected DiscreteGenerator operationchooser; + + /** The maximum number of interval offsets from the starting timestamp. Calculated + * based on the number of records configured for the run. */ + protected int maxOffsets; + + /** The number of records or operations to perform for this run. */ + protected int recordcount; + + /** The number of tag pairs per time series. */ + protected int tagPairs; + + /** The table we'll write to. */ + protected String table; + + /** How many time series keys will be generated. */ + protected int numKeys; + + /** The generated list of possible time series key values. */ + protected String[] keys; + + /** The generated list of possible tag key values. */ + protected String[] tagKeys; + + /** The generated list of possible tag value values. */ + protected String[] tagValues; + + /** The cardinality for each tag key. */ + protected int[] tagCardinality; + + /** A helper to skip non-incrementing tag values. */ + protected int firstIncrementableCardinality; + + /** How sparse the data written should be. */ + protected double sparsity; + + /** The percentage of time series that should be delayed in writes. */ + protected double delayedSeries; + + /** The maximum number of intervals to delay a series. */ + protected int delayedIntervals; + + /** Optional query time interval during reads/scans. */ + protected int queryTimeSpan; + + /** Whether or not the actual interval should be randomly chosen, using + * queryTimeSpan as the maximum value. */ + protected boolean queryRandomTimeSpan; + + /** The delimiter for tag pairs in fields. */ + protected String tagPairDelimiter; + + /** The delimiter between parameters for the delete key. */ + protected String deleteDelimiter; + + /** The delimiter between timestamps for query time spans. */ + protected String queryTimeSpanDelimiter; + + /** Whether or not to issue group-by queries. */ + protected boolean groupBy; + + /** The key used for group-by tag keys. */ + protected String groupByKey; + + /** The function used for group-by's. */ + protected String groupByFunction; + + /** The tag keys to group on. */ + protected boolean[] groupBys; + + /** Whether or not to issue downsampling queries. */ + protected boolean downsample; + + /** The key used for downsampling tag keys. */ + protected String downsampleKey; + + /** The downsampling function. */ + protected String downsampleFunction; + + /** The downsampling interval. */ + protected int downsampleInterval; + + /** + * Set to true if want to check correctness of reads. Must also + * be set to true during loading phase to function. + */ + protected boolean dataintegrity; + + /** Measurements to write data integrity results to. */ + protected Measurements measurements = Measurements.getMeasurements(); + + @Override + public void init(final Properties p) throws WorkloadException { + properties = p; + recordcount = + Integer.parseInt(p.getProperty(Client.RECORD_COUNT_PROPERTY, + Client.DEFAULT_RECORD_COUNT)); + if (recordcount == 0) { + recordcount = Integer.MAX_VALUE; + } + timestampKey = p.getProperty(TIMESTAMP_KEY_PROPERTY, TIMESTAMP_KEY_PROPERTY_DEFAULT); + valueKey = p.getProperty(VALUE_KEY_PROPERTY, VALUE_KEY_PROPERTY_DEFAULT); + operationchooser = CoreHelper.createOperationGenerator(properties); + + final int maxscanlength = + Integer.parseInt(p.getProperty(MAX_SCAN_LENGTH_PROPERTY, + MAX_SCAN_LENGTH_PROPERTY_DEFAULT)); + String scanlengthdistrib = + p.getProperty(SCAN_LENGTH_DISTRIBUTION_PROPERTY, + SCAN_LENGTH_DISTRIBUTION_PROPERTY_DEFAULT); + + if (scanlengthdistrib.compareTo("uniform") == 0) { + scanlength = new UniformLongGenerator(1, maxscanlength); + } else if (scanlengthdistrib.compareTo("zipfian") == 0) { + scanlength = new ZipfianGenerator(1, maxscanlength); + } else { + throw new WorkloadException( + "Distribution \"" + scanlengthdistrib + "\" not allowed for scan length"); + } + + randomizeTimestampOrder = Boolean.parseBoolean(p.getProperty( + RANDOMIZE_TIMESTAMP_ORDER_PROPERTY, + RANDOMIZE_TIMESTAMP_ORDER_PROPERTY_DEFAULT)); + randomizeTimeseriesOrder = Boolean.parseBoolean(p.getProperty( + RANDOMIZE_TIMESERIES_ORDER_PROPERTY, + RANDOMIZE_TIMESERIES_ORDER_PROPERTY_DEFAULT)); + + // setup the cardinality + numKeys = Integer.parseInt(p.getProperty(FIELD_COUNT_PROPERTY, + FIELD_COUNT_PROPERTY_DEFAULT)); + tagPairs = Integer.parseInt(p.getProperty(TAG_COUNT_PROPERTY, + TAG_COUNT_PROPERTY_DEFAULT)); + sparsity = Double.parseDouble(p.getProperty(SPARSITY_PROPERTY, SPARSITY_PROPERTY_DEFAULT)); + tagCardinality = new int[tagPairs]; + + final String requestdistrib = + p.getProperty(REQUEST_DISTRIBUTION_PROPERTY, + REQUEST_DISTRIBUTION_PROPERTY_DEFAULT); + if (requestdistrib.compareTo("uniform") == 0) { + keychooser = new UniformLongGenerator(0, numKeys - 1); + } else if (requestdistrib.compareTo("sequential") == 0) { + keychooser = new SequentialGenerator(0, numKeys - 1); + } else if (requestdistrib.compareTo("zipfian") == 0) { + keychooser = new ScrambledZipfianGenerator(0, numKeys - 1); + //} else if (requestdistrib.compareTo("latest") == 0) { + // keychooser = new SkewedLatestGenerator(transactioninsertkeysequence); + } else if (requestdistrib.equals("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, numKeys - 1, + hotsetfraction, hotopnfraction); + } else { + throw new WorkloadException("Unknown request distribution \"" + requestdistrib + "\""); + } + + // figure out the start timestamp based on the units, cardinality and interval + try { + timestampInterval = Integer.parseInt(p.getProperty( + TIMESTAMP_INTERVAL_PROPERTY, TIMESTAMP_INTERVAL_PROPERTY_DEFAULT)); + } catch (NumberFormatException nfe) { + throw new WorkloadException("Unable to parse the " + + TIMESTAMP_INTERVAL_PROPERTY, nfe); + } + + try { + timeUnits = TimeUnit.valueOf(p.getProperty(TIMESTAMP_UNITS_PROPERTY, + TIMESTAMP_UNITS_PROPERTY_DEFAULT).toUpperCase()); + } catch (IllegalArgumentException e) { + throw new WorkloadException("Unknown time unit type", e); + } + if (timeUnits == TimeUnit.NANOSECONDS || timeUnits == TimeUnit.MICROSECONDS) { + throw new WorkloadException("YCSB doesn't support " + timeUnits + + " at this time."); + } + + tagPairDelimiter = p.getProperty(PAIR_DELIMITER_PROPERTY, PAIR_DELIMITER_PROPERTY_DEFAULT); + deleteDelimiter = p.getProperty(DELETE_DELIMITER_PROPERTY, DELETE_DELIMITER_PROPERTY_DEFAULT); + dataintegrity = Boolean.parseBoolean( + p.getProperty(DATA_INTEGRITY_PROPERTY, + DATA_INTEGRITY_PROPERTY_DEFAULT)); + if (dataintegrity) { + System.out.println("Data integrity is enabled."); + } + + queryTimeSpan = Integer.parseInt(p.getProperty(QUERY_TIMESPAN_PROPERTY, + QUERY_TIMESPAN_PROPERTY_DEFAULT)); + queryRandomTimeSpan = Boolean.parseBoolean(p.getProperty(QUERY_RANDOM_TIMESPAN_PROPERTY, + QUERY_RANDOM_TIMESPAN_PROPERTY_DEFAULT)); + queryTimeSpanDelimiter = p.getProperty(QUERY_TIMESPAN_DELIMITER_PROPERTY, + QUERY_TIMESPAN_DELIMITER_PROPERTY_DEFAULT); + + groupByKey = p.getProperty(GROUPBY_KEY_PROPERTY, GROUPBY_KEY_PROPERTY_DEFAULT); + groupByFunction = p.getProperty(GROUPBY_PROPERTY); + if (groupByFunction != null && !groupByFunction.isEmpty()) { + final String groupByKeys = p.getProperty(GROUPBY_KEYS_PROPERTY); + if (groupByKeys == null || groupByKeys.isEmpty()) { + throw new WorkloadException("Group by was enabled but no keys were specified."); + } + final String[] gbKeys = groupByKeys.split(","); + if (gbKeys.length != tagKeys.length) { + throw new WorkloadException("Only " + gbKeys.length + " group by keys " + + "were specified but there were " + tagKeys.length + " tag keys given."); + } + groupBys = new boolean[gbKeys.length]; + for (int i = 0; i < gbKeys.length; i++) { + groupBys[i] = Integer.parseInt(gbKeys[i].trim()) == 0 ? false : true; + } + groupBy = true; + } + + downsampleKey = p.getProperty(DOWNSAMPLING_KEY_PROPERTY, DOWNSAMPLING_KEY_PROPERTY_DEFAULT); + downsampleFunction = p.getProperty(DOWNSAMPLING_FUNCTION_PROPERTY); + if (downsampleFunction != null && !downsampleFunction.isEmpty()) { + final String interval = p.getProperty(DOWNSAMPLING_INTERVAL_PROPERTY); + if (interval == null || interval.isEmpty()) { + throw new WorkloadException("'" + DOWNSAMPLING_INTERVAL_PROPERTY + "' was missing despite '" + + DOWNSAMPLING_FUNCTION_PROPERTY + "' being set."); + } + downsampleInterval = Integer.parseInt(interval); + downsample = true; + } + + delayedSeries = Double.parseDouble(p.getProperty(DELAYED_SERIES_PROPERTY, DELAYED_SERIES_PROPERTY_DEFAULT)); + delayedIntervals = Integer.parseInt(p.getProperty(DELAYED_INTERVALS_PROPERTY, DELAYED_INTERVALS_PROPERTY_DEFAULT)); + + valueType = ValueType.fromString(p.getProperty(VALUE_TYPE_PROPERTY, VALUE_TYPE_PROPERTY_DEFAULT)); + table = p.getProperty(TABLENAME_PROPERTY, TABLENAME_PROPERTY_DEFAULT); + initKeysAndTags(); + validateSettings(); + } + + @Override + public Object initThread(Properties p, int mythreadid, int threadcount) throws WorkloadException { + if (properties == null) { + throw new WorkloadException("Workload has not been initialized."); + } + return new ThreadState(mythreadid, threadcount); + } + + @Override + public boolean doInsert(DB db, Object threadstate) { + if (threadstate == null) { + throw new IllegalStateException("Missing thread state."); + } + final Map tags = new TreeMap(); + final String key = ((ThreadState)threadstate).nextDataPoint(tags, true); + final List fields = new ArrayList<>(tags.size()); + tags.entrySet().forEach(entry -> { + final DataWrapper data = ByteIteratorWrapper.create(entry.getValue()); + fields.add(new DatabaseField(entry.getKey(), data)); + }); + if (db.insert(table, key, fields) == Status.OK) { + return true; + } + return false; + } + + @Override + public boolean doTransaction(DB db, Object threadstate) { + if (threadstate == null) { + throw new IllegalStateException("Missing thread state."); + } + switch (operationchooser.nextString()) { + case "READ": + doTransactionRead(db, threadstate); + break; + case "UPDATE": + doTransactionUpdate(db, threadstate); + break; + case "INSERT": + doTransactionInsert(db, threadstate); + break; + case "SCAN": + doTransactionScan(db, threadstate); + break; + case "DELETE": + doTransactionDelete(db, threadstate); + break; + default: + return false; + } + return true; + } + + protected void doTransactionRead(final DB db, Object threadstate) { + final ThreadState state = (ThreadState) threadstate; + final String keyname = keys[keychooser.nextValue().intValue()]; + final Random random = ThreadLocalRandom.current(); + int offsets = state.queryOffsetGenerator.nextValue().intValue(); + //int offsets = random.nextInt(maxOffsets - 1); + final long startTimestamp; + if (offsets > 0) { + startTimestamp = state.startTimestamp + state.timestampGenerator.getOffset(offsets); + } else { + startTimestamp = state.startTimestamp; + } + + // rando tags + Set fields = new HashSet(); + for (int i = 0; i < tagPairs; ++i) { + if (groupBy && groupBys[i]) { + fields.add(tagKeys[i]); + } else { + fields.add(tagKeys[i] + tagPairDelimiter + + tagValues[random.nextInt(tagCardinality[i])]); + } + } + + if (queryTimeSpan > 0) { + final long endTimestamp; + if (queryRandomTimeSpan) { + endTimestamp = startTimestamp + (timestampInterval * random.nextInt(queryTimeSpan / timestampInterval)); + } else { + endTimestamp = startTimestamp + queryTimeSpan; + } + fields.add(timestampKey + tagPairDelimiter + startTimestamp + queryTimeSpanDelimiter + endTimestamp); + } else { + fields.add(timestampKey + tagPairDelimiter + startTimestamp); + } + if (groupBy) { + fields.add(groupByKey + tagPairDelimiter + groupByFunction); + } + if (downsample) { + fields.add(downsampleKey + tagPairDelimiter + downsampleFunction + downsampleInterval); + } + + final Map cells = new HashMap(); + final Status status = db.read(table, keyname, fields, cells); + + if (dataintegrity && status == Status.OK) { + verifyRow(keyname, cells); + } + } + + protected void doTransactionUpdate(final DB db, Object threadstate) { + if (threadstate == null) { + throw new IllegalStateException("Missing thread state."); + } + final Map tags = new TreeMap(); + final String key = ((ThreadState)threadstate).nextDataPoint(tags, false); + db.update(table, key, tags); + } + + protected void doTransactionInsert(final DB db, Object threadstate) { + doInsert(db, threadstate); + } + + protected void doTransactionScan(final DB db, Object threadstate) { + final ThreadState state = (ThreadState) threadstate; + final Random random = ThreadLocalRandom.current(); + final String keyname = keys[random.nextInt(keys.length)]; + + // choose a random scan length + int len = scanlength.nextValue().intValue(); + + int offsets = random.nextInt(maxOffsets - 1); + final long startTimestamp; + if (offsets > 0) { + startTimestamp = state.startTimestamp + state.timestampGenerator.getOffset(offsets); + } else { + startTimestamp = state.startTimestamp; + } + + // rando tags + Set fields = new HashSet(); + for (int i = 0; i < tagPairs; ++i) { + if (groupBy && groupBys[i]) { + fields.add(tagKeys[i]); + } else { + fields.add(tagKeys[i] + tagPairDelimiter + + tagValues[random.nextInt(tagCardinality[i])]); + } + } + + if (queryTimeSpan > 0) { + final long endTimestamp; + if (queryRandomTimeSpan) { + endTimestamp = startTimestamp + (timestampInterval * random.nextInt(queryTimeSpan / timestampInterval)); + } else { + endTimestamp = startTimestamp + queryTimeSpan; + } + fields.add(timestampKey + tagPairDelimiter + startTimestamp + queryTimeSpanDelimiter + endTimestamp); + } else { + fields.add(timestampKey + tagPairDelimiter + startTimestamp); + } + if (groupBy) { + fields.add(groupByKey + tagPairDelimiter + groupByFunction); + } + if (downsample) { + fields.add(downsampleKey + tagPairDelimiter + downsampleFunction + tagPairDelimiter + downsampleInterval); + } + + final Vector> results = new Vector>(); + db.scan(table, keyname, len, fields, results); + } + + protected void doTransactionDelete(final DB db, Object threadstate) { + final ThreadState state = (ThreadState) threadstate; + final Random random = ThreadLocalRandom.current(); + final StringBuilder buf = new StringBuilder().append(keys[random.nextInt(keys.length)]); + + int offsets = random.nextInt(maxOffsets - 1); + final long startTimestamp; + if (offsets > 0) { + startTimestamp = state.startTimestamp + state.timestampGenerator.getOffset(offsets); + } else { + startTimestamp = state.startTimestamp; + } + + // rando tags + for (int i = 0; i < tagPairs; ++i) { + if (groupBy && groupBys[i]) { + buf.append(deleteDelimiter) + .append(tagKeys[i]); + } else { + buf.append(deleteDelimiter).append(tagKeys[i] + tagPairDelimiter + + tagValues[random.nextInt(tagCardinality[i])]); + } + } + + if (queryTimeSpan > 0) { + final long endTimestamp; + if (queryRandomTimeSpan) { + endTimestamp = startTimestamp + (timestampInterval * random.nextInt(queryTimeSpan / timestampInterval)); + } else { + endTimestamp = startTimestamp + queryTimeSpan; + } + buf.append(deleteDelimiter) + .append(timestampKey + tagPairDelimiter + startTimestamp + queryTimeSpanDelimiter + endTimestamp); + } else { + buf.append(deleteDelimiter) + .append(timestampKey + tagPairDelimiter + startTimestamp); + } + + db.delete(table, buf.toString()); + } + + /** + * Parses the values returned by a read or scan operation and determines whether + * or not the integer value matches the hash and timestamp of the original timestamp. + * Only works for raw data points, will not work for group-by's or downsampled data. + * @param key The time series key. + * @param cells The cells read by the DB. + * @return {@link Status#OK} if the data matched or {@link Status#UNEXPECTED_STATE} if + * the data did not match. + */ + protected Status verifyRow(final String key, final Map cells) { + Status verifyStatus = Status.UNEXPECTED_STATE; + long startTime = System.nanoTime(); + + double value = 0; + long timestamp = 0; + final TreeMap validationTags = new TreeMap(); + for (final Entry entry : cells.entrySet()) { + if (entry.getKey().equals(timestampKey)) { + final NumericByteIterator it = (NumericByteIterator) entry.getValue(); + timestamp = it.getLong(); + } else if (entry.getKey().equals(valueKey)) { + final NumericByteIterator it = (NumericByteIterator) entry.getValue(); + value = it.isFloatingPoint() ? it.getDouble() : it.getLong(); + } else { + validationTags.put(entry.getKey(), entry.getValue().toString()); + } + } + + if (validationFunction(key, timestamp, validationTags) == value) { + verifyStatus = Status.OK; + } + long endTime = System.nanoTime(); + measurements.measure("VERIFY", (int) (endTime - startTime) / 1000); + measurements.reportStatus("VERIFY", verifyStatus); + return verifyStatus; + } + + /** + * Function used for generating a deterministic hash based on the combination + * of metric, tags and timestamp. + * @param key A non-null string representing the key. + * @param timestamp A timestamp in the proper units for the workload. + * @param tags A non-null map of tag keys and values NOT including the YCSB + * key or timestamp. + * @return A hash value as an 8 byte integer. + */ + protected long validationFunction(final String key, final long timestamp, + final TreeMap tags) { + final StringBuilder validationBuffer = new StringBuilder(keys[0].length() + + (tagPairs * tagKeys[0].length()) + (tagPairs * tagCardinality[1])); + for (final Entry pair : tags.entrySet()) { + validationBuffer.append(pair.getKey()).append(pair.getValue()); + } + return (long) validationBuffer.toString().hashCode() ^ timestamp; + } + + /** + * Breaks out the keys, tags and cardinality initialization in another method + * to keep CheckStyle happy. + * @throws WorkloadException If something goes pear shaped. + */ + protected void initKeysAndTags() throws WorkloadException { + final int keyLength = Integer.parseInt(properties.getProperty( + FIELD_LENGTH_PROPERTY, + FIELD_LENGTH_PROPERTY_DEFAULT)); + final int tagKeyLength = Integer.parseInt(properties.getProperty( + TAG_KEY_LENGTH_PROPERTY, TAG_KEY_LENGTH_PROPERTY_DEFAULT)); + final int tagValueLength = Integer.parseInt(properties.getProperty( + TAG_VALUE_LENGTH_PROPERTY, TAG_VALUE_LENGTH_PROPERTY_DEFAULT)); + + keyGenerator = new IncrementingPrintableStringGenerator(keyLength); + tagKeyGenerator = new IncrementingPrintableStringGenerator(tagKeyLength); + tagValueGenerator = new IncrementingPrintableStringGenerator(tagValueLength); + + final int threads = Integer.parseInt(properties.getProperty(Client.THREAD_COUNT_PROPERTY, "1")); + final String tagCardinalityString = properties.getProperty( + TAG_CARDINALITY_PROPERTY, + TAG_CARDINALITY_PROPERTY_DEFAULT); + final String[] tagCardinalityParts = tagCardinalityString.split(","); + int idx = 0; + totalCardinality = numKeys; + perKeyCardinality = 1; + int maxCardinality = 0; + for (final String card : tagCardinalityParts) { + try { + tagCardinality[idx] = Integer.parseInt(card.trim()); + } catch (NumberFormatException nfe) { + throw new WorkloadException("Unable to parse cardinality: " + + card, nfe); + } + if (tagCardinality[idx] < 1) { + throw new WorkloadException("Cardinality must be greater than zero: " + + tagCardinality[idx]); + } + totalCardinality *= tagCardinality[idx]; + perKeyCardinality *= tagCardinality[idx]; + if (tagCardinality[idx] > maxCardinality) { + maxCardinality = tagCardinality[idx]; + } + ++idx; + if (idx >= tagPairs) { + // we have more cardinalities than tag keys so bail at this point. + break; + } + } + if (numKeys < threads) { + throw new WorkloadException("Field count " + numKeys + " (keys for time " + + "series workloads) must be greater or equal to the number of " + + "threads " + threads); + } + + // fill tags without explicit cardinality with 1 + if (idx < tagPairs) { + tagCardinality[idx++] = 1; + } + + for (int i = 0; i < tagCardinality.length; ++i) { + if (tagCardinality[i] > 1) { + firstIncrementableCardinality = i; + break; + } + } + + keys = new String[numKeys]; + tagKeys = new String[tagPairs]; + tagValues = new String[maxCardinality]; + for (int i = 0; i < numKeys; ++i) { + keys[i] = keyGenerator.nextString(); + } + + for (int i = 0; i < tagPairs; ++i) { + tagKeys[i] = tagKeyGenerator.nextString(); + } + + for (int i = 0; i < maxCardinality; i++) { + tagValues[i] = tagValueGenerator.nextString(); + } + if (randomizeTimeseriesOrder) { + Utils.shuffleArray(keys); + Utils.shuffleArray(tagValues); + } + + maxOffsets = (recordcount / totalCardinality) + 1; + final int[] keyAndTagCardinality = new int[tagPairs + 1]; + keyAndTagCardinality[0] = numKeys; + for (int i = 0; i < tagPairs; i++) { + keyAndTagCardinality[i + 1] = tagCardinality[i]; + } + + cumulativeCardinality = new int[keyAndTagCardinality.length]; + for (int i = 0; i < keyAndTagCardinality.length; i++) { + int cumulation = 1; + for (int x = i; x <= keyAndTagCardinality.length - 1; x++) { + cumulation *= keyAndTagCardinality[x]; + } + if (i > 0) { + cumulativeCardinality[i - 1] = cumulation; + } + } + cumulativeCardinality[cumulativeCardinality.length - 1] = 1; + } + + /** + * Makes sure the settings as given are compatible. + * @throws WorkloadException If one or more settings were invalid. + */ + protected void validateSettings() throws WorkloadException { + if (dataintegrity) { + if (valueType != ValueType.INTEGERS) { + throw new WorkloadException("Data integrity was enabled. 'valuetype' must " + + "be set to 'integers'."); + } + if (groupBy) { + throw new WorkloadException("Data integrity was enabled. 'groupbyfunction' must " + + "be empty or null."); + } + if (downsample) { + throw new WorkloadException("Data integrity was enabled. 'downsamplingfunction' must " + + "be empty or null."); + } + if (queryTimeSpan > 0) { + throw new WorkloadException("Data integrity was enabled. 'querytimespan' must " + + "be empty or 0."); + } + if (randomizeTimeseriesOrder) { + throw new WorkloadException("Data integrity was enabled. 'randomizetimeseriesorder' must " + + "be false."); + } + final String startTimestamp = properties.getProperty(CoreWorkload.INSERT_START_PROPERTY); + if (startTimestamp == null || startTimestamp.isEmpty()) { + throw new WorkloadException("Data integrity was enabled. 'insertstart' must " + + "be set to a Unix Epoch timestamp."); + } + } + } + + /** + * Thread state class holding thread local generators and indices. + */ + protected class ThreadState { + /** The timestamp generator for this thread. */ + protected final UnixEpochTimestampGenerator timestampGenerator; + + /** An offset generator to select a random offset for queries. */ + protected final NumberGenerator queryOffsetGenerator; + + /** The current write key index. */ + protected int keyIdx; + + /** The starting fence for writing keys. */ + protected int keyIdxStart; + + /** The ending fence for writing keys. */ + protected int keyIdxEnd; + + /** Indices for each tag value for writes. */ + protected int[] tagValueIdxs; + + /** Whether or not all time series have written values for the current timestamp. */ + protected boolean rollover; + + /** The starting timestamp. */ + protected long startTimestamp; + + /** + * Default ctor. + * @param threadID The zero based thread ID. + * @param threadCount The total number of threads. + * @throws WorkloadException If something went pear shaped. + */ + protected ThreadState(final int threadID, final int threadCount) throws WorkloadException { + int totalThreads = threadCount > 0 ? threadCount : 1; + + if (threadID >= totalThreads) { + throw new IllegalStateException("Thread ID " + threadID + " cannot be greater " + + "than or equal than the thread count " + totalThreads); + } + if (keys.length < threadCount) { + throw new WorkloadException("Thread count " + totalThreads + " must be greater " + + "than or equal to key count " + keys.length); + } + + int keysPerThread = keys.length / totalThreads; + keyIdx = keysPerThread * threadID; + keyIdxStart = keyIdx; + if (totalThreads - 1 == threadID) { + keyIdxEnd = keys.length; + } else { + keyIdxEnd = keyIdxStart + keysPerThread; + } + + tagValueIdxs = new int[tagPairs]; // all zeros + + final String startingTimestamp = + properties.getProperty(CoreWorkload.INSERT_START_PROPERTY); + if (startingTimestamp == null || startingTimestamp.isEmpty()) { + timestampGenerator = randomizeTimestampOrder ? + new RandomDiscreteTimestampGenerator(timestampInterval, timeUnits, maxOffsets) : + new UnixEpochTimestampGenerator(timestampInterval, timeUnits); + } else { + try { + timestampGenerator = randomizeTimestampOrder ? + new RandomDiscreteTimestampGenerator(timestampInterval, timeUnits, + Long.parseLong(startingTimestamp), maxOffsets) : + new UnixEpochTimestampGenerator(timestampInterval, timeUnits, + Long.parseLong(startingTimestamp)); + } catch (NumberFormatException nfe) { + throw new WorkloadException("Unable to parse the " + + CoreWorkload.INSERT_START_PROPERTY, nfe); + } + } + // Set the last value properly for the timestamp, otherwise it may start + // one interval ago. + startTimestamp = timestampGenerator.nextValue(); + // TODO - pick it + queryOffsetGenerator = new UniformLongGenerator(0, maxOffsets - 2); + } + + /** + * Generates the next write value for thread. + * @param map An initialized map to populate with tag keys and values as well + * as the timestamp and actual value. + * @param isInsert Whether or not it's an insert or an update. Updates will pick + * an older timestamp (if random isn't enabled). + * @return The next key to write. + */ + protected String nextDataPoint(final Map map, final boolean isInsert) { + final Random random = ThreadLocalRandom.current(); + int iterations = sparsity <= 0 ? 1 : random.nextInt((int) ((double) perKeyCardinality * sparsity)); + if (iterations < 1) { + iterations = 1; + } + while (true) { + iterations--; + if (rollover) { + timestampGenerator.nextValue(); + rollover = false; + } + String key = null; + if (iterations <= 0) { + final TreeMap validationTags; + if (dataintegrity) { + validationTags = new TreeMap(); + } else { + validationTags = null; + } + key = keys[keyIdx]; + int overallIdx = keyIdx * cumulativeCardinality[0]; + for (int i = 0; i < tagPairs; ++i) { + int tvidx = tagValueIdxs[i]; + map.put(tagKeys[i], new StringByteIterator(tagValues[tvidx])); + if (dataintegrity) { + validationTags.put(tagKeys[i], tagValues[tvidx]); + } + if (delayedSeries > 0) { + overallIdx += (tvidx * cumulativeCardinality[i + 1]); + } + } + + if (!isInsert) { + final long delta = (timestampGenerator.currentValue() - startTimestamp) / timestampInterval; + final int intervals = random.nextInt((int) delta); + map.put(timestampKey, new NumericByteIterator(startTimestamp + (intervals * timestampInterval))); + } else if (delayedSeries > 0) { + // See if the series falls in a delay bucket and calculate an offset earlier + // than the current timestamp value if so. + double pct = (double) overallIdx / (double) totalCardinality; + if (pct < delayedSeries) { + int modulo = overallIdx % delayedIntervals; + if (modulo < 0) { + modulo *= -1; + } + map.put(timestampKey, new NumericByteIterator(timestampGenerator.currentValue() - + timestampInterval * modulo)); + } else { + map.put(timestampKey, new NumericByteIterator(timestampGenerator.currentValue())); + } + } else { + map.put(timestampKey, new NumericByteIterator(timestampGenerator.currentValue())); + } + + if (dataintegrity) { + map.put(valueKey, new NumericByteIterator(validationFunction(key, + timestampGenerator.currentValue(), validationTags))); + } else { + switch (valueType) { + case INTEGERS: + map.put(valueKey, new NumericByteIterator(random.nextInt())); + break; + case FLOATS: + map.put(valueKey, new NumericByteIterator(random.nextDouble() * (double) 100000)); + break; + case MIXED: + if (random.nextBoolean()) { + map.put(valueKey, new NumericByteIterator(random.nextInt())); + } else { + map.put(valueKey, new NumericByteIterator(random.nextDouble() * (double) 100000)); + } + break; + default: + throw new IllegalStateException("Somehow we didn't have a value " + + "type configured that we support: " + valueType); + } + } + } + + boolean tagRollover = false; + for (int i = tagCardinality.length - 1; i >= 0; --i) { + if (tagCardinality[i] <= 1) { + tagRollover = true; // Only one tag so needs roll over. + continue; + } + + if (tagValueIdxs[i] + 1 >= tagCardinality[i]) { + tagValueIdxs[i] = 0; + if (i == firstIncrementableCardinality) { + tagRollover = true; + } + } else { + ++tagValueIdxs[i]; + break; + } + } + + if (tagRollover) { + if (keyIdx + 1 >= keyIdxEnd) { + keyIdx = keyIdxStart; + rollover = true; + } else { + ++keyIdx; + } + } + + if (iterations <= 0) { + return key; + } + } + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/site/ycsb/workloads/airport/Airlines.java b/core/src/main/java/site/ycsb/workloads/airport/Airlines.java new file mode 100644 index 0000000..b2fe8f4 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/airport/Airlines.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.airport; + +import java.util.List; + +import site.ycsb.generator.DiscreteGenerator; + +import java.util.LinkedList; + +final class Airline { + final String name; + final String code; + Airline(String code, String name) { + this.name = name; + this.code = code; + } +} +final class AirlineListBuilder { + private final List airlines = new LinkedList<>(); + AirlineListBuilder addAirline(String iata, String name) { + airlines.add(new Airline(iata, name)); + return this; + } + Airline[] build() { + return airlines.toArray(new Airline[airlines.size()]); + } +} + +final class Airlines { + public final static Airline[] ALL_AIRLINES = new AirlineListBuilder() + .addAirline("QH", "Air Florida") + .addAirline("RV", "Air Canada Rouge") + .addAirline("EV", "Atlantic Southeast Airlines") + .addAirline("HP", "America West Airlines") + .addAirline("AA", "American Airlines") + .addAirline("GQ", "Big Sky Airlines") + .addAirline("4B", "Boutique Air") + .addAirline("L9", "Bristow U.S. LLC") + .addAirline("E9", "Boston-Maine Airways") + .addAirline("C6", "CanJet") + .addAirline("W2", "Canadian Western Airlines") + .addAirline("9M", "Central Mountain Air") + .addAirline("QE", "Crossair Europe") + .addAirline("DK", "Eastland Air") + .addAirline("MB", "Execair Aviation") + .addAirline("OW", "Executive Airlines") + .addAirline("EV", "ExpressJet") + .addAirline("7F", "First Air") + .addAirline("F8", "Flair Airlines") + .addAirline("PA", "Florida Coastal Airlines") + .addAirline("RF", "Florida West International Airways") + .addAirline("F7", "Flybaboo") + .addAirline("ST", "Germania") + .addAirline("4U", "Germanwings") + .addAirline("HF", "Hapagfly") + .addAirline("HB", "Harbor Airlines") + .addAirline("HQ", "Harmony Airways") + .addAirline("HA", "Hawaiian Airlines") + .addAirline("2S", "Island Express") + .addAirline("QJ", "Jet Airways") + .addAirline("PP", "Jet Aviation") + .addAirline("3K", "Jetstar Asia Airways") + .addAirline("B6", "JetBlue Airways") + .addAirline("0J", "Jetclub") + .addAirline("LT", "LTU International") + .addAirline("LH", "Lufthansa") + .addAirline("CL", "Lufthansa CityLine") + .addAirline("L1", "Lufthansa Systems") + .addAirline("L2", "Lynden Air Cargo") + .addAirline("Y9", "Lynx Air") + .addAirline("L4", "Lynx Aviation") + .addAirline("BF", "MarkAir") + .addAirline("CC", "Macair Airlines") + .addAirline("YV", "Mesa Airlines") + .addAirline("XJ", "Mesaba Airlines") + .addAirline("LL", "Miami Air International") + .addAirline("OL", "OLT Express Germany") + .addAirline("8P", "Pacific Coastal Airlines") + .addAirline("LW", "Pacific Wings") + .addAirline("KS", "Peninsula Airways") + .addAirline("HP", "Phoenix Airways") + .addAirline("QF", "Qantas") + .addAirline("RW", "Republic Airways") + .addAirline("V2", "Vision Airlines") + .addAirline("7S", "Ryan Air Services") + .addAirline("GG", "Sky Lease Cargo") + .addAirline("SH", "Sharp Airlines") + .addAirline("KV", "Sky Regional Airlines") + .addAirline("BB", "Seaborne Airlines") + .addAirline("SQ", "Singapore Airlines Cargo") + .addAirline("LX", "Swiss International Air Lines") + .addAirline("XG", "Sunexpress Deutschland") + .addAirline("X3", "TUI fly Deutschland") + .addAirline("VX", "Virgin America") + .addAirline("W7", "Western Pacific Airlines") + .addAirline("3J", "Zip") + .build(); + static String nameForKey(String key) { + for(Airline a : ALL_AIRLINES) { + if(a.code.equals(key)) { + return a.name; + } + } + return null; + } + static DiscreteGenerator createAirlinesGenerator() { + final double uniformDistribution = 1 / ((double) ALL_AIRLINES.length); + final DiscreteGenerator generator = new DiscreteGenerator(); + for(Airline a : ALL_AIRLINES) { + generator.addValue(uniformDistribution, a.code); + } + return generator; + } + private Airlines() {} +} diff --git a/core/src/main/java/site/ycsb/workloads/airport/Airplane.java b/core/src/main/java/site/ycsb/workloads/airport/Airplane.java new file mode 100644 index 0000000..a569aad --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/airport/Airplane.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.airport; + +import java.util.List; + +import site.ycsb.generator.DiscreteGenerator; + +import java.util.LinkedList; + +final class Airplane { + final String name; + final String code; + Airplane(String code, String name) { + this.name = name; + this.code = code; + } +} +final class AirplaneListBuilder { + private final List Airplanes = new LinkedList<>(); + AirplaneListBuilder addAirplane(String iata, String name) { + Airplanes.add(new Airplane(iata, name)); + return this; + } + Airplane[] build() { + return Airplanes.toArray(new Airplane[Airplanes.size()]); + } +} + +final class Airplanes { + public final static Airplane[] ALL_AIRCRAFTS = new AirplaneListBuilder() + .addAirplane("A318", "Airbus A318") + .addAirplane("B37M", "Boeing 737 MAX 7") + .addAirplane("B734", "Boeing 737-400") + .addAirplane("C208", "Cessna 208 Caravan") + .addAirplane("CL2T", "Bombardier 415") + .addAirplane("D228", "Dornier 228") + .addAirplane("DC93", "Douglas DC-9-30") + .addAirplane("WW24", "Israel Aircraft Industries 1124 Westwind") + .addAirplane("A388", "Airbus A380-800") + .addAirplane("C130", "\tLockheed L-182 / 282 / 382 (L-100) Hercules") + .build(); + static DiscreteGenerator createAircraftGenerator() { + final double uniformDistribution = 1 / ((double) ALL_AIRCRAFTS.length); + final DiscreteGenerator generator = new DiscreteGenerator(); + for(Airplane a : ALL_AIRCRAFTS) { + generator.addValue(uniformDistribution, a.code); + } + return generator; + } + private Airplanes() {} +} diff --git a/core/src/main/java/site/ycsb/workloads/airport/AirportWorkload.java b/core/src/main/java/site/ycsb/workloads/airport/AirportWorkload.java new file mode 100644 index 0000000..532b7f2 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/airport/AirportWorkload.java @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.airport; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; + +import site.ycsb.ByteIterator; +import site.ycsb.DB; +import site.ycsb.IndexableDB; +import site.ycsb.RandomByteIterator; +import site.ycsb.WorkloadException; +import site.ycsb.generator.DiscreteGenerator; +import site.ycsb.generator.UniformLongGenerator; +import site.ycsb.workloads.CoreWorkload; +import site.ycsb.workloads.core.CoreConstants; +import site.ycsb.workloads.schema.SchemaHolder; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnType; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.ComparisonOperator; +import site.ycsb.wrappers.Comparisons; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; +import site.ycsb.wrappers.Wrappers; + +/** + { + "airline":{ + "name": str, pre-generated 50 unique values, + "alias": str, align with name + }, + "src_airport": str, pre-generated 500 unique values, + "dst_airport": str, pre-generated 500 unique values, + "codeshares": str array, length 0 to 3, from airline aliases + "stops": int, from 0 to 3 + "airplane": str, pre-generated 10 unique values, + "field1": str, random length from 0 to 1000 + } + */ + + /** + * field length: only for field1 + */ + public final class AirportWorkload extends CoreWorkload { + /** + * The name of the property for the proportion of transactions that are deletes. + */ + public static final String FINDONE_PROPORTION_PROPERTY = "findoneproportion"; + + /** + * The default proportion of transactions that are deletes. + */ + public static final String FINDONE_PROPORTION_PROPERTY_DEFAULT = "0.70"; + /** + * The name of the property for the proportion of transactions that are deletes. + */ + public static final String DELETE_PROPORTION_PROPERTY = "deleteproportion"; + + /** + * The default proportion of transactions that are deletes. + */ + public static final String DELETE_PROPORTION_PROPERTY_DEFAULT = "0.05"; + /** + * The default proportion of transactions that are inserted. + */ + public static final String INSERT_PROPORTION_PROPERTY_DEFAULT = "0.05"; + /** + * The default proportion of transactions that are reads. + */ + public static final String READ_PROPORTION_PROPERTY_DEFAULT = "0.00"; + public static final String NESTED_DATA_STRUCTURE_KEY = "nesteddata"; + public static final String NESTED_DATA_STRUCTURE_DEFAULT = "true"; + + private static final String[] FIELD_NAMES = { + "airline_name", + "airline_alias", + "src_airport", + "dst_airport", + "airplane", + "codeshares", + "stops", + "field1" + }; + + protected DiscreteGenerator airportchooser; + protected DiscreteGenerator aircraftchooser; + protected DiscreteGenerator airlinechooser; + protected UniformLongGenerator codeshareslengthgenerator; + protected UniformLongGenerator stopsgenerator; + protected boolean useNestedDataStructure; + + private void registerSchema() { + if(useNestedDataStructure) { + // .addColumn("airline_alias", SchemaColumnType.TEXT) + SchemaHolder.schemaBuilder() + .addEmbeddedColumn("airline", + SchemaHolder.schemaBuilder() + .addColumn("name", SchemaColumnType.TEXT) + .addColumn("alias", SchemaColumnType.TEXT) + .build() + ) + .addColumn("src_airport", SchemaColumnType.TEXT) + .addColumn("dst_airport", SchemaColumnType.TEXT) + .addColumn("airplane", SchemaColumnType.TEXT) + .addArrayColumn("codeshares", SchemaColumnType.INT) + .addColumn("stops", SchemaColumnType.INT) + .addColumn("field1", SchemaColumnType.TEXT) + .register(); + } else { + SchemaHolder.schemaBuilder() + .addColumn("airline_name", SchemaColumnType.TEXT) + .addColumn("airline_alias", SchemaColumnType.TEXT) + .addColumn("src_airport", SchemaColumnType.TEXT) + .addColumn("dst_airport", SchemaColumnType.TEXT) + .addColumn("airplane", SchemaColumnType.TEXT) + .addColumn("codeshares_0", SchemaColumnType.TEXT) + .addColumn("codeshares_1", SchemaColumnType.TEXT) + .addColumn("codeshares_2", SchemaColumnType.TEXT) + .addColumn("stops", SchemaColumnType.INT) + .addColumn("field1", SchemaColumnType.TEXT) + .register(); + } + } + /** + * Initialize the scenario. + * Called once, in the main client thread, before any operations are started. + */ + @Override + public void init(Properties p) throws WorkloadException { + super.init(p); + airlinechooser = Airlines.createAirlinesGenerator(); + aircraftchooser = Airplanes.createAircraftGenerator(); + airportchooser = Airports.createAirportsGenerator(); + codeshareslengthgenerator = new UniformLongGenerator(0, 3); + stopsgenerator = new UniformLongGenerator(0, 3); + fieldnames = new ArrayList(Arrays.asList(FIELD_NAMES)); + useNestedDataStructure = Boolean.parseBoolean( + p.getProperty(NESTED_DATA_STRUCTURE_KEY, NESTED_DATA_STRUCTURE_DEFAULT) + ); + registerSchema(); + operationchooser = createOperationGenerator(p); + } + + private String getDstAirport(String src) { + String dst = null; + do { + dst = airportchooser.nextString(); + } while(dst == null || src.equals(dst)); + return dst; + } + /** + * Builds values for all fields. + */ + @Override + protected List buildValues(String key) { + List values = new ArrayList<>(); + String airlineKey = airlinechooser.nextString(); + String src = airportchooser.nextString(); + String dst = getDstAirport(src); + String plane = aircraftchooser.nextString(); + int codeshareslength = (int) codeshareslengthgenerator.nextValue().longValue(); + List codeshares = new ArrayList<>(); + while(codeshares.size() < codeshareslength) { + String next = airlinechooser.nextValue(); + if(next != null && !next.equals(airlineKey) && !codeshares.contains(next)) { + codeshares.add(next); + } + } + values.add(new DatabaseField("airplane", Wrappers.wrapString(plane))); + values.add(new DatabaseField("src_airport", Wrappers.wrapString(src))); + values.add(new DatabaseField("dst_airport", Wrappers.wrapString(dst))); + values.add(new DatabaseField("stops", Wrappers.wrapInteger(stopsgenerator.nextValue().intValue()))); + values.add(new DatabaseField("field1", Wrappers.wrapIterator(new RandomByteIterator(fieldlengthgenerator.nextValue().longValue())))); + final DataWrapper airlineNameWrapper = Wrappers.wrapString(Airlines.nameForKey(airlineKey)); + final DataWrapper airlineAliasWrapper = Wrappers.wrapString(airlineKey); + final DataWrapper[] csa = new DataWrapper[codeshareslength]; + for(int i = 0; i < codeshareslength; i++) { + csa[i] = Wrappers.wrapString(codeshares.get(i)); + } + if(useNestedDataStructure) { + values.add(new DatabaseField( + "airline", + Wrappers.wrapNested(new DatabaseField[] { + new DatabaseField("name", airlineNameWrapper), + new DatabaseField("alias", airlineAliasWrapper) + } + ))); + values.add(new DatabaseField("codeshares", Wrappers.wrapArray(csa))); + } else { + values.add(new DatabaseField("airline_alias", airlineAliasWrapper)); + values.add(new DatabaseField("airline_name", airlineNameWrapper)); + for(int i = 0; i < csa.length; i++) { + values.add(new DatabaseField("codeshares_" + i, csa[i])); + } + } + // build + return values; + } + + public final void doTransactionDelete(DB db) { + // choose a random key + long keynum = nextKeynum(); + // build the key + String keyname = CoreWorkload.buildKeyName(keynum, zeropadding, orderedinserts); + // delete it + db.delete(table, keyname); + } + + @Override + public void doTransactionUpdate(DB db) { + IndexableDB idb = (IndexableDB) db; + List filters = new ArrayList<>(); + List values = new ArrayList<>(); + final String alias = airlinechooser.nextString(); + // filter by airline.alias + if(useNestedDataStructure) { + filters.add( + Comparisons.createSimpleNestingComparison("airline", + Comparisons.createStringComparison("alias", ComparisonOperator.STRING_EQUAL, alias) + )); + } else { + filters.add(Comparisons.createStringComparison("airline_alias", ComparisonOperator.STRING_EQUAL, alias)); + } + // and update field1 with random string + values.add(new DatabaseField("field1", Wrappers.wrapIterator(new RandomByteIterator(fieldlengthgenerator.nextValue().longValue())))); + idb.updateOne(table, filters, values); + } + + public void doTransactionFind(IndexableDB db) { + // choose a src airport + String src = airportchooser.nextString(); + // choose a dst airport != src airport + String dst = getDstAirport(src); + // choose the max stops + int maxStops = stopsgenerator.nextValue().intValue(); + List values = new ArrayList<>(); + values.add(Comparisons.createStringComparison("src_airport", ComparisonOperator.STRING_EQUAL, src)); + values.add(Comparisons.createStringComparison("dst_airport", ComparisonOperator.STRING_EQUAL, dst)); + values.add(Comparisons.createIntComparison("stops", ComparisonOperator.INT_LTE, maxStops)); + HashMap cells = new HashMap(); + // we always search for all fields + db.findOne(table, values, null, cells); + } + + /** + * Do one transaction operation. Because it will be called concurrently from multiple client + * threads, this function must be thread safe. However, avoid synchronized, or the threads will block waiting + * for each other, and it will be difficult to reach the target throughput. Ideally, this function would + * have no side effects other than DB operations. + */ + @Override + public boolean doTransaction(DB db, Object threadstate) { + String operation = operationchooser.nextString(); + if(operation == null) { + return false; + } + switch (operation) { + case "DELETE": + // Delete: delete_one by _id. This can be implemented by + // deleting the record 5 out of 70 times when a find + // command is executed. + doTransactionDelete(db); + break; + case "INSERT": + // Insert: insert a new record generated in the same way with initial records + doTransactionInsert(db); + break; + case "FINDONE": + // Find: find by src_airport, dest_airport and stops (using $lte), for example: + // {Src_airport: “SEA”, dest_airport: “JFK”, stops: {$lte: 0}} + if(! (db instanceof IndexableDB)) { + throw new IllegalStateException("cannot handle default dbs for find operations"); + } + doTransactionFind((IndexableDB) db); + break; + case "UPDATE": + // probably more a readmodifywrite + // Update: filter by airline.alias and update field1 with random string + doTransactionUpdate(db); + break; + default: + // dont! + // doTransactionReadModifyWrite(db); + } + + return true; + } + /** + * Creates a weighted discrete values with database operations for a workload to perform. + * Weights/proportions are read from the properties list and defaults are used + * when values are not configured. + * Current operations are "READ", "UPDATE", "INSERT", "SCAN" and "READMODIFYWRITE". + * + * @param p The properties list to pull weights from. + * @return A generator that can be used to determine the next operation to perform. + * @throws IllegalArgumentException if the properties object was null. + */ + protected static DiscreteGenerator createOperationGenerator(final Properties p) { + if (p == null) { + throw new IllegalArgumentException("Properties object cannot be null"); + } + final double readproportion = Double.parseDouble( + p.getProperty(CoreConstants.READ_PROPORTION_PROPERTY, "0")); + final double updateproportion = Double.parseDouble( + p.getProperty(CoreConstants.UPDATE_PROPORTION_PROPERTY, "0")); + final double findoneproportion = Double.parseDouble(p.getProperty( + FINDONE_PROPORTION_PROPERTY, "0")); + final double insertproportion = Double.parseDouble( + p.getProperty(CoreConstants.INSERT_PROPORTION_PROPERTY, "0")); + final double deleteproportion = Double.parseDouble( + p.getProperty(DELETE_PROPORTION_PROPERTY, "0")); + /*final double readmodifywriteproportion = Double.parseDouble(p.getProperty( + READMODIFYWRITE_PROPORTION_PROPERTY, READMODIFYWRITE_PROPORTION_PROPERTY_DEFAULT)); + */ + final DiscreteGenerator operationchooser = new DiscreteGenerator(); + if (readproportion > 0) { + operationchooser.addValue(readproportion, "READ"); + } + if (updateproportion > 0) { + operationchooser.addValue(updateproportion, "UPDATE"); + } + if (findoneproportion > 0) { + operationchooser.addValue(findoneproportion, "FINDONE"); + } + if (insertproportion > 0) { + operationchooser.addValue(insertproportion, "INSERT"); + } + if (deleteproportion > 0) { + operationchooser.addValue(deleteproportion, "DELETE"); + } + /* + if (readmodifywriteproportion > 0) { + operationchooser.addValue(readmodifywriteproportion, "READMODIFYWRITE"); + } + */ + return operationchooser; + } +} diff --git a/core/src/main/java/site/ycsb/workloads/airport/Airports.java b/core/src/main/java/site/ycsb/workloads/airport/Airports.java new file mode 100644 index 0000000..1329e40 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/airport/Airports.java @@ -0,0 +1,618 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.airport; + +import java.util.LinkedList; +import java.util.List; + +import site.ycsb.generator.DiscreteGenerator; + +final class Airport { + final String name; + final String code; + Airport(String code, String name) { + this.name = name; + this.code = code; + } +} +final class AirportListBuilder { + private final List airports = new LinkedList<>(); + AirportListBuilder addAirport(String iata, String name) { + airports.add(new Airport(iata, name)); + return this; + } + Airport[] build() { + return airports.toArray(new Airport[airports.size()]); + } +} + +final class Airports { + public final static Airport[] ALL_AIRPORTS = + new AirportListBuilder() + .addAirport("ATL", "ATLANTA GA, US") + .addAirport("PEK", "BEIJING, CN") + .addAirport("LHR", "LONDON, GB") + .addAirport("ORD", "CHICAGO IL, US") + .addAirport("HND", "TOKYO, JP") + .addAirport("LAX", "LOS ANGELES CA, US") + .addAirport("CDG", "PARIS, FR") + .addAirport("DFW", "DALLAS/FORT WORTH TX, US") + .addAirport("FRA", "FRANKFURT, DE") + .addAirport("HKG", "HONG KONG, HK") + .addAirport("DEN", "DENVER CO, US") + .addAirport("DXB", "DUBAI, AE") + .addAirport("CGK", "JAKARTA, ID") + .addAirport("AMS", "AMSTERDAM, NL") + .addAirport("MAD", "MADRID, ES") + .addAirport("BKK", "BANGKOK, TH") + .addAirport("JFK", "NEW YORK NY, US") + .addAirport("SIN", "SINGAPORE, SG") + .addAirport("CAN", "GUANGZHOU, CN") + .addAirport("LAS", "LAS VEGAS NV, US") + .addAirport("PVG", "SHANGHAI, CN") + .addAirport("SFO", "SAN FRANCISCO CA, US") + .addAirport("PHX", "PHOENIX AZ, US") + .addAirport("IAH", "HOUSTON TX, US") + .addAirport("CLT", "CHARLOTTE NC, US") + .addAirport("MIA", "MIAMI FL, US") + .addAirport("MUC", "MUNICH, DE") + .addAirport("KUL", "KUALA LUMPUR, MY") + .addAirport("FCO", "ROME, IT") + .addAirport("IST", "ISTANBUL, TR") + .addAirport("SYD", "SYDNEY, AU") + .addAirport("MCO", "ORLANDO FL, US") + .addAirport("ICN", "INCHEON, KR") + .addAirport("DEL", "NEW DELHI, IN") + .addAirport("BCN", "BARCELONA, ES") + .addAirport("EWR", "NEWARK NJ, US") + .addAirport("YYZ", "TORONTO ON, CA") + .addAirport("LGW", "LONDON, GB") + .addAirport("SHA", "SHANGHAI, CN") + .addAirport("MSP", "MINNEAPOLIS MN, US") + .addAirport("SEA", "SEATTLE WA, US") + .addAirport("DTW", "DETROIT MI, US") + .addAirport("PHL", "PHILADELPHIA PA, US") + .addAirport("BOM", "MUMBAI, IN") + .addAirport("GRU", "SAO PAULO, BR") + .addAirport("MNL", "MANILA, PH") + .addAirport("CTU", "CHENGDU, CN") + .addAirport("BOS", "BOSTON MA, US") + .addAirport("SZX", "SHENZHEN, CN") + .addAirport("MEL", "MELBOURNE, AU") + .addAirport("NRT", "TOKYO, JP") + .addAirport("ORY", "PARIS, FR") + .addAirport("MEX", "MEXICO CITY, MX") + .addAirport("DME", "MOSCOW, RU") + .addAirport("AYT", "ANTALYA, TR") + .addAirport("TPE", "TAIPEI, TW") + .addAirport("ZRH", "ZURICH, CH") + .addAirport("LGA", "NEW YORK NY, US") + .addAirport("FLL", "FORT LAUDERDALE, FL, US") + .addAirport("IAD", "WASHINGTON, DC, US") + .addAirport("PMI", "PALMA DE MALLORCA, ES") + .addAirport("CPH", "COPENHAGEN, DK") + .addAirport("SVO", "MOSCOW, RU") + .addAirport("BWI", "BALTIMORE MD, US") + .addAirport("KMG", "KUNMING, CN") + .addAirport("VIE", "VIENNA, AT") + .addAirport("OSL", "OSLO, NO") + .addAirport("JED", "JEDDAH, SA") + .addAirport("BNE", "BRISBANE, AU") + .addAirport("SLC", "SALT LAKE CITY UT, US") + .addAirport("DUS", "DÜSSELDORF, DE") + .addAirport("BOG", "BOGOTA, CO") + .addAirport("MXP", "MILAN, IT") + .addAirport("JNB", "JOHANNESBURG, ZA") + .addAirport("ARN", "STOCKHOLM, SE") + .addAirport("MAN", "MANCHESTER, GB") + .addAirport("MDW", "CHICAGO IL, US") + .addAirport("DCA", "WASHINGTON DC, US") + .addAirport("BRU", "BRUSSELS, BE") + .addAirport("DUB", "DUBLIN, IE") + .addAirport("GMP", "SEOUL, KR") + .addAirport("DOH", "DOHA, QA") + .addAirport("STN", "LONDON, GB") + .addAirport("HGH", "HANGZHOU, CN") + .addAirport("CJU", "JEJU, KR") + .addAirport("YVR", "VANCOUVER BC, CA") + .addAirport("TXL", "BERLIN, DE") + .addAirport("SAN", "SAN DIEGO CA, US") + .addAirport("TPA", "TAMPA FL, US") + .addAirport("CGH", "SÃO PAULO, BR") + .addAirport("BSB", "BRASILIA, BR") + .addAirport("CTS", "SAPPORO, JP") + .addAirport("XMN", "XIAMEN, CN") + .addAirport("RUH", "RIYADH, SA") + .addAirport("FUK", "FUKUOKA, JP") + .addAirport("GIG", "RIO DE JANEIRO, BR") + .addAirport("HEL", "HELSINKI, FI") + .addAirport("LIS", "LISBON, PT") + .addAirport("ATH", "ATHENS, GR") + .addAirport("AKL", "AUCKLAND, NZ") + .addAirport("AAA", "Anaa Airport") + .addAirport("AAB", "Arrabury Airport") + .addAirport("AAC", "El Arish International Airport") + .addAirport("AAO", "Anaco Airport") + .addAirport("AAP", "Aji Pangeran Tumenggung Pranoto International Airport") + .addAirport("AAQ", "Anapa Airport") + .addAirport("AAX", "Araxá Airport") + .addAirport("AAY", "Al Ghaydah Airport") + .addAirport("ABE", "Lehigh Valley International Airport") + .addAirport("ABF", "Abaiang Atoll Airport") + .addAirport("ABN", "Albina Airstrip") + .addAirport("ABO", "Aboisso Airport") + .addAirport("ABP", "Atkamba Airport") + .addAirport("ABQ", "Albuquerque International Sunport") + .addAirport("ABV", "Nnamdi Azikiwe International Airport") + .addAirport("ABW", "Abau Airport") + .addAirport("ACE", "Lanzarote Airport") + .addAirport("ACK", "Nantucket Memorial Airport") + .addAirport("ACS", "Achinsk Airport") + .addAirport("ADA", "Adana Şakirpaşa Airport") + .addAirport("ADH", "Aldan Airport") + .addAirport("ADI", "Arandis Airport") + .addAirport("ADN", "Andes Airport") + .addAirport("ADS", "Addison Airport") + .addAirport("ADY", "Alldays Airport") + .addAirport("AEM", "Amgu Airport") + .addAirport("AER", "Sochi International Airport") + .addAirport("AES", "Ålesund Airport, Vigra") + .addAirport("AFS", "Zarafshan Airport") + .addAirport("AFT", "Afutara Airport") + .addAirport("AGE", "Wangerooge Airfield") + .addAirport("AGF", "Agen La Garenne Airport") + .addAirport("AGK", "Kagua Airport") + .addAirport("AGL", "Wanigela Airport") + .addAirport("AGQ", "Agrinion Airport") + .addAirport("AGR", "Agra Airport") + .addAirport("AGU", "Lic. Jesús Terán Peredo International Airport") + .addAirport("AHF", "Arapahoe Municipal Airport (FAA: 37V)") + .addAirport("AHL", "Aishalton Airport") + .addAirport("AHU", "Cherif Al Idrissi Airport") + .addAirport("AHW", "Saih Rawl Airport") + .addAirport("AID", "Anderson Municipal Airport (Darlington Field)") + .addAirport("AIE", "Aiome Airport") + .addAirport("AIK", "Aiken Municipal Airport") + .addAirport("AIO", "Atlantic Municipal Airport") + .addAirport("AIP", "Adampur Airport") + .addAirport("AIV", "George Downer Airport") + .addAirport("AIW", "Ai-Ais Airport") + .addAirport("AJI", "Ağrı Airport") + .addAirport("AJN", "Ouani Airport") + .addAirport("AKE", "Akieni Airport") + .addAirport("AKI", "Akiak Airport") + .addAirport("AKN", "King Salmon Airport") + .addAirport("AKO", "Colorado Plains Regional Airport") + .addAirport("AKT", "RAF Akrotiri") + .addAirport("ALD", "Alerta Airport") + .addAirport("ALI", "Alice International Airport") + .addAirport("ALJ", "Alexander Bay Airport") + .addAirport("ALN", "St. Louis Regional Airport") + .addAirport("ALO", "Waterloo Regional Airport") + .addAirport("AMC", "Am Timan Airport") + .addAirport("AMG", "Amboin Airport") + .addAirport("AMQ", "Pattimura Airport") + .addAirport("AMS", "Amsterdam Airport Schiphol") + .addAirport("AMT", "Amata Airport") + .addAirport("AMX", "Ammaroo Airport") + .addAirport("ANB", "Anniston Regional Airport") + .addAirport("ANI", "Aniak Airport") + .addAirport("ANJ", "Zanaga Airport") + .addAirport("ANO", "Angoche Airport") + .addAirport("ANS", "Andahuaylas Airport") + .addAirport("ANW", "Ainsworth Regional Airport") + .addAirport("AOC", "Leipzig–Altenburg Airport") + .addAirport("AOD", "Abou-Deïa Airport") + .addAirport("AOE", "Anadolu Airport") + .addAirport("AOI", "Ancona Falconara Airport") + .addAirport("AON", "Arona Airport") + .addAirport("AOO", "Altoona–Blair County Airport") + .addAirport("AOT", "Aosta Valley Airport") + .addAirport("APC", "Napa County Airport") + .addAirport("APF", "Naples Municipal Airport") + .addAirport("API", "Captain Luis F. Gómez Niño Air Base") + .addAirport("APO", "Antonio Roldán Betancourt Airport") + .addAirport("APP", "Asapa Airport") + .addAirport("APV", "Apple Valley Airport") + .addAirport("APW", "Faleolo International Airport") + .addAirport("AQI", "Al Qaisumah/Hafr Al Batin Airport") + .addAirport("ARC", "Arctic Village Airport") + .addAirport("ARD", "Alor Island Airport") + .addAirport("ARK", "Arusha Airport") + .addAirport("ARL", "Arly Airport") + .addAirport("ARQ", "El Troncal Airport") + .addAirport("ARU", "Araçatuba Airport") + .addAirport("ARY", "Ararat Airport") + .addAirport("ASD", "Andros Town International Airport") + .addAirport("ASI", "RAF Ascension Island (Wideawake Field)") + .addAirport("ASU", "Silvio Pettirossi International Airport") + .addAirport("ASY", "Ashley Municipal Airport") + .addAirport("ATG", "PAF Base Minhas") + .addAirport("ATK", "Atqasuk Edward Burnell Sr. Memorial Airport") + .addAirport("ATQ", "Sri Guru Ram Dass Jee International Airport") + .addAirport("ATV", "Ati Airport") + .addAirport("ATW", "Appleton International Airport") + .addAirport("ATX", "Atbasar Airport") + .addAirport("AUC", "Santiago Pérez Quiroz Airport") + .addAirport("AUD", "Augustus Downs Airport") + .addAirport("AUH", "Abu Dhabi International Airport") + .addAirport("AUI", "Aua Island Airport") + .addAirport("AUL", "Aur Airport") + .addAirport("AUN", "Auburn Municipal Airport") + .addAirport("AUO", "Auburn University Regional Airport") + .addAirport("AUU", "Aurukun Airport") + .addAirport("AUY", "Anatom Airport") + .addAirport("AVB", "Aviano Air Base") + .addAirport("AVG", "Auvergne Airport") + .addAirport("AVK", "Arvaikheer Airport") + .addAirport("AVU", "Avu Avu Airport") + .addAirport("AWA", "Awasa Airport") + .addAirport("AWE", "Alowe Airport") + .addAirport("AWP", "Austral Downs Airport") + .addAirport("AXC", "Aramac Airport") + .addAirport("AXD", "Alexandroupoli Airport (Dimokritos Airport)") + .addAirport("AXE", "Xanxere Airport") + .addAirport("AXF", "Alxa Left Banner Bayanhot Airport") + .addAirport("AXM", "El Edén International Airport") + .addAirport("AXS", "Altus/Quartz Mountain Regional Airport") + .addAirport("AXV", "Neil Armstrong Airport") + .addAirport("AYD", "Alroy Downs Airport") + .addAirport("AYG", "Yaguara Airport") + .addAirport("AYM", "Yas Island Seaplane Base") + .addAirport("AYP", "Coronel FAP Alfredo Mendívil Duarte Airport") + .addAirport("AYQ", "Ayers Rock Airport") + .addAirport("AYS", "Waycross–Ware County Airport") + .addAirport("AYT", "Antalya Airport") + .addAirport("AYX", "Tnte. Gral. Gerardo Pérez Pinedo Airport") + .addAirport("AYY", "Arugam Bay Seaplane Base") + .addAirport("AZG", "Pablo L. Sidar Airport") + .addAirport("AZI", "Al Bateen Executive Airport") + .addAirport("AZL", "Fazenda Tucunaré Airport") + .addAirport("AZT", "Zapatoca Airport") + .addAirport("AZZ", "Ambriz Airport") + .addAirport("SAA", "Shively Field") + .addAirport("SAB", "Juancho E. Yrausquin Airport") + .addAirport("SQW", "Skive Airport") + .addAirport("SQX", "Hélio Wasum Airport") + .addAirport("SQY", "São Lourenço do Sul Airport") + .addAirport("SUO", "Sunriver Airport (FAA: S21)") + .addAirport("SUP", "Trunojoyo Airport") + .addAirport("SUV", "Nausori International Airport") + .addAirport("SVF", "Savé Airport") + .addAirport("SVG", "Stavanger Airport, Sola") + .addAirport("SVH", "Statesville Regional Airport") + .addAirport("SVI", "Eduardo Falla Solano Airport") + .addAirport("SVJ", "Svolvær Airport, Helle") + .addAirport("SVK", "Silver Creek Airport") + .addAirport("SVL", "Savonlinna Airport") + .addAirport("SVM", "St Pauls Airport") + .addAirport("SVZ", "Juan Vicente Gómez International Airport") + .addAirport("SWC", "Stawell Airport") + .addAirport("SWD", "Seward Airport") + .addAirport("SWE", "Siwea Airport") + .addAirport("SWF", "Stewart International Airport") + .addAirport("SWG", "Satwag Airport") + .addAirport("SWH", "Swan Hill Airport") + .addAirport("SWJ", "South West Bay Airport") + .addAirport("SWL", "San Vicente Airport") + .addAirport("SWM", "Suia-Missu Airport") + .addAirport("SWR", "Silur Airport") + .addAirport("SWS", "Swansea Airport") + .addAirport("SWT", "Strezhevoy Airport") + .addAirport("SWU", "Suwon Air Base") + .addAirport("SXA", "Sialum Airport") + .addAirport("SXB", "Strasbourg Airport") + .addAirport("SXE", "West Sale Airport") + .addAirport("SXG", "Senanga Airport") + .addAirport("SXH", "Sehulea Airport") + .addAirport("SXI", "Sirri Island Airport") + .addAirport("SXJ", "Shanshan Airport") + .addAirport("SXK", "Saumlaki Airport (Olilit Airport)") + .addAirport("SXL", "Sligo Airport") + .addAirport("SXM", "Princess Juliana International Airport") + .addAirport("SXN", "Sua Pan Airport") + .addAirport("SXO", "São Félix do Araguaia Airport") + .addAirport("SXP", "Sheldon Point Airport") + .addAirport("SXQ", "Soldotna Airport") + .addAirport("SXR", "Sheikh ul-Alam International Airport") + .addAirport("SXS", "Sahabat Airport") + .addAirport("SXT", "Sungai Tiang Airport") + .addAirport("SXU", "Soddu Airport") + .addAirport("SXV", "Salem Airport") + .addAirport("SXW", "Sauren Airport") + .addAirport("SXX", "São Félix do Xingu Airport") + .addAirport("SXY", "Sidney Municipal Airport (FAA: N23)") + .addAirport("SXZ", "Siirt Airport") + .addAirport("SYD", "Sydney Airport (Kingsford Smith Airport)") + .addAirport("SYE", "Saadah Airport") + .addAirport("SYF", "Silva Bay Seaplane Base") + .addAirport("SYI", "Shelbyville Municipal Airport (Bomar Field)") + .addAirport("SYJ", "Sirjan Airport") + .addAirport("SYK", "Stykkishólmur Airport") + .addAirport("SYN", "Stanton Airfield") + .addAirport("SYO", "Shonai Airport") + .addAirport("SYP", "Ruben Cantu Airport") + .addAirport("SYQ", "Tobías Bolaños International Airport") + .addAirport("SYR", "Syracuse Hancock International Airport") + .addAirport("SYS", "Saskylakh Airport") + .addAirport("SYT", "Saint-Yan Airport (Charolais Bourgogne Sud Airport)") + .addAirport("SYU", "Warraber Island Airport") + .addAirport("SYV", "Sylvester Airport") + .addAirport("SYW", "Sehwan Sharif Airport") + .addAirport("SYX", "Sanya Phoenix International Airport") + .addAirport("SYY", "Stornoway Airport") + .addAirport("SZA", "Soyo Airport") + .addAirport("SZB", "Sultan Abdul Aziz Shah Airport") + .addAirport("SZE", "Semera Airport") + .addAirport("SZF", "Samsun-Çarşamba Airport") + .addAirport("SZG", "Salzburg Airport") + .addAirport("SZI", "Zaysan Airport") + .addAirport("SZJ", "Siguanea Airport") + .addAirport("SZK", "Skukuza Airport") + .addAirport("SZL", "Whiteman Air Force Base") + .addAirport("SZM", "Sesriem Airport") + .addAirport("SZN", "Santa Cruz Island Airport") + .addAirport("SZP", "Santa Paula Airport") + .addAirport("SZR", "Stara Zagora Airport") + .addAirport("SZU", "Ségou Airport") + .addAirport("SZV", "Suzhou Guangfu Airport") + .addAirport("SZW", "Schwerin-Parchim International Airport") + .addAirport("SZX", "Shenzhen Bao'an International Airport") + .addAirport("SZY", "Olsztyn-Mazury Regional Airport") + .addAirport("TAF", "Oran Tafaraoui Airport") + .addAirport("TAG", "Bohol–Panglao International Airport") + .addAirport("TAH", "Whitegrass Airport") + .addAirport("TAI", "Taiz International Airport") + .addAirport("TAQ", "Tarcoola Airport") + .addAirport("TAU", "Tauramena Airport") + .addAirport("TBB", "Dong Tac Airport") + .addAirport("TBH", "Tugdan Airport") + .addAirport("TBL", "Tableland Homestead Airport") + .addAirport("TBP", "Cap. FAP Pedro Canga Rodríguez Airport") + .addAirport("TBQ", "Tarabo Airport") + .addAirport("TBW", "Tambov Donskoye Airport") + .addAirport("TBY", "Tshabong Airport") + .addAirport("TCE", "Tulcea Danube Delta Airport") + .addAirport("TCI", "metropolitan area1") + .addAirport("TCL", "Tuscaloosa Regional Airport") + .addAirport("TCT", "Takotna Airport") + .addAirport("TCU", "Thaba Nchu Airport") + .addAirport("TDB", "Tetebedi Airport") + .addAirport("TDR", "Theodore Airport") + .addAirport("TDW", "Tradewind Airport") + .addAirport("TDX", "Trat Airport") + .addAirport("TEE", "Cheikh Larbi Tébessi Airport") + .addAirport("TEF", "Telfer Airport") + .addAirport("TEM", "Temora Airport") + .addAirport("TEN", "Tongren Fenghuang Airport") + .addAirport("TEV", "Teruel Airport") + .addAirport("TEX", "Telluride Regional Airport") + .addAirport("TFI", "Tufi Airport") + .addAirport("TFL", "Teófilo Otoni Airport (Juscelino Kubitscheck Airport)") + .addAirport("TGB", "Tagbita Airport") + .addAirport("TGH", "Tongoa Airport") + .addAirport("TGM", "Târgu Mureș International Airport") + .addAirport("TGS", "Chokwe Airport") + .addAirport("TGT", "Tanga Airport") + .addAirport("THB", "Thaba Tseka Airport") + .addAirport("THI", "Tichitt Airport") + .addAirport("THK", "Thakhek Airport") + .addAirport("THU", "Pituffik Space Base") + .addAirport("TIA", "Tirana International Airport Nënë Tereza") + .addAirport("TIM", "Mozes Kilangin Airport") + .addAirport("TIN", "Tindouf Airport") + .addAirport("TIU", "Richard Pearse Airport") + .addAirport("TJB", "Sei Bati Airport") + .addAirport("TJL", "Plínio Alarcom Airport") + .addAirport("TJM", "Roshchino International Airport") + .addAirport("TKA", "Talkeetna Airport") + .addAirport("TKG", "Radin Inten II Airport") + .addAirport("TMN", "Tamana Airport") + .addAirport("TMW", "Tamworth Airport") + .addAirport("TNC", "Tin City LRRS Airport") + .addAirport("TOG", "Togiak Airport") + .addAirport("TOH", "Torres Airport") + .addAirport("TOM", "Timbuktu Airport") + .addAirport("TOT", "Totness Airstrip") + .addAirport("TPA", "Tampa International Airport") + .addAirport("TPI", "Tapini Airport") + .addAirport("TPQ", "Amado Nervo International Airport") + .addAirport("TQQ", "Maranggo Airport") + .addAirport("TRQ", "José Galera dos Santos Airport") + .addAirport("TRR", "China Bay Airport") + .addAirport("TRS", "Trieste – Friuli Venezia Giulia Airport (Ronchi dei Legionari Airport)") + .addAirport("TSN", "Tianjin Binhai International Airport") + .addAirport("TTB", "Tortolì Airport (Arbatax Airport)") + .addAirport("TTI", "Tetiaroa Airport") + .addAirport("TTT", "Taitung Airport (Taitung Fongnian Airport)") + .addAirport("TTU", "Sania Ramel Airport") + .addAirport("TUM", "Tumut Airport") + .addAirport("TUN", "Tunis–Carthage International Airport") + .addAirport("TUO", "Taupo Airport") + .addAirport("TUP", "Tupelo Regional Airport") + .addAirport("TUQ", "Tougan Airport") + .addAirport("TVI", "Thomasville Regional Airport") + .addAirport("TWP", "Torwood Airport") + .addAirport("TWT", "Sanga-Sanga Airport") + .addAirport("TWU", "Tawau Airport") + .addAirport("TXM", "Teminabuan Airport") + .addAirport("TXU", "Tabou Airport") + .addAirport("TYE", "Tyonek Airport") + .addAirport("TYN", "Taiyuan Wusu International Airport") + .addAirport("TYS", "McGhee Tyson Airport") + .addAirport("TZC", "Tuscola Area Airport (FAA: CFS)") + .addAirport("VAA", "Vaasa Airport") + .addAirport("VAB", "Yavarate Airport") + .addAirport("VAI", "Vanimo Airport") + .addAirport("VAN", "Van Ferit Melen Airport") + .addAirport("VAO", "Suavanao Airport") + .addAirport("VAT", "Vatomandry Airport") + .addAirport("VAU", "Vatukoula Airport") + .addAirport("VBG", "Vandenberg Air Force Base") + .addAirport("VBP", "Bokpyin Airport") + .addAirport("VCD", "Victoria River Downs Airport") + .addAirport("VCR", "Carora Airport") + .addAirport("VDB", "Fagernes Airport, Leirin") + .addAirport("VDH", "Dong Hoi Airport") + .addAirport("VDR", "Villa Dolores Airport") + .addAirport("VEE", "Venetie Airport") + .addAirport("VGO", "Vigo–Peinador Airport") + .addAirport("VGZ", "Villa Garzón Airport") + .addAirport("VIC", "Vicenza Airport") + .addAirport("VIJ", "Virgin Gorda Airport") + .addAirport("VJQ", "Gurúé Airport") + .addAirport("VKS", "Vicksburg Municipal Airport") + .addAirport("VLD", "Valdosta Regional Airport") + .addAirport("VNE", "Meucon Airport") + .addAirport("VOG", "Volgograd International Airport") + .addAirport("VOL", "Nea Anchialos National Airport") + .addAirport("VPS", "Destin–Fort Walton Beach Airport / Eglin Air Force Base") + .addAirport("VRL", "Vila Real Airport") + .addAirport("VRN", "Verona Villafranca Airport") + .addAirport("VST", "Stockholm Västerås Airport") + .addAirport("VTL", "Vittel - Champ-de-Courses Airport") + .addAirport("VUP", "Alfonso López Pumarejo Airport") + .addAirport("VVB", "Mahanoro Airport") + .addAirport("VVC", "La Vanguardia Airport") + .addAirport("VXC", "Lichinga Airport") + .addAirport("VYI", "Vilyuysk Airport") + .addAirport("WAR", "Waris Airport") + .addAirport("WBG", "Schleswig Air Base") + .addAirport("WBK", "West Branch Community Airport") + .addAirport("WBM", "Wapenamanda Airport") + .addAirport("WDN", "Waldronaire Airport") + .addAirport("WET", "Waghete Airport") + .addAirport("WEW", "Wee Waa Airport") + .addAirport("WGC", "Warangal Airport") + .addAirport("WGE", "Walgett Airport") + .addAirport("WHF", "Wadi Halfa Airport") + .addAirport("WHK", "Whakatane Airport") + .addAirport("WHL", "Welshpool Airport") + .addAirport("WHO", "Franz Josef Glacier Aerodrome") + .addAirport("WHP", "Whiteman Airport") + .addAirport("WHS", "Whalsay Airstrip") + .addAirport("WIE", "Wiesbaden Army Airfield") + .addAirport("WIK", "Waiheke Island Aerodrome") + .addAirport("WIL", "Wilson Airport") + .addAirport("WIT", "Wittenoom Airport") + .addAirport("WLC", "Walcha Airport") + .addAirport("WML", "Malaimbandy Airport") + .addAirport("WMN", "Maroantsetra Airport") + .addAirport("WRE", "Whangarei Airport") + .addAirport("WRG", "Wrangell Airport") + .addAirport("WRI", "McGuire Air Force Base") + .addAirport("WRL", "Worland Municipal Airport") + .addAirport("WRN", "Windarling Airport") + .addAirport("WRO", "Copernicus Airport Wrocław") + .addAirport("WRT", "Warton Aerodrome") + .addAirport("WSB", "Steamboat Bay Seaplane Base") + .addAirport("WSK", "Chongqing Wushan Airport") + .addAirport("WSM", "Wiseman Airport") + .addAirport("WUH", "Wuhan Tianhe International Airport") + .addAirport("WZA", "Wa Airport") + .addAirport("WZQ", "Urad Middle Banner Airport") + .addAirport("XAI", "Xinyang Minggang Airport") + .addAirport("XAL", "Álamos Airport") + .addAirport("XAP", "Serafin Enoss Bertaso Airport") + .addAirport("XAR", "Aribinda Airport") + .addAirport("XAU", "Saül Airport") + .addAirport("XBB", "Blubber Bay Seaplane Base") + .addAirport("XBE", "Bearskin Lake Airport (TC: CNE3)") + .addAirport("XBG", "Bogande Airport") + .addAirport("XBJ", "Birjand International Airport") + .addAirport("XBL", "Bedele Airport (Buno Bedele Airport)") + .addAirport("XBN", "Biniguni Airport") + .addAirport("XBO", "Boulsa Airport") + .addAirport("XBR", "Brockville Regional Tackaberry Airport (TC: CNL3)") + .addAirport("XCR", "Châlons Vatry Airport") + .addAirport("XEN", "Xingcheng Airport") + .addAirport("XES", "Grand Geneva Resort Airport (FAA: C02)") + .addAirport("XGA", "Gaoua Airport (Amilcar Cabral Airport)") + .addAirport("XGG", "Gorom Gorom Airport") + .addAirport("XGN", "Xangongo Airport") + .addAirport("XGR", "Kangiqsualujjuaq (Georges River) Airport") + .addAirport("XIE", "Xienglom Airport") + .addAirport("XIG", "Xinguara Municipal Airport") + .addAirport("XIJ", "Ahmad al-Jaber Air Base") + .addAirport("XIL", "Xilinhot Airport") + .addAirport("XIN", "Xingning Air Base") + .addAirport("XKH", "Xieng Khouang Airport") + .addAirport("XKS", "Kasabonika Airport") + .addAirport("XKY", "Kaya Airport") + .addAirport("XLW", "Lemwerder Airport") + .addAirport("XMI", "Masasi Airport") + .addAirport("XML", "Minlaton Airport") + .addAirport("XMN", "Xiamen Gaoqi International Airport") + .addAirport("XTL", "Tadoule Lake Airport") + .addAirport("XYR", "Edwaki Airport") + .addAirport("XZA", "Zabré Airport") + .addAirport("YAU", "Kattiniq/Donaldson Airport (TC: CTP9)") + .addAirport("YCU", "Yuncheng Guangong Airport)") + .addAirport("YEM", "Manitowaning/Manitoulin East Municipal Airport)") + .addAirport("YGB", "Texada/Gillies Bay Airport)") + .addAirport("YIW", "Yiwu)") + .addAirport("YHH", "Campbell River Water Aerodrome)") + .addAirport("YKY", "Kindersley Regional Airport)") + .addAirport("YMH", "Mary's Harbour Airport)") + .addAirport("YMV", "Mary River Aerodrome") + .addAirport("YNT", "Yantai Penglai International Airport)") + .addAirport("YOH", "Oxford House Airport") + .addAirport("YPB", "Alberni Valley Regional Airport)") + .addAirport("YPL", "Pickle Lake Airport") + .addAirport("YPR", "Prince Rupert Airport Prince Rup)") + .addAirport("YQH", "Watson Lake Airport)") + .addAirport("YQX", "Gander International Airport") + .addAirport("YRM", "Rocky Mountain House Airport)") + .addAirport("YSI", "Parry Sound/Frying Pan Island-Sans Souci Water Aerodrome") + .addAirport("YTY", "Yangzhou Taizhou Airport)") + .addAirport("YVD", "Yeva Airport)") + .addAirport("YWK", "Wabush Airport)") + .addAirport("YWS", "Whistler/Green Lake Water Aerodrome") + .addAirport("YXR", "Earlton (Timiskaming Regional) Airport") + .addAirport("YYB", "North Bay/Jack Garland Airport") + .addAirport("YYW", "Armstrong Airport") + .addAirport("YZH", "Slave Lake Airpor") + .addAirport("ZAL", "Pichoy Airport Valdivia, Chile") + .addAirport("ZAO", "Cahors - Lalbenque Airport") + .addAirport("ZBE", "Zábřeh Airport Dolní Benešov") + .addAirport("ZBO", "Bowen Airport") + .addAirport("ZDJ", "Bern Railway Station") + .addAirport("ZEN", "Zenag Airport") + .addAirport("ZGF", "Grand Forks Airport") + .addAirport("ZHY", "Zhongwei Shapotou Airport") + .addAirport("ZIC", "Victoria Airport") + .addAirport("ZKE", "Kashechewan Airport") + .addAirport("ZND", "Zinder Airport") + .addAirport("ZOS", "Cañal Bajo Carlos Hott Siebert Airport") + .addAirport("ZQN", "Queenstown Airport") + .addAirport("ZRJ", "Round Lake (Weagamow Lake) Airport") + .addAirport("ZST", "Stewart Aerodrome") + .addAirport("ZTR", "Zhytomyr Airport") + .addAirport("ZUH", "Zhuhai Jinwan Airport") + .addAirport("ZVG", "Springvale Airport") + .addAirport("ZYI", "Zunyi Xinzhou Airport") + .build(); + static DiscreteGenerator createAirportsGenerator() { + final double uniformDistribution = 1 / ((double) ALL_AIRPORTS.length); + final DiscreteGenerator generator = new DiscreteGenerator(); + for(Airport a : ALL_AIRPORTS) { + generator.addValue(uniformDistribution, a.code); + } + return generator; + } + private Airports() {} +} diff --git a/core/src/main/java/site/ycsb/workloads/core/CoreConstants.java b/core/src/main/java/site/ycsb/workloads/core/CoreConstants.java new file mode 100644 index 0000000..ab89a15 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/core/CoreConstants.java @@ -0,0 +1,303 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.core; + +public final class CoreConstants { + /** + * The name of the database table to run queries against. + */ + public static final String TABLENAME_PROPERTY = "table"; + + /** + * The default name of the database table to run queries against. + */ + public static final String TABLENAME_PROPERTY_DEFAULT = "usertable"; + + /** + * The name of the property for the number of fields in a record. + */ + public static final String FIELD_COUNT_PROPERTY = "fieldcount"; + + /** + * Default number of fields in a record. + */ + public static final String FIELD_COUNT_PROPERTY_DEFAULT = "10"; + + /** + * The name of the property for the field length distribution. Options are "uniform", "zipfian" + * (favouring short records), "constant", and "histogram". + *

+ * If "uniform", "zipfian" or "constant", the maximum field length will be that specified by the + * fieldlength property. If "histogram", then the histogram will be read from the filename + * specified in the "fieldlengthhistogram" property. + */ + public static final String FIELD_LENGTH_DISTRIBUTION_PROPERTY = "fieldlengthdistribution"; + + /** + * The default field length distribution. + */ + public static final String FIELD_LENGTH_DISTRIBUTION_PROPERTY_DEFAULT = "constant"; + + /** + * The name of the property for the length of a field in bytes. + */ + public static final String FIELD_LENGTH_PROPERTY = "fieldlength"; + + /** + * The default maximum length of a field in bytes. + */ + public static final String FIELD_LENGTH_PROPERTY_DEFAULT = "100"; + + /** + * The name of the property for the minimum length of a field in bytes. + */ + public static final String MIN_FIELD_LENGTH_PROPERTY = "minfieldlength"; + + /** + * The default minimum length of a field in bytes. + */ + public static final String MIN_FIELD_LENGTH_PROPERTY_DEFAULT = "1"; + + /** + * The name of a property that specifies the filename containing the field length histogram (only + * used if fieldlengthdistribution is "histogram"). + */ + public static final String FIELD_LENGTH_HISTOGRAM_FILE_PROPERTY = "fieldlengthhistogram"; + + /** + * The default filename containing a field length histogram. + */ + public static final String FIELD_LENGTH_HISTOGRAM_FILE_PROPERTY_DEFAULT = "hist.txt"; + + /** + * The name of the property for deciding whether to read one field (false) or all fields (true) of + * a record. + */ + public static final String READ_ALL_FIELDS_PROPERTY = "readallfields"; + + /** + * The default value for the readallfields property. + */ + public static final String READ_ALL_FIELDS_PROPERTY_DEFAULT = "true"; + + /** + * The name of the property for determining how to read all the fields when readallfields is true. + * If set to true, all the field names will be passed into the underlying client. If set to false, + * null will be passed into the underlying client. When passed a null, some clients may retrieve + * the entire row with a wildcard, which may be slower than naming all the fields. + */ + public static final String READ_ALL_FIELDS_BY_NAME_PROPERTY = "readallfieldsbyname"; + + /** + * The default value for the readallfieldsbyname property. + */ + public static final String READ_ALL_FIELDS_BY_NAME_PROPERTY_DEFAULT = "false"; + /** + * The name of the property for deciding whether to write one field (false) or all fields (true) + * of a record. + */ + public static final String WRITE_ALL_FIELDS_PROPERTY = "writeallfields"; + + /** + * The default value for the writeallfields property. + */ + public static final String WRITE_ALL_FIELDS_PROPERTY_DEFAULT = "false"; + /** + * The name of the property for deciding whether to check all returned + * data against the formation template to ensure data integrity. + */ + public static final String DATA_INTEGRITY_PROPERTY = "dataintegrity"; + + /** + * The default value for the dataintegrity property. + */ + public static final String DATA_INTEGRITY_PROPERTY_DEFAULT = "false"; + + /** + * The name of the property for the proportion of transactions that are reads. + */ + public static final String READ_PROPORTION_PROPERTY = "readproportion"; + + /** + * The default proportion of transactions that are reads. + */ + public static final String READ_PROPORTION_PROPERTY_DEFAULT = "0.95"; + + /** + * The name of the property for the proportion of transactions that are updates. + */ + public static final String UPDATE_PROPORTION_PROPERTY = "updateproportion"; + + /** + * The default proportion of transactions that are updates. + */ + public static final String UPDATE_PROPORTION_PROPERTY_DEFAULT = "0.05"; + + /** + * The name of the property for the proportion of transactions that are inserts. + */ + public static final String INSERT_PROPORTION_PROPERTY = "insertproportion"; + + /** + * The default proportion of transactions that are inserts. + */ + public static final String INSERT_PROPORTION_PROPERTY_DEFAULT = "0.0"; + + /** + * The name of the property for the proportion of transactions that are scans. + */ + public static final String SCAN_PROPORTION_PROPERTY = "scanproportion"; + + /** + * The default proportion of transactions that are scans. + */ + public static final String SCAN_PROPORTION_PROPERTY_DEFAULT = "0.0"; + + /** + * The name of the property for the proportion of transactions that are read-modify-write. + */ + public static final String READMODIFYWRITE_PROPORTION_PROPERTY = "readmodifywriteproportion"; + + /** + * The default proportion of transactions that are scans. + */ + public static final String READMODIFYWRITE_PROPORTION_PROPERTY_DEFAULT = "0.0"; + + /** + * The name of the property for the the distribution of requests across the keyspace. Options are + * "uniform", "zipfian" and "latest" + */ + public static final String REQUEST_DISTRIBUTION_PROPERTY = "requestdistribution"; + + /** + * The default distribution of requests across the keyspace. + */ + public static final String REQUEST_DISTRIBUTION_PROPERTY_DEFAULT = "uniform"; + + /** + * The name of the property for adding zero padding to record numbers in order to match + * string sort order. Controls the number of 0s to left pad with. + */ + public static final String ZERO_PADDING_PROPERTY = "zeropadding"; + + /** + * The default zero padding value. Matches integer sort order + */ + public static final String ZERO_PADDING_PROPERTY_DEFAULT = "1"; + + + /** + * The name of the property for the min scan length (number of records). + */ + public static final String MIN_SCAN_LENGTH_PROPERTY = "minscanlength"; + + /** + * The default min scan length. + */ + public static final String MIN_SCAN_LENGTH_PROPERTY_DEFAULT = "1"; + + /** + * The name of the property for the max scan length (number of records). + */ + public static final String MAX_SCAN_LENGTH_PROPERTY = "maxscanlength"; + + /** + * The default max scan length. + */ + public static final String MAX_SCAN_LENGTH_PROPERTY_DEFAULT = "1000"; + + /** + * The name of the property for the scan length distribution. Options are "uniform" and "zipfian" + * (favoring short scans) + */ + public static final String SCAN_LENGTH_DISTRIBUTION_PROPERTY = "scanlengthdistribution"; + + /** + * The default max scan length. + */ + public static final String SCAN_LENGTH_DISTRIBUTION_PROPERTY_DEFAULT = "uniform"; + + /** + * The name of the property for the order to insert records. Options are "ordered" or "hashed" + */ + public static final String INSERT_ORDER_PROPERTY = "insertorder"; + + /** + * Default insert order. + */ + public static final String INSERT_ORDER_PROPERTY_DEFAULT = "hashed"; + + /** + * Percentage data items that constitute the hot set. + */ + public static final String HOTSPOT_DATA_FRACTION = "hotspotdatafraction"; + + /** + * Default value of the size of the hot set. + */ + public static final String HOTSPOT_DATA_FRACTION_DEFAULT = "0.2"; + + /** + * Percentage operations that access the hot set. + */ + public static final String HOTSPOT_OPN_FRACTION = "hotspotopnfraction"; + + /** + * Default value of the percentage operations accessing the hot set. + */ + public static final String HOTSPOT_OPN_FRACTION_DEFAULT = "0.8"; + + /** + * How many times to retry when insertion of a single item to a DB fails. + */ + public static final String INSERTION_RETRY_LIMIT = "core_workload_insertion_retry_limit"; + public static final String INSERTION_RETRY_LIMIT_DEFAULT = "0"; + + /** + * On average, how long to wait between the retries, in seconds. + */ + public static final String INSERTION_RETRY_INTERVAL = "core_workload_insertion_retry_interval"; + public static final String INSERTION_RETRY_INTERVAL_DEFAULT = "3"; + + /** + * Field name prefix. + */ + public static final String FIELD_NAME_PREFIX = "fieldnameprefix"; + + /** + * Default value of the field name prefix. + */ + public static final String FIELD_NAME_PREFIX_DEFAULT = "field"; + + /** + * transactioninsertkeysequence: values "default", "simple" + */ + public static final String TRANSACTION_INSERTKEY_GENERATOR = "transactioninsertkeygenerator"; + + /** + * Default value of the field name prefix. + */ + public static final String TRANSACTION_INSERTKEY_GENERATOR_DEFAULT = "default"; + + /** + * transactioninsertkeysequence window size + */ + public static final String TRANSACTION_INSERTKEY_WINDOW = "transactioninsertkeywindow"; + private CoreConstants() { + // nope + } +} diff --git a/core/src/main/java/site/ycsb/workloads/core/CoreHelper.java b/core/src/main/java/site/ycsb/workloads/core/CoreHelper.java new file mode 100644 index 0000000..9ee5258 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/core/CoreHelper.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.core; + +import java.io.IOException; +import java.util.Properties; + +import site.ycsb.WorkloadException; +import site.ycsb.generator.ConstantIntegerGenerator; +import site.ycsb.generator.DiscreteGenerator; +import site.ycsb.generator.HistogramGenerator; +import site.ycsb.generator.NumberGenerator; +import site.ycsb.generator.UniformLongGenerator; +import site.ycsb.generator.ZipfianGenerator; +import site.ycsb.generator.acknowledge.AcknowledgedCounterGenerator; +import site.ycsb.generator.acknowledge.DefaultAcknowledgedCounterGenerator; +import site.ycsb.generator.acknowledge.StupidAcknowledgedCounterGenerator; + +import static site.ycsb.workloads.core.CoreConstants.*; + +public final class CoreHelper { + + public static AcknowledgedCounterGenerator + createTransactionInsertKeyGenerator(final Properties p, long recordcount) + throws WorkloadException { + String generator = + p.getProperty(TRANSACTION_INSERTKEY_GENERATOR, TRANSACTION_INSERTKEY_GENERATOR_DEFAULT); + if (generator.equals("default")) { + String window = p.getProperty(TRANSACTION_INSERTKEY_WINDOW); + if(window == null) { + System.out.println("Generating insert keys with default and default window size"); + return new DefaultAcknowledgedCounterGenerator(recordcount); + } else { + System.out.println("Generating insert keys with default generator and a window size of " + window); + return new DefaultAcknowledgedCounterGenerator(recordcount, Integer.parseInt(window)); + } + } else if (generator.equals("simple")) { + System.out.println("Generating insert keys with stupid generator"); + return new StupidAcknowledgedCounterGenerator(recordcount); + } else { + throw new WorkloadException("Unknown request distribution \"" + generator + "\""); + } + } + + public static NumberGenerator getFieldLengthGenerator(Properties p) throws WorkloadException { + NumberGenerator fieldlengthgenerator; + String fieldlengthdistribution = p.getProperty( + FIELD_LENGTH_DISTRIBUTION_PROPERTY, FIELD_LENGTH_DISTRIBUTION_PROPERTY_DEFAULT); + int fieldlength = + Integer.parseInt(p.getProperty(FIELD_LENGTH_PROPERTY, FIELD_LENGTH_PROPERTY_DEFAULT)); + int minfieldlength = + Integer.parseInt(p.getProperty(MIN_FIELD_LENGTH_PROPERTY, MIN_FIELD_LENGTH_PROPERTY_DEFAULT)); + String fieldlengthhistogram = p.getProperty( + FIELD_LENGTH_HISTOGRAM_FILE_PROPERTY, FIELD_LENGTH_HISTOGRAM_FILE_PROPERTY_DEFAULT); + if (fieldlengthdistribution.compareTo("constant") == 0) { + fieldlengthgenerator = new ConstantIntegerGenerator(fieldlength); + } else if (fieldlengthdistribution.compareTo("uniform") == 0) { + fieldlengthgenerator = new UniformLongGenerator(minfieldlength, fieldlength); + } else if (fieldlengthdistribution.compareTo("zipfian") == 0) { + fieldlengthgenerator = new ZipfianGenerator(minfieldlength, fieldlength); + } else if (fieldlengthdistribution.compareTo("histogram") == 0) { + try { + fieldlengthgenerator = new HistogramGenerator(fieldlengthhistogram); + } catch (IOException e) { + throw new WorkloadException( + "Couldn't read field length histogram file: " + fieldlengthhistogram, e); + } + } else { + throw new WorkloadException( + "Unknown field length distribution \"" + fieldlengthdistribution + "\""); + } + return fieldlengthgenerator; + } + + /** + * Creates a weighted discrete values with database operations for a workload to perform. + * Weights/proportions are read from the properties list and defaults are used + * when values are not configured. + * Current operations are "READ", "UPDATE", "INSERT", "SCAN" and "READMODIFYWRITE". + * + * @param p The properties list to pull weights from. + * @return A generator that can be used to determine the next operation to perform. + * @throws IllegalArgumentException if the properties object was null. + */ + public static DiscreteGenerator createOperationGenerator(final Properties p) { + if (p == null) { + throw new IllegalArgumentException("Properties object cannot be null"); + } + final double readproportion = Double.parseDouble( + p.getProperty(READ_PROPORTION_PROPERTY, READ_PROPORTION_PROPERTY_DEFAULT)); + final double updateproportion = Double.parseDouble( + p.getProperty(UPDATE_PROPORTION_PROPERTY, UPDATE_PROPORTION_PROPERTY_DEFAULT)); + final double insertproportion = Double.parseDouble( + p.getProperty(INSERT_PROPORTION_PROPERTY, INSERT_PROPORTION_PROPERTY_DEFAULT)); + final double scanproportion = Double.parseDouble( + p.getProperty(SCAN_PROPORTION_PROPERTY, SCAN_PROPORTION_PROPERTY_DEFAULT)); + final double readmodifywriteproportion = Double.parseDouble(p.getProperty( + READMODIFYWRITE_PROPORTION_PROPERTY, READMODIFYWRITE_PROPORTION_PROPERTY_DEFAULT)); + + final DiscreteGenerator operationchooser = new DiscreteGenerator(); + if (readproportion > 0) { + operationchooser.addValue(readproportion, "READ"); + } + + if (updateproportion > 0) { + operationchooser.addValue(updateproportion, "UPDATE"); + } + + if (insertproportion > 0) { + operationchooser.addValue(insertproportion, "INSERT"); + } + + if (scanproportion > 0) { + operationchooser.addValue(scanproportion, "SCAN"); + } + + if (readmodifywriteproportion > 0) { + operationchooser.addValue(readmodifywriteproportion, "READMODIFYWRITE"); + } + return operationchooser; + } + + private CoreHelper() { + // nope + } +} diff --git a/core/src/main/java/site/ycsb/workloads/package-info.java b/core/src/main/java/site/ycsb/workloads/package-info.java new file mode 100644 index 0000000..ba7a23d --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2015 - 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. + */ + +/** + * The YCSB workloads. + */ +package site.ycsb.workloads; + diff --git a/core/src/main/java/site/ycsb/workloads/schema/ArraySchemaColumn.java b/core/src/main/java/site/ycsb/workloads/schema/ArraySchemaColumn.java new file mode 100644 index 0000000..580acda --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/schema/ArraySchemaColumn.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.schema; + +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumn; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnKind; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnType; + +final class ArraySchemaColumn implements SchemaColumn { + final String name; + final SchemaColumnType arrayElementType; + ArraySchemaColumn(String name, SchemaColumnType arrayElementType) { + this.name = name; + this.arrayElementType = arrayElementType; + } + @Override + public String getColumnName() { + return name; + } + @Override + public SchemaColumnKind getColumnKind() { + return SchemaColumnKind.ARRAY; + } + @Override + public SchemaColumnType getColumnType() { + return arrayElementType; + } +} diff --git a/core/src/main/java/site/ycsb/workloads/schema/EmbeddedSchemaColumn.java b/core/src/main/java/site/ycsb/workloads/schema/EmbeddedSchemaColumn.java new file mode 100644 index 0000000..0465969 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/schema/EmbeddedSchemaColumn.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.schema; + +import java.util.List; + +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumn; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnKind; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnType; + +final class EmbeddedSchemaColumn implements SchemaColumn { + final String name; + final List elements; + EmbeddedSchemaColumn(String name, List elements) { + this.name = name; + this.elements = elements; + } + @Override + public String getColumnName() { + return name; + } + @Override + public SchemaColumnKind getColumnKind() { + return SchemaColumnKind.NESTED; + } + @Override + public SchemaColumnType getColumnType() { + return null; + } +} diff --git a/core/src/main/java/site/ycsb/workloads/schema/ScalarSchemaColumn.java b/core/src/main/java/site/ycsb/workloads/schema/ScalarSchemaColumn.java new file mode 100644 index 0000000..0087759 --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/schema/ScalarSchemaColumn.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.schema; + +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumn; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnKind; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnType; + +final class ScalarSchemaColumn implements SchemaColumn { + final String name; + final SchemaColumnType t; + ScalarSchemaColumn(String name, SchemaColumnType t) { + this.name = name; + this.t = t; + } + @Override + public String getColumnName() { + return name; + } + @Override + public SchemaColumnKind getColumnKind() { + return SchemaColumnKind.SCALAR; + } + @Override + public SchemaColumnType getColumnType() { + return t; + } +} diff --git a/core/src/main/java/site/ycsb/workloads/schema/SchemaHolder.java b/core/src/main/java/site/ycsb/workloads/schema/SchemaHolder.java new file mode 100644 index 0000000..dc1170f --- /dev/null +++ b/core/src/main/java/site/ycsb/workloads/schema/SchemaHolder.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.workloads.schema; + +import java.util.ArrayList; +import java.util.List; + +public enum SchemaHolder { + INSTANCE; + private volatile List theList; + public List getOrderedListOfColumns() { + return new ArrayList<>(theList); + } + public static SchemaBuilder schemaBuilder() { + return new SchemaBuilder(); + } + /*public static EmbeddedColumnBuilder embeddingBuilder() { + return new EmbeddedColumnBuilder(); + }*/ + private void register(List schema) { + theList = new ArrayList<>(schema); + } + public static enum SchemaColumnKind { + SCALAR, + NESTED, + ARRAY, + } + public static enum SchemaColumnType { + INT, + LONG, + TEXT, + BYTES, + CUSTOM + } + /* + public static class EmbeddedColumnBuilder { + private final Map entries = new HashMap<>(); + public EmbeddedColumnBuilder addEntry(String name, SchemaColumnType type) { + entries.put(name, type); + return this; + } + }*/ + public static class SchemaBuilder { + private final List schema = new ArrayList<>(); + public SchemaBuilder addEmbeddedColumn(String name, List elements) { + SchemaColumn c = new EmbeddedSchemaColumn(name, elements); + schema.add(c); + return this; + } + public SchemaBuilder addArrayColumn(String name, SchemaColumnType t) { + SchemaColumn c = new ArraySchemaColumn(name, t); + schema.add(c); + return this; + } + public SchemaBuilder addScalarColumn(String name, SchemaColumnType t) { + SchemaColumn c = new ScalarSchemaColumn(name, t); + schema.add(c); + return this; + } + public SchemaBuilder addColumn(String name, SchemaColumnType t) { + return addScalarColumn(name, t); + } + public void register() { + SchemaHolder.INSTANCE.register(schema); + } + public List build() { + return schema; + } + } + + public static interface SchemaColumn { + public String getColumnName(); + public SchemaColumnKind getColumnKind(); + public SchemaColumnType getColumnType(); + } +} diff --git a/core/src/main/java/site/ycsb/wrappers/ByteIteratorWrapper.java b/core/src/main/java/site/ycsb/wrappers/ByteIteratorWrapper.java new file mode 100644 index 0000000..c7a7af8 --- /dev/null +++ b/core/src/main/java/site/ycsb/wrappers/ByteIteratorWrapper.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.wrappers; + +import java.util.List; + +import site.ycsb.ByteIterator; + +public final class ByteIteratorWrapper implements DataWrapper { + private final ByteIterator theIterator; + ByteIteratorWrapper(ByteIterator it) { + theIterator = it; + } + @Override + public long asLong() { + throw new UnsupportedOperationException("byte iterator is not a long"); + } + @Override + public int asInteger() { + throw new UnsupportedOperationException("byte iterator is not an integer"); + } + @Override + public String asString() { + return theIterator.toString(); + } + @Override + public boolean isInteger() { + return false; + } + @Override + public boolean isLong() { + return false; + } + @Override + public boolean isString() { + return false; + } + @Override + public List arrayAsList() { + throw new UnsupportedOperationException("byte iterator is not an arrays nested"); + } + @Deprecated + public static ByteIteratorWrapper create(ByteIterator it) { + return new ByteIteratorWrapper(it); + } + public ByteIterator asIterator() { + return theIterator; + } + @Override + public List asNested() { + throw new UnsupportedOperationException("String cannot be nested"); + } + @Override + public Object asObject() { + return theIterator.toArray(); + } + + @Override + public boolean isArray() { + return false; + } + @Override + public boolean isNested() { + return false; + } + @Override + public boolean isTerminal() { + return true; + } +} diff --git a/core/src/main/java/site/ycsb/wrappers/Comparison.java b/core/src/main/java/site/ycsb/wrappers/Comparison.java new file mode 100644 index 0000000..f03bb53 --- /dev/null +++ b/core/src/main/java/site/ycsb/wrappers/Comparison.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.wrappers; + +public interface Comparison { + + String getFieldname(); + ComparisonOperator getOperator(); + String getOperandAsString(); + int getOperandAsInt(); + String getContentAsString(); + boolean comparesStrings(); + boolean comparesInts(); + boolean isSimpleNesting(); + Comparison getSimpleNesting(); + Comparison normalized(); +} diff --git a/core/src/main/java/site/ycsb/wrappers/ComparisonOperator.java b/core/src/main/java/site/ycsb/wrappers/ComparisonOperator.java new file mode 100644 index 0000000..f0b1727 --- /dev/null +++ b/core/src/main/java/site/ycsb/wrappers/ComparisonOperator.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.wrappers; + +public enum ComparisonOperator { + STRING_EQUAL, + INT_LTE, +} diff --git a/core/src/main/java/site/ycsb/wrappers/Comparisons.java b/core/src/main/java/site/ycsb/wrappers/Comparisons.java new file mode 100644 index 0000000..295d281 --- /dev/null +++ b/core/src/main/java/site/ycsb/wrappers/Comparisons.java @@ -0,0 +1,287 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.wrappers; + +public final class Comparisons { + + public static Comparison createStringComparison(String fieldName, ComparisonOperator op, String operand) { + return new StringComparison(fieldName, op, operand); + } + + public static Comparison createIntComparison(String fieldName, ComparisonOperator op, int operand) { + return new IntComparison(fieldName, op, operand); + } + + public static Comparison createSimpleNestingComparison(String fieldName, Comparison c) { + return new SimpleNestingComparison(fieldName, c); + } + + private Comparisons() { + // private constructor + } +} + +class StringComparison implements Comparison { + private final String fieldName; + private final ComparisonOperator operator; + private final String operand; + StringComparison(String fieldName, ComparisonOperator op, String operand) { + this.fieldName = fieldName; + operator = op; + this.operand = operand; + } + @Override + public StringComparison normalized() { + return new StringComparison(fieldName, operator, null); + } + @Override + public final String getFieldname() { + return fieldName; + } + @Override + public String getContentAsString() { + return operator + " " + operand; + } + @Override + public boolean comparesStrings() { + return true; + } + @Override + public boolean comparesInts() { + return false; + } + @Override + public ComparisonOperator getOperator(){ + return operator; + } + @Override + public String getOperandAsString() { + return operand; + } + @Override + public int getOperandAsInt() { + throw new UnsupportedOperationException("wrong type"); + } + @Override + public String toString() { + return fieldName + "_" + operator + "_" + operand; + } + @Override + public boolean isSimpleNesting() { + return false; + } + @Override + public Comparison getSimpleNesting() { + throw new UnsupportedOperationException("wrong type"); + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((fieldName == null) ? 0 : fieldName.hashCode()); + result = prime * result + ((operator == null) ? 0 : operator.hashCode()); + result = prime * result + ((operand == null) ? 0 : operand.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + StringComparison other = (StringComparison) obj; + if (fieldName == null) { + if (other.fieldName != null) + return false; + } else if (!fieldName.equals(other.fieldName)) + return false; + if (operator != other.operator) + return false; + if (operand == null) { + if (other.operand != null) + return false; + } else if (!operand.equals(other.operand)) + return false; + return true; + } +} +class IntComparison implements Comparison { + private final String fieldName; + private final ComparisonOperator operator; + private final Integer operand; + IntComparison(String fieldName, ComparisonOperator op, Integer operand) { + this.fieldName = fieldName; + operator = op; + this.operand = operand; + } + public IntComparison normalized() { + return new IntComparison(fieldName, operator, null); + } + @Override + public final String getFieldname() { + return fieldName; + } + @Override + public String getContentAsString() { + return operator + " " + operand; + } + @Override + public boolean comparesStrings() { + return false; + } + @Override + public boolean comparesInts() { + return true; + } + @Override + public ComparisonOperator getOperator(){ + return operator; + } + @Override + public String getOperandAsString() { + throw new UnsupportedOperationException("wrong type"); + } + @Override + public int getOperandAsInt() { + return operand; + } + @Override + public String toString() { + return fieldName + "_" + operator + "_" + operand; + } + @Override + public boolean isSimpleNesting() { + return false; + } + @Override + public Comparison getSimpleNesting() { + throw new UnsupportedOperationException("wrong type"); + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((fieldName == null) ? 0 : fieldName.hashCode()); + result = prime * result + ((operator == null) ? 0 : operator.hashCode()); + result = prime * result + ((operand == null) ? 0 : operand.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + IntComparison other = (IntComparison) obj; + if (fieldName == null) { + if (other.fieldName != null) + return false; + } else if (!fieldName.equals(other.fieldName)) + return false; + if (operator != other.operator) + return false; + if (operand != other.operand) + return false; + return true; + } + +} +class SimpleNestingComparison implements Comparison { + private final String fieldName; + private final Comparison nesting; + SimpleNestingComparison(String fieldName, Comparison nesting) { + this.fieldName = fieldName; + this.nesting = nesting; + } + public SimpleNestingComparison normalized() { + return new SimpleNestingComparison(fieldName, nesting.normalized()); + } + @Override + public final String getFieldname() { + return fieldName; + } + @Override + public String getContentAsString() { + throw new UnsupportedOperationException("wrong type"); + } + @Override + public boolean comparesStrings() { + return false; + } + @Override + public boolean comparesInts() { + return true; + } + @Override + public ComparisonOperator getOperator(){ + return null; + } + @Override + public String getOperandAsString() { + throw new UnsupportedOperationException("wrong type"); + } + @Override + public int getOperandAsInt() { + throw new UnsupportedOperationException("wrong type"); + } + @Override + public String toString() { + return fieldName + "." + nesting; + } + @Override + public boolean isSimpleNesting() { + return true; + } + @Override + public Comparison getSimpleNesting() { + return nesting; + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((fieldName == null) ? 0 : fieldName.hashCode()); + result = prime * result + ((nesting == null) ? 0 : nesting.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SimpleNestingComparison other = (SimpleNestingComparison) obj; + if (fieldName == null) { + if (other.fieldName != null) + return false; + } else if (!fieldName.equals(other.fieldName)) + return false; + if (nesting == null) { + if (other.nesting != null) + return false; + } else if (!nesting.equals(other.nesting)) + return false; + return true; + } + +} \ No newline at end of file diff --git a/core/src/main/java/site/ycsb/wrappers/DataWrapper.java b/core/src/main/java/site/ycsb/wrappers/DataWrapper.java new file mode 100644 index 0000000..a1c4781 --- /dev/null +++ b/core/src/main/java/site/ycsb/wrappers/DataWrapper.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.wrappers; + +import java.util.List; + +import site.ycsb.ByteIterator; + +public interface DataWrapper { + ByteIterator asIterator(); + Object asObject(); + long asLong(); + int asInteger(); + List arrayAsList(); + String asString(); + boolean isTerminal(); + boolean isArray(); + boolean isNested(); + boolean isLong(); + boolean isInteger(); + boolean isString(); + List asNested(); +} \ No newline at end of file diff --git a/core/src/main/java/site/ycsb/wrappers/DatabaseField.java b/core/src/main/java/site/ycsb/wrappers/DatabaseField.java new file mode 100644 index 0000000..0115e10 --- /dev/null +++ b/core/src/main/java/site/ycsb/wrappers/DatabaseField.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.wrappers; + +public final class DatabaseField { + + private final String fieldname; + private final DataWrapper wrapper; + + public DatabaseField(String fieldname, DataWrapper wrapper) { + this.fieldname = fieldname; + this.wrapper = wrapper; + } + + public String getFieldname() { + return fieldname; + } + public DataWrapper getContent() { + return wrapper; + } +} diff --git a/core/src/main/java/site/ycsb/wrappers/Wrappers.java b/core/src/main/java/site/ycsb/wrappers/Wrappers.java new file mode 100644 index 0000000..9a0bd28 --- /dev/null +++ b/core/src/main/java/site/ycsb/wrappers/Wrappers.java @@ -0,0 +1,302 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.wrappers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import site.ycsb.ByteIterator; +import site.ycsb.NumericByteIterator; +import site.ycsb.StringByteIterator; + +public final class Wrappers { + public static DataWrapper wrapString(String string) { + return new StringWrapper(string); + } + public static DataWrapper wrapInteger(Integer string) { + return new NumberWrapper(string); + } + public static DataWrapper wrapLong(Long string) { + return new NumberWrapper(string); + } + public static DataWrapper wrapIterator(ByteIterator it) { + return new ByteIteratorWrapper(it); + } + public static DataWrapper wrapArray(List entries) { + return new ArrayWrapper(entries); + } + public static DataWrapper wrapArray(DataWrapper[] entries) { + return new ArrayWrapper(Arrays.asList(entries)); + } + public static DataWrapper wrapNested(DatabaseField[] fields) { + return new NestedWrapper(Arrays.asList(fields)); + } + private Wrappers() {} +} + +final class NestedWrapper implements DataWrapper { + private final List myEntries; + NestedWrapper(List entries) { + myEntries = entries; + } + @Override + public List arrayAsList() { + throw new UnsupportedOperationException("Nested objects are no arrays"); + } + @Override + public long asLong() { + throw new UnsupportedOperationException("Nested objects cannot be represented as long"); + } + @Override + public int asInteger() { + throw new UnsupportedOperationException("Nested objects are not integer"); + } + @Override + public String asString() { + throw new UnsupportedOperationException("Nested objects are not strings"); + } + @Override + public boolean isInteger() { + return false; + } + @Override + public boolean isLong() { + return false; + } + @Override + public boolean isString() { + return false; + } + @Override + public ByteIterator asIterator() { + throw new UnsupportedOperationException("Nested objects cannot be transferred into a ByteIterator"); + } + @Override + public List asNested() { + return new ArrayList(myEntries); + } + @Override + public Object asObject() { + throw new UnsupportedOperationException("Nested objecsts cannot be returned as Object"); + } + + @Override + public boolean isArray() { + return false; + } + @Override + public boolean isNested() { + return true; + } + @Override + public boolean isTerminal() { + return false; + } +} + +final class ArrayWrapper implements DataWrapper { + private final List myEntries; + ArrayWrapper(List entries) { + myEntries = entries; + } + @Override + public int asInteger() { + throw new UnsupportedOperationException("Arrays are not integer"); + } + @Override + public String asString() { + throw new UnsupportedOperationException("Arrays are not strings"); + } + @Override + public boolean isInteger() { + return false; + } + @Override + public boolean isLong() { + return false; + } + @Override + public boolean isString() { + return false; + } + @Override + public List arrayAsList() { + return Collections.unmodifiableList(myEntries); + } + @Override + public long asLong() { + throw new UnsupportedOperationException("arrays cannot be represented as long"); + } + @Override + public ByteIterator asIterator() { + throw new UnsupportedOperationException("Arrays cannot be transferred into a ByteIterator"); + } + @Override + public List asNested() { + throw new UnsupportedOperationException("Arrays cannot be nested"); + } + @Override + public Object asObject() { + // Object[] o = new Object[myEntries.size()]; + List o = new ArrayList<>(); + for(int i = 0; i < myEntries.size(); i++) { + o.add(i,myEntries.get(i).asObject()); + } + return o; + } + + @Override + public boolean isArray() { + return true; + } + @Override + public boolean isNested() { + return false; + } + @Override + public boolean isTerminal() { + return false; + } +} + +final class NumberWrapper implements DataWrapper { + boolean isLong; + private final Number myContent; + NumberWrapper(Integer myInt) { + isLong = false; + myContent = myInt; + } + NumberWrapper(Long myLong) { + isLong = true; + myContent = myLong; + } + @Override + public long asLong() { + return myContent.longValue(); + } + @Override + public int asInteger() { + return myContent.intValue(); + } + @Override + public String asString() { + return myContent.toString(); + } + @Override + public boolean isInteger() { + return !isLong; + } + @Override + public boolean isLong() { + return isLong; + } + @Override + public boolean isString() { + return false; + } + @Override + public ByteIterator asIterator() { + return new NumericByteIterator(myContent.longValue()); + } + @Override + public List asNested() { + throw new UnsupportedOperationException("Numbers cannot be nested"); + } + @Override + public List arrayAsList() { + throw new UnsupportedOperationException("Numbers are no arrays nested"); + } + @Override + public Object asObject() { + return myContent; + } + + @Override + public boolean isArray() { + return false; + } + @Override + public boolean isNested() { + return false; + } + @Override + public boolean isTerminal() { + return true; + } +} + +final class StringWrapper implements DataWrapper { + private final String myContent; + StringWrapper(String string) { + myContent = string; + } + @Override + public List arrayAsList() { + throw new UnsupportedOperationException("Strings are no arrays"); + } + @Override + public long asLong() { + throw new UnsupportedOperationException("Strings are not long"); + } + @Override + public int asInteger() { + throw new UnsupportedOperationException("Strings are not integer"); + } + @Override + public String asString() { + return myContent; + } + @Override + public boolean isInteger() { + return false; + } + @Override + public boolean isLong() { + return false; + } + @Override + public boolean isString() { + return true; + } + @Override + public ByteIterator asIterator() { + return new StringByteIterator(myContent); + } + @Override + public List asNested() { + throw new UnsupportedOperationException("String cannot be nested"); + } + @Override + public Object asObject() { + return myContent; + } + + @Override + public boolean isArray() { + return false; + } + @Override + public boolean isNested() { + return false; + } + @Override + public boolean isTerminal() { + return true; + } +} \ No newline at end of file diff --git a/core/src/main/resources/project.properties b/core/src/main/resources/project.properties new file mode 100644 index 0000000..9c6df29 --- /dev/null +++ b/core/src/main/resources/project.properties @@ -0,0 +1,15 @@ +# 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. +version=${project.version} diff --git a/core/src/test/java/site/ycsb/TestByteIterator.java b/core/src/test/java/site/ycsb/TestByteIterator.java new file mode 100644 index 0000000..01bf0f9 --- /dev/null +++ b/core/src/test/java/site/ycsb/TestByteIterator.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2012 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 site.ycsb; + +import org.testng.annotations.Test; +import static org.testng.AssertJUnit.*; + +public class TestByteIterator { + @Test + public void testRandomByteIterator() { + int size = 100; + ByteIterator itor = new RandomByteIterator(size); + assertTrue(itor.hasNext()); + assertEquals(size, itor.bytesLeft()); + assertEquals(size, itor.toString().getBytes().length); + assertFalse(itor.hasNext()); + assertEquals(0, itor.bytesLeft()); + + itor = new RandomByteIterator(size); + assertEquals(size, itor.toArray().length); + assertFalse(itor.hasNext()); + assertEquals(0, itor.bytesLeft()); + } +} diff --git a/core/src/test/java/site/ycsb/TestNumericByteIterator.java b/core/src/test/java/site/ycsb/TestNumericByteIterator.java new file mode 100644 index 0000000..c98ea83 --- /dev/null +++ b/core/src/test/java/site/ycsb/TestNumericByteIterator.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2017 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 site.ycsb; + +import org.testng.annotations.Test; +import static org.testng.AssertJUnit.*; + +public class TestNumericByteIterator { + + @Test + public void testLong() throws Exception { + NumericByteIterator it = new NumericByteIterator(42L); + assertFalse(it.isFloatingPoint()); + assertEquals(42L, it.getLong()); + + try { + it.getDouble(); + fail("Expected IllegalStateException."); + } catch (IllegalStateException e) { } + try { + it.next(); + fail("Expected UnsupportedOperationException."); + } catch (UnsupportedOperationException e) { } + + assertEquals(8, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(7, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(6, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(5, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(4, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(3, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(2, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(1, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 42, (byte) it.nextByte()); + assertEquals(0, it.bytesLeft()); + assertFalse(it.hasNext()); + + it.reset(); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + } + + @Test + public void testDouble() throws Exception { + NumericByteIterator it = new NumericByteIterator(42.75); + assertTrue(it.isFloatingPoint()); + assertEquals(42.75, it.getDouble(), 0.001); + + try { + it.getLong(); + fail("Expected IllegalStateException."); + } catch (IllegalStateException e) { } + try { + it.next(); + fail("Expected UnsupportedOperationException."); + } catch (UnsupportedOperationException e) { } + + assertEquals(8, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 64, (byte) it.nextByte()); + assertEquals(7, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 69, (byte) it.nextByte()); + assertEquals(6, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 96, (byte) it.nextByte()); + assertEquals(5, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(4, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(3, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(2, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(1, it.bytesLeft()); + assertTrue(it.hasNext()); + assertEquals((byte) 0, (byte) it.nextByte()); + assertEquals(0, it.bytesLeft()); + assertFalse(it.hasNext()); + + it.reset(); + assertTrue(it.hasNext()); + assertEquals((byte) 64, (byte) it.nextByte()); + } +} diff --git a/core/src/test/java/site/ycsb/TestStatus.java b/core/src/test/java/site/ycsb/TestStatus.java new file mode 100644 index 0000000..a426463 --- /dev/null +++ b/core/src/test/java/site/ycsb/TestStatus.java @@ -0,0 +1,41 @@ +/** + * 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 site.ycsb; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * Test class for {@link Status}. + */ +public class TestStatus { + + @Test + public void testAcceptableStatus() { + assertTrue(Status.OK.isOk()); + assertTrue(Status.BATCHED_OK.isOk()); + assertFalse(Status.BAD_REQUEST.isOk()); + assertFalse(Status.ERROR.isOk()); + assertFalse(Status.FORBIDDEN.isOk()); + assertFalse(Status.NOT_FOUND.isOk()); + assertFalse(Status.NOT_IMPLEMENTED.isOk()); + assertFalse(Status.SERVICE_UNAVAILABLE.isOk()); + assertFalse(Status.UNEXPECTED_STATE.isOk()); + } +} diff --git a/core/src/test/java/site/ycsb/TestUtils.java b/core/src/test/java/site/ycsb/TestUtils.java new file mode 100644 index 0000000..450f460 --- /dev/null +++ b/core/src/test/java/site/ycsb/TestUtils.java @@ -0,0 +1,146 @@ +/** + * 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 site.ycsb; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; + +import org.testng.annotations.Test; + +public class TestUtils { + + @Test + public void bytesToFromLong() throws Exception { + byte[] bytes = new byte[8]; + assertEquals(Utils.bytesToLong(bytes), 0L); + assertArrayEquals(Utils.longToBytes(0), bytes); + + bytes[7] = 1; + assertEquals(Utils.bytesToLong(bytes), 1L); + assertArrayEquals(Utils.longToBytes(1L), bytes); + + bytes = new byte[] { 127, -1, -1, -1, -1, -1, -1, -1 }; + assertEquals(Utils.bytesToLong(bytes), Long.MAX_VALUE); + assertArrayEquals(Utils.longToBytes(Long.MAX_VALUE), bytes); + + bytes = new byte[] { -128, 0, 0, 0, 0, 0, 0, 0 }; + assertEquals(Utils.bytesToLong(bytes), Long.MIN_VALUE); + assertArrayEquals(Utils.longToBytes(Long.MIN_VALUE), bytes); + + bytes = new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF }; + assertEquals(Utils.bytesToLong(bytes), -1L); + assertArrayEquals(Utils.longToBytes(-1L), bytes); + + // if the array is too long we just skip the remainder + bytes = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1, 42, 42, 42 }; + assertEquals(Utils.bytesToLong(bytes), 1L); + } + + @Test + public void bytesToFromDouble() throws Exception { + byte[] bytes = new byte[8]; + assertEquals(Utils.bytesToDouble(bytes), 0, 0.0001); + assertArrayEquals(Utils.doubleToBytes(0), bytes); + + bytes = new byte[] { 63, -16, 0, 0, 0, 0, 0, 0 }; + assertEquals(Utils.bytesToDouble(bytes), 1, 0.0001); + assertArrayEquals(Utils.doubleToBytes(1), bytes); + + bytes = new byte[] { -65, -16, 0, 0, 0, 0, 0, 0 }; + assertEquals(Utils.bytesToDouble(bytes), -1, 0.0001); + assertArrayEquals(Utils.doubleToBytes(-1), bytes); + + bytes = new byte[] { 127, -17, -1, -1, -1, -1, -1, -1 }; + assertEquals(Utils.bytesToDouble(bytes), Double.MAX_VALUE, 0.0001); + assertArrayEquals(Utils.doubleToBytes(Double.MAX_VALUE), bytes); + + bytes = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }; + assertEquals(Utils.bytesToDouble(bytes), Double.MIN_VALUE, 0.0001); + assertArrayEquals(Utils.doubleToBytes(Double.MIN_VALUE), bytes); + + bytes = new byte[] { 127, -8, 0, 0, 0, 0, 0, 0 }; + assertTrue(Double.isNaN(Utils.bytesToDouble(bytes))); + assertArrayEquals(Utils.doubleToBytes(Double.NaN), bytes); + + bytes = new byte[] { 63, -16, 0, 0, 0, 0, 0, 0, 42, 42, 42 }; + assertEquals(Utils.bytesToDouble(bytes), 1, 0.0001); + } + + @Test (expectedExceptions = NullPointerException.class) + public void bytesToLongNull() throws Exception { + Utils.bytesToLong(null); + } + + @Test (expectedExceptions = IndexOutOfBoundsException.class) + public void bytesToLongTooShort() throws Exception { + Utils.bytesToLong(new byte[] { 0, 0, 0, 0, 0, 0, 0 }); + } + + @Test (expectedExceptions = IllegalArgumentException.class) + public void bytesToDoubleTooShort() throws Exception { + Utils.bytesToDouble(new byte[] { 0, 0, 0, 0, 0, 0, 0 }); + } + + @Test + public void jvmUtils() throws Exception { + // This should ALWAYS return at least one thread. + assertTrue(Utils.getActiveThreadCount() > 0); + // This should always be greater than 0 or something is goofed up in the JVM. + assertTrue(Utils.getUsedMemoryBytes() > 0); + // Some operating systems may not implement this so we don't have a good + // test. Just make sure it doesn't throw an exception. + Utils.getSystemLoadAverage(); + // This will probably be zero but should never be negative. + assertTrue(Utils.getGCTotalCollectionCount() >= 0); + // Could be zero similar to GC total collection count + assertTrue(Utils.getGCTotalTime() >= 0); + // Could be empty + assertTrue(Utils.getGCStatst().size() >= 0); + } + + /** + * Since this version of TestNG doesn't appear to have an assertArrayEquals, + * this will compare the two to make sure they're the same. + * @param actual Actual array to validate + * @param expected What the array should contain + * @throws AssertionError if the test fails. + */ + public void assertArrayEquals(final byte[] actual, final byte[] expected) { + if (actual == null && expected != null) { + throw new AssertionError("Expected " + Arrays.toString(expected) + + " but found [null]"); + } + if (actual != null && expected == null) { + throw new AssertionError("Expected [null] but found " + + Arrays.toString(actual)); + } + if (actual.length != expected.length) { + throw new AssertionError("Expected length " + expected.length + + " but found " + actual.length); + } + for (int i = 0; i < expected.length; i++) { + if (actual[i] != expected[i]) { + throw new AssertionError("Expected byte [" + expected[i] + + "] at index " + i + " but found [" + actual[i] + "]"); + } + } + } +} \ No newline at end of file diff --git a/core/src/test/java/site/ycsb/generator/TestIncrementingPrintableStringGenerator.java b/core/src/test/java/site/ycsb/generator/TestIncrementingPrintableStringGenerator.java new file mode 100644 index 0000000..f236c5a --- /dev/null +++ b/core/src/test/java/site/ycsb/generator/TestIncrementingPrintableStringGenerator.java @@ -0,0 +1,130 @@ +/** + * 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 site.ycsb.generator; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.fail; + +import java.util.NoSuchElementException; + +import org.testng.annotations.Test; + +public class TestIncrementingPrintableStringGenerator { + private final static int[] ATOC = new int[] { 65, 66, 67 }; + + @Test + public void rolloverOK() throws Exception { + final IncrementingPrintableStringGenerator gen = + new IncrementingPrintableStringGenerator(2, ATOC); + + assertNull(gen.lastValue()); + assertEquals(gen.nextValue(), "AA"); + assertEquals(gen.lastValue(), "AA"); + assertEquals(gen.nextValue(), "AB"); + assertEquals(gen.lastValue(), "AB"); + assertEquals(gen.nextValue(), "AC"); + assertEquals(gen.lastValue(), "AC"); + assertEquals(gen.nextValue(), "BA"); + assertEquals(gen.lastValue(), "BA"); + assertEquals(gen.nextValue(), "BB"); + assertEquals(gen.lastValue(), "BB"); + assertEquals(gen.nextValue(), "BC"); + assertEquals(gen.lastValue(), "BC"); + assertEquals(gen.nextValue(), "CA"); + assertEquals(gen.lastValue(), "CA"); + assertEquals(gen.nextValue(), "CB"); + assertEquals(gen.lastValue(), "CB"); + assertEquals(gen.nextValue(), "CC"); + assertEquals(gen.lastValue(), "CC"); + assertEquals(gen.nextValue(), "AA"); // <-- rollover + assertEquals(gen.lastValue(), "AA"); + } + + @Test + public void rolloverOneCharacterOK() throws Exception { + // It would be silly to create a generator with one character. + final IncrementingPrintableStringGenerator gen = + new IncrementingPrintableStringGenerator(2, new int[] { 65 }); + for (int i = 0; i < 5; i++) { + assertEquals(gen.nextValue(), "AA"); + } + } + + @Test + public void rolloverException() throws Exception { + final IncrementingPrintableStringGenerator gen = + new IncrementingPrintableStringGenerator(2, ATOC); + gen.setThrowExceptionOnRollover(true); + + int i = 0; + try { + while(i < 11) { + ++i; + gen.nextValue(); + } + fail("Expected NoSuchElementException"); + } catch (NoSuchElementException e) { + assertEquals(i, 10); + } + } + + @Test + public void rolloverOneCharacterException() throws Exception { + // It would be silly to create a generator with one character. + final IncrementingPrintableStringGenerator gen = + new IncrementingPrintableStringGenerator(2, new int[] { 65 }); + gen.setThrowExceptionOnRollover(true); + + int i = 0; + try { + while(i < 3) { + ++i; + gen.nextValue(); + } + fail("Expected NoSuchElementException"); + } catch (NoSuchElementException e) { + assertEquals(i, 2); + } + } + + @Test + public void invalidLengths() throws Exception { + try { + new IncrementingPrintableStringGenerator(0, ATOC); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + try { + new IncrementingPrintableStringGenerator(-42, ATOC); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + } + + @Test + public void invalidCharacterSets() throws Exception { + try { + new IncrementingPrintableStringGenerator(2, null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + + try { + new IncrementingPrintableStringGenerator(2, new int[] {}); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + } +} diff --git a/core/src/test/java/site/ycsb/generator/TestRandomDiscreteTimestampGenerator.java b/core/src/test/java/site/ycsb/generator/TestRandomDiscreteTimestampGenerator.java new file mode 100644 index 0000000..4dbc9e2 --- /dev/null +++ b/core/src/test/java/site/ycsb/generator/TestRandomDiscreteTimestampGenerator.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2017 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 site.ycsb.generator; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.testng.annotations.Test; +import org.testng.collections.Lists; + +public class TestRandomDiscreteTimestampGenerator { + + @Test + public void systemTime() throws Exception { + final RandomDiscreteTimestampGenerator generator = + new RandomDiscreteTimestampGenerator(60, TimeUnit.SECONDS, 60); + List generated = Lists.newArrayList(); + for (int i = 0; i < 60; i++) { + generated.add(generator.nextValue()); + } + assertEquals(generated.size(), 60); + try { + generator.nextValue(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { } + } + + @Test + public void withStartTime() throws Exception { + final RandomDiscreteTimestampGenerator generator = + new RandomDiscreteTimestampGenerator(60, TimeUnit.SECONDS, 1072915200L, 60); + List generated = Lists.newArrayList(); + for (int i = 0; i < 60; i++) { + generated.add(generator.nextValue()); + } + assertEquals(generated.size(), 60); + Collections.sort(generated); + long ts = 1072915200L - 60; // starts 1 interval in the past + for (final long t : generated) { + assertEquals(t, ts); + ts += 60; + } + try { + generator.nextValue(); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { } + } + + @Test (expectedExceptions = IllegalArgumentException.class) + public void tooLarge() throws Exception { + new RandomDiscreteTimestampGenerator(60, TimeUnit.SECONDS, + RandomDiscreteTimestampGenerator.MAX_INTERVALS + 1); + } + + //TODO - With PowerMockito we could UT the initializeTimestamp(long) call. + // Otherwise it would involve creating more functions and that would get ugly. +} diff --git a/core/src/test/java/site/ycsb/generator/TestUnixEpochTimestampGenerator.java b/core/src/test/java/site/ycsb/generator/TestUnixEpochTimestampGenerator.java new file mode 100644 index 0000000..8c80f6f --- /dev/null +++ b/core/src/test/java/site/ycsb/generator/TestUnixEpochTimestampGenerator.java @@ -0,0 +1,122 @@ +/** + * 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 site.ycsb.generator; + +import static org.testng.Assert.assertEquals; + +import java.util.concurrent.TimeUnit; +import org.testng.annotations.Test; + +public class TestUnixEpochTimestampGenerator { + + @Test + public void defaultCtor() throws Exception { + final UnixEpochTimestampGenerator generator = + new UnixEpochTimestampGenerator(); + final long startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + 60); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + 120); + assertEquals((long) generator.lastValue(), startTime + 60); + assertEquals((long) generator.nextValue(), startTime + 180); + } + + @Test + public void ctorWithIntervalAndUnits() throws Exception { + final UnixEpochTimestampGenerator generator = + new UnixEpochTimestampGenerator(120, TimeUnit.SECONDS); + final long startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + 120); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + 240); + assertEquals((long) generator.lastValue(), startTime + 120); + } + + @Test + public void ctorWithIntervalAndUnitsAndStart() throws Exception { + final UnixEpochTimestampGenerator generator = + new UnixEpochTimestampGenerator(120, TimeUnit.SECONDS, 1072915200L); + assertEquals((long) generator.nextValue(), 1072915200L); + assertEquals((long) generator.lastValue(), 1072915200L - 120); + assertEquals((long) generator.nextValue(), 1072915200L + 120); + assertEquals((long) generator.lastValue(), 1072915200L); + } + + @Test + public void variousIntervalsAndUnits() throws Exception { + // negatives could happen, just start and roll back in time + UnixEpochTimestampGenerator generator = + new UnixEpochTimestampGenerator(-60, TimeUnit.SECONDS); + long startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime - 60); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime - 120); + assertEquals((long) generator.lastValue(), startTime - 60); + + generator = new UnixEpochTimestampGenerator(100, TimeUnit.NANOSECONDS); + startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + 100); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + 200); + assertEquals((long) generator.lastValue(), startTime + 100); + + generator = new UnixEpochTimestampGenerator(100, TimeUnit.MICROSECONDS); + startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + 100); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + 200); + assertEquals((long) generator.lastValue(), startTime + 100); + + generator = new UnixEpochTimestampGenerator(100, TimeUnit.MILLISECONDS); + startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + 100); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + 200); + assertEquals((long) generator.lastValue(), startTime + 100); + + generator = new UnixEpochTimestampGenerator(100, TimeUnit.SECONDS); + startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + 100); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + 200); + assertEquals((long) generator.lastValue(), startTime + 100); + + generator = new UnixEpochTimestampGenerator(1, TimeUnit.MINUTES); + startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + (1 * 60)); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + (2 * 60)); + assertEquals((long) generator.lastValue(), startTime + (1 * 60)); + + generator = new UnixEpochTimestampGenerator(1, TimeUnit.HOURS); + startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + (1 * 60 * 60)); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + (2 * 60 * 60)); + assertEquals((long) generator.lastValue(), startTime + (1 * 60 * 60)); + + generator = new UnixEpochTimestampGenerator(1, TimeUnit.DAYS); + startTime = generator.currentValue(); + assertEquals((long) generator.nextValue(), startTime + (1 * 60 * 60 * 24)); + assertEquals((long) generator.lastValue(), startTime); + assertEquals((long) generator.nextValue(), startTime + (2 * 60 * 60 * 24)); + assertEquals((long) generator.lastValue(), startTime + (1 * 60 * 60 * 24)); + } + + // TODO - With PowerMockito we could UT the initializeTimestamp(long) call. + // Otherwise it would involve creating more functions and that would get ugly. +} diff --git a/core/src/test/java/site/ycsb/generator/TestZipfianGenerator.java b/core/src/test/java/site/ycsb/generator/TestZipfianGenerator.java new file mode 100644 index 0000000..cca16ef --- /dev/null +++ b/core/src/test/java/site/ycsb/generator/TestZipfianGenerator.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010 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. + */ + +package site.ycsb.generator; + +import org.testng.annotations.Test; + +import static org.testng.AssertJUnit.assertFalse; + + +public class TestZipfianGenerator { + @Test + public void testMinAndMaxParameter() { + long min = 5; + long max = 10; + ZipfianGenerator zipfian = new ZipfianGenerator(min, max); + + for (int i = 0; i < 10000; i++) { + long rnd = zipfian.nextValue(); + assertFalse(rnd < min); + assertFalse(rnd > max); + } + + } +} diff --git a/core/src/test/java/site/ycsb/generator/acknowledge/AcknowledgedCounterGeneratorTest.java b/core/src/test/java/site/ycsb/generator/acknowledge/AcknowledgedCounterGeneratorTest.java new file mode 100644 index 0000000..20ae872 --- /dev/null +++ b/core/src/test/java/site/ycsb/generator/acknowledge/AcknowledgedCounterGeneratorTest.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-2017 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 site.ycsb.generator.acknowledge; + +import java.util.Random; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +import org.testng.annotations.Test; + +/** + * Tests for the AcknowledgedCounterGenerator class. + */ +public class AcknowledgedCounterGeneratorTest { + + /** + * Test that advancing past {@link Integer#MAX_VALUE} works. + */ + @Test + public void testIncrementPastIntegerMaxValue() { + final long toTry = DefaultAcknowledgedCounterGenerator.DEFAUL_WINDOW_SIZE * 3; + + DefaultAcknowledgedCounterGenerator generator = + new DefaultAcknowledgedCounterGenerator(Integer.MAX_VALUE - 1000); + + Random rand = new Random(System.currentTimeMillis()); + BlockingQueue pending = new ArrayBlockingQueue(1000); + for (long i = 0; i < toTry; ++i) { + long value = generator.nextValue(); + + while (!pending.offer(value)) { + + Long first = pending.poll(); + + // Don't always advance by one. + if (rand.nextBoolean()) { + generator.acknowledge(first); + } else { + Long second = pending.poll(); + pending.add(first); + generator.acknowledge(second); + } + } + } + + } +} diff --git a/core/src/test/java/site/ycsb/measurements/exporter/TestMeasurementsExporter.java b/core/src/test/java/site/ycsb/measurements/exporter/TestMeasurementsExporter.java new file mode 100644 index 0000000..318d5b2 --- /dev/null +++ b/core/src/test/java/site/ycsb/measurements/exporter/TestMeasurementsExporter.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015 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. + */ +package site.ycsb.measurements.exporter; + +import site.ycsb.generator.ZipfianGenerator; +import site.ycsb.measurements.Measurements; +import site.ycsb.measurements.OneMeasurementHistogram; + +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.map.ObjectMapper; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Properties; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; + +public class TestMeasurementsExporter { + @Test + public void testJSONArrayMeasurementsExporter() throws IOException { + Properties props = new Properties(); + props.put(Measurements.MEASUREMENT_TYPE_PROPERTY, "histogram"); + props.put(OneMeasurementHistogram.VERBOSE_PROPERTY, "true"); + Measurements mm = new Measurements(props); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + JSONArrayMeasurementsExporter export = new JSONArrayMeasurementsExporter(out); + + long min = 5000; + long max = 100000; + ZipfianGenerator zipfian = new ZipfianGenerator(min, max); + for (int i = 0; i < 1000; i++) { + int rnd = zipfian.nextValue().intValue(); + mm.measure("UPDATE", rnd); + } + mm.exportMeasurements(export); + export.close(); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode json = mapper.readTree(out.toString("UTF-8")); + assertTrue(json.isArray()); + assertEquals(json.get(0).get("measurement").asText(), "Operations"); + assertEquals(json.get(4).get("measurement").asText(), "MaxLatency(us)"); + assertEquals(json.get(11).get("measurement").asText(), "4"); + } +} diff --git a/core/src/test/java/site/ycsb/workloads/TestCoreWorkload.java b/core/src/test/java/site/ycsb/workloads/TestCoreWorkload.java new file mode 100644 index 0000000..2fc8256 --- /dev/null +++ b/core/src/test/java/site/ycsb/workloads/TestCoreWorkload.java @@ -0,0 +1,72 @@ +/** + * 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 site.ycsb.workloads; + +import static org.testng.Assert.assertTrue; + +import java.util.Properties; + +import org.testng.annotations.Test; + +import site.ycsb.generator.DiscreteGenerator; +import site.ycsb.workloads.core.CoreHelper; +import static site.ycsb.workloads.core.CoreConstants.*; + +public class TestCoreWorkload { + + @Test + public void createOperationChooser() { + final Properties p = new Properties(); + p.setProperty(READ_PROPORTION_PROPERTY, "0.20"); + p.setProperty(UPDATE_PROPORTION_PROPERTY, "0.20"); + p.setProperty(INSERT_PROPORTION_PROPERTY, "0.20"); + p.setProperty(SCAN_PROPORTION_PROPERTY, "0.20"); + p.setProperty(READMODIFYWRITE_PROPORTION_PROPERTY, "0.20"); + final DiscreteGenerator generator = CoreHelper.createOperationGenerator(p); + final int[] counts = new int[5]; + + for (int i = 0; i < 100; ++i) { + switch (generator.nextString()) { + case "READ": + ++counts[0]; + break; + case "UPDATE": + ++counts[1]; + break; + case "INSERT": + ++counts[2]; + break; + case "SCAN": + ++counts[3]; + break; + default: + ++counts[4]; + } + } + + for (int i : counts) { + // Doesn't do a wonderful job of equal distribution, but in a hundred, if we + // don't see at least one of each operation then the generator is really broke. + assertTrue(i > 1); + } + } + + @Test (expectedExceptions = IllegalArgumentException.class) + public void createOperationChooserNullProperties() { + CoreHelper.createOperationGenerator(null); + } +} \ No newline at end of file diff --git a/core/src/test/java/site/ycsb/workloads/TestTimeSeriesWorkload.java b/core/src/test/java/site/ycsb/workloads/TestTimeSeriesWorkload.java new file mode 100644 index 0000000..7cea7b3 --- /dev/null +++ b/core/src/test/java/site/ycsb/workloads/TestTimeSeriesWorkload.java @@ -0,0 +1,582 @@ +/** + * Copyright (c) 2017 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 site.ycsb.workloads; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; +import java.util.Vector; + +import site.ycsb.ByteIterator; +import site.ycsb.Client; +import site.ycsb.DB; +import site.ycsb.NumericByteIterator; +import site.ycsb.Status; +import site.ycsb.StringByteIterator; +import site.ycsb.Utils; +import site.ycsb.WorkloadException; +import site.ycsb.measurements.Measurements; +import site.ycsb.wrappers.DatabaseField; + +import static site.ycsb.workloads.core.CoreConstants.*; +import org.testng.annotations.Test; + +public class TestTimeSeriesWorkload { + + @Test + public void twoThreads() throws Exception { + final Properties p = getUTProperties(); + Measurements.setProperties(p); + + final TimeSeriesWorkload wl = new TimeSeriesWorkload(); + wl.init(p); + Object threadState = wl.initThread(p, 0, 2); + + MockDB db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + long timestamp = 1451606400; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.keys.get(i), "AAAA"); + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertNotNull(db.values.get(i).get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)); + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + timestamp += 60; + } + } + + threadState = wl.initThread(p, 1, 2); + db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + timestamp = 1451606400; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.keys.get(i), "AAAB"); + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertNotNull(db.values.get(i).get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)); + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + timestamp += 60; + } + } + } + + @Test (expectedExceptions = WorkloadException.class) + public void badTimeUnit() throws Exception { + final Properties p = new Properties(); + p.put(TimeSeriesWorkload.TIMESTAMP_UNITS_PROPERTY, "foobar"); + getWorkload(p, true); + } + + @Test (expectedExceptions = WorkloadException.class) + public void failedToInitWorkloadBeforeThreadInit() throws Exception { + final Properties p = getUTProperties(); + final TimeSeriesWorkload wl = getWorkload(p, false); + //wl.init(p); // <-- we NEED this :( + final Object threadState = wl.initThread(p, 0, 2); + + final MockDB db = new MockDB(); + wl.doInsert(db, threadState); + } + + @Test (expectedExceptions = IllegalStateException.class) + public void failedToInitThread() throws Exception { + final Properties p = getUTProperties(); + final TimeSeriesWorkload wl = getWorkload(p, true); + + final MockDB db = new MockDB(); + wl.doInsert(db, null); + } + + @Test + public void insertOneKeyOneTagCardinalityOne() throws Exception { + final Properties p = getUTProperties(); + p.put(FIELD_COUNT_PROPERTY, "1"); + p.put(TimeSeriesWorkload.TAG_COUNT_PROPERTY, "1"); + p.put(TimeSeriesWorkload.TAG_CARDINALITY_PROPERTY, "1"); + final TimeSeriesWorkload wl = getWorkload(p, true); + final Object threadState = wl.initThread(p, 0, 1); + + final MockDB db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + long timestamp = 1451606400; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.keys.get(i), "AAAA"); + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertTrue(((NumericByteIterator) db.values.get(i) + .get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)).isFloatingPoint()); + timestamp += 60; + } + } + + @Test + public void insertOneKeyTwoTagsLowCardinality() throws Exception { + final Properties p = getUTProperties(); + p.put(FIELD_COUNT_PROPERTY, "1"); + final TimeSeriesWorkload wl = getWorkload(p, true); + final Object threadState = wl.initThread(p, 0, 1); + + final MockDB db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + long timestamp = 1451606400; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.keys.get(i), "AAAA"); + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertTrue(((NumericByteIterator) db.values.get(i) + .get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)).isFloatingPoint()); + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + timestamp += 60; + } + } + } + + @Test + public void insertTwoKeysTwoTagsLowCardinality() throws Exception { + final Properties p = getUTProperties(); + + final TimeSeriesWorkload wl = getWorkload(p, true); + final Object threadState = wl.initThread(p, 0, 1); + + final MockDB db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + long timestamp = 1451606400; + int metricCtr = 0; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertTrue(((NumericByteIterator) db.values.get(i) + .get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)).isFloatingPoint()); + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + } + if (metricCtr++ > 1) { + assertEquals(db.keys.get(i), "AAAB"); + if (metricCtr >= 4) { + metricCtr = 0; + timestamp += 60; + } + } else { + assertEquals(db.keys.get(i), "AAAA"); + } + } + } + + @Test + public void insertTwoKeysTwoThreads() throws Exception { + final Properties p = getUTProperties(); + + final TimeSeriesWorkload wl = getWorkload(p, true); + Object threadState = wl.initThread(p, 0, 2); + + MockDB db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + long timestamp = 1451606400; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.keys.get(i), "AAAA"); // <-- key 1 + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertTrue(((NumericByteIterator) db.values.get(i) + .get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)).isFloatingPoint()); + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + timestamp += 60; + } + } + + threadState = wl.initThread(p, 1, 2); + db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + timestamp = 1451606400; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.keys.get(i), "AAAB"); // <-- key 2 + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertNotNull(db.values.get(i).get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)); + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + timestamp += 60; + } + } + } + + @Test + public void insertThreeKeysTwoThreads() throws Exception { + // To make sure the distribution doesn't miss any metrics + final Properties p = getUTProperties(); + p.put(FIELD_COUNT_PROPERTY, "3"); + + final TimeSeriesWorkload wl = getWorkload(p, true); + Object threadState = wl.initThread(p, 0, 2); + + MockDB db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + long timestamp = 1451606400; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.keys.get(i), "AAAA"); + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertTrue(((NumericByteIterator) db.values.get(i) + .get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)).isFloatingPoint()); + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + timestamp += 60; + } + } + + threadState = wl.initThread(p, 1, 2); + db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + timestamp = 1451606400; + int metricCtr = 0; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertNotNull(db.values.get(i).get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)); + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + } + if (metricCtr++ > 1) { + assertEquals(db.keys.get(i), "AAAC"); + if (metricCtr >= 4) { + metricCtr = 0; + timestamp += 60; + } + } else { + assertEquals(db.keys.get(i), "AAAB"); + } + } + } + + @Test + public void insertWithValidation() throws Exception { + final Properties p = getUTProperties(); + p.put(FIELD_COUNT_PROPERTY, "1"); + p.put(DATA_INTEGRITY_PROPERTY, "true"); + p.put(TimeSeriesWorkload.VALUE_TYPE_PROPERTY, "integers"); + final TimeSeriesWorkload wl = getWorkload(p, true); + final Object threadState = wl.initThread(p, 0, 1); + + final MockDB db = new MockDB(); + for (int i = 0; i < 74; i++) { + assertTrue(wl.doInsert(db, threadState)); + } + + assertEquals(db.keys.size(), 74); + assertEquals(db.values.size(), 74); + long timestamp = 1451606400; + for (int i = 0; i < db.keys.size(); i++) { + assertEquals(db.keys.get(i), "AAAA"); + assertEquals(db.values.get(i).get("AA").toString(), "AAAA"); + assertEquals(Utils.bytesToLong(db.values.get(i).get( + TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT).toArray()), timestamp); + assertFalse(((NumericByteIterator) db.values.get(i) + .get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)).isFloatingPoint()); + + // validation check + final TreeMap validationTags = new TreeMap(); + for (final Entry entry : db.values.get(i).entrySet()) { + if (entry.getKey().equals(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT) || + entry.getKey().equals(TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT)) { + continue; + } + validationTags.put(entry.getKey(), entry.getValue().toString()); + } + assertEquals(wl.validationFunction(db.keys.get(i), timestamp, validationTags), + ((NumericByteIterator) db.values.get(i).get(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT)).getLong()); + + if (i % 2 == 0) { + assertEquals(db.values.get(i).get("AB").toString(), "AAAA"); + } else { + assertEquals(db.values.get(i).get("AB").toString(), "AAAB"); + timestamp += 60; + } + } + } + + @Test + public void read() throws Exception { + final Properties p = getUTProperties(); + final TimeSeriesWorkload wl = getWorkload(p, true); + final Object threadState = wl.initThread(p, 0, 1); + + final MockDB db = new MockDB(); + for (int i = 0; i < 20; i++) { + wl.doTransactionRead(db, threadState); + } + } + + @Test + public void verifyRow() throws Exception { + final Properties p = getUTProperties(); + final TimeSeriesWorkload wl = getWorkload(p, true); + + final TreeMap validationTags = new TreeMap(); + final HashMap cells = new HashMap(); + + validationTags.put("AA", "AAAA"); + cells.put("AA", new StringByteIterator("AAAA")); + validationTags.put("AB", "AAAB"); + cells.put("AB", new StringByteIterator("AAAB")); + long hash = wl.validationFunction("AAAA", 1451606400L, validationTags); + + cells.put(TimeSeriesWorkload.TIMESTAMP_KEY_PROPERTY_DEFAULT, new NumericByteIterator(1451606400L)); + cells.put(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT, new NumericByteIterator(hash)); + + assertEquals(wl.verifyRow("AAAA", cells), Status.OK); + + // tweak the last value a bit + for (final ByteIterator it : cells.values()) { + it.reset(); + } + cells.put(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT, new NumericByteIterator(hash + 1)); + assertEquals(wl.verifyRow("AAAA", cells), Status.UNEXPECTED_STATE); + + // no value cell, returns an unexpected state + for (final ByteIterator it : cells.values()) { + it.reset(); + } + cells.remove(TimeSeriesWorkload.VALUE_KEY_PROPERTY_DEFAULT); + assertEquals(wl.verifyRow("AAAA", cells), Status.UNEXPECTED_STATE); + } + + @Test + public void validateSettingsDataIntegrity() throws Exception { + Properties p = getUTProperties(); + + // data validation incompatibilities + p.setProperty(DATA_INTEGRITY_PROPERTY, "true"); + try { + getWorkload(p, true); + fail("Expected WorkloadException"); + } catch (WorkloadException e) { } + + p.setProperty(TimeSeriesWorkload.VALUE_TYPE_PROPERTY, "integers"); // now it's ok + p.setProperty(TimeSeriesWorkload.GROUPBY_PROPERTY, "sum"); // now it's not + try { + getWorkload(p, true); + fail("Expected WorkloadException"); + } catch (WorkloadException e) { } + + p.setProperty(TimeSeriesWorkload.GROUPBY_PROPERTY, ""); + p.setProperty(TimeSeriesWorkload.DOWNSAMPLING_FUNCTION_PROPERTY, "sum"); + p.setProperty(TimeSeriesWorkload.DOWNSAMPLING_INTERVAL_PROPERTY, "60"); + try { + getWorkload(p, true); + fail("Expected WorkloadException"); + } catch (WorkloadException e) { } + + p.setProperty(TimeSeriesWorkload.DOWNSAMPLING_FUNCTION_PROPERTY, ""); + p.setProperty(TimeSeriesWorkload.DOWNSAMPLING_INTERVAL_PROPERTY, ""); + p.setProperty(TimeSeriesWorkload.QUERY_TIMESPAN_PROPERTY, "60"); + try { + getWorkload(p, true); + fail("Expected WorkloadException"); + } catch (WorkloadException e) { } + + p = getUTProperties(); + p.setProperty(DATA_INTEGRITY_PROPERTY, "true"); + p.setProperty(TimeSeriesWorkload.VALUE_TYPE_PROPERTY, "integers"); + p.setProperty(TimeSeriesWorkload.RANDOMIZE_TIMESERIES_ORDER_PROPERTY, "true"); + try { + getWorkload(p, true); + fail("Expected WorkloadException"); + } catch (WorkloadException e) { } + + p.setProperty(TimeSeriesWorkload.RANDOMIZE_TIMESERIES_ORDER_PROPERTY, "false"); + p.setProperty(TimeSeriesWorkload.INSERT_START_PROPERTY, ""); + try { + getWorkload(p, true); + fail("Expected WorkloadException"); + } catch (WorkloadException e) { } + } + + /** Helper method that generates unit testing defaults for the properties map */ + private Properties getUTProperties() { + final Properties p = new Properties(); + p.put(Client.RECORD_COUNT_PROPERTY, "10"); + p.put(FIELD_COUNT_PROPERTY, "2"); + p.put(FIELD_LENGTH_PROPERTY, "4"); + p.put(TimeSeriesWorkload.TAG_KEY_LENGTH_PROPERTY, "2"); + p.put(TimeSeriesWorkload.TAG_VALUE_LENGTH_PROPERTY, "4"); + p.put(TimeSeriesWorkload.TAG_COUNT_PROPERTY, "2"); + p.put(TimeSeriesWorkload.TAG_CARDINALITY_PROPERTY, "1,2"); + p.put(CoreWorkload.INSERT_START_PROPERTY, "1451606400"); + p.put(TimeSeriesWorkload.DELAYED_SERIES_PROPERTY, "0"); + p.put(TimeSeriesWorkload.RANDOMIZE_TIMESERIES_ORDER_PROPERTY, "false"); + return p; + } + + /** Helper to setup the workload for testing. */ + private TimeSeriesWorkload getWorkload(final Properties p, final boolean init) + throws WorkloadException { + Measurements.setProperties(p); + if (!init) { + return new TimeSeriesWorkload(); + } else { + final TimeSeriesWorkload workload = new TimeSeriesWorkload(); + workload.init(p); + return workload; + } + } + + static class MockDB extends DB { + final List keys = new ArrayList(); + final List> values = + new ArrayList>(); + + @Override + public Status read(String table, String key, Set fields, + Map result) { + return Status.OK; + } + + @Override + public Status scan(String table, String startkey, int recordcount, + Set fields, Vector> result) { + // TODO Auto-generated method stub + return Status.OK; + } + + @Override + public Status update(String table, String key, + Map values) { + // TODO Auto-generated method stub + return Status.OK; + } + + @Override + public Status insert(String table, String key, List values) { + keys.add(key); + Map buff = new HashMap<>(); + for(DatabaseField f : values) { + buff.put(f.getFieldname(), f.getContent().asIterator()); + } + this.values.add(buff); + return Status.OK; + } + + @Override + public Status delete(String table, String key) { + // TODO Auto-generated method stub + return Status.OK; + } + + public void dumpStdout() { + for (int i = 0; i < keys.size(); i++) { + System.out.print("[" + i + "] Key: " + keys.get(i) + " Values: {"); + int x = 0; + for (final Entry entry : values.get(i).entrySet()) { + if (x++ > 0) { + System.out.print(", "); + } + System.out.print("{" + entry.getKey() + " => "); + if (entry.getKey().equals("YCSBV")) { + System.out.print(new String(Utils.bytesToDouble(entry.getValue().toArray()) + "}")); + } else if (entry.getKey().equals("YCSBTS")) { + System.out.print(new String(Utils.bytesToLong(entry.getValue().toArray()) + "}")); + } else { + System.out.print(new String(entry.getValue().toArray()) + "}"); + } + } + System.out.println("}"); + } + } + } +} \ No newline at end of file diff --git a/couchbase3/README.md b/couchbase3/README.md new file mode 100644 index 0000000..7986811 --- /dev/null +++ b/couchbase3/README.md @@ -0,0 +1,145 @@ + + +# Couchbase (SDK 2.x) Driver for YCSB +This driver is a binding for the YCSB facilities to operate against a Couchbase Server cluster. It uses the official +Couchbase Java SDK (version 2.x) and provides a rich set of configuration options, including support for the N1QL +query language. + +## Quickstart + +### 1. Start Couchbase Server +You need to start a single node or a cluster to point the client at. Please see [http://couchbase.com](couchbase.com) +for more details and instructions. + +### 2. Set up YCSB +You can either download the release zip and run it, or just clone from master. + +``` +git clone git://github.com/brianfrankcooper/YCSB.git +cd YCSB +mvn clean package +``` + +### 3. Run the Workload +Before you can actually run the workload, you need to "load" the data first. + +``` +bin/ycsb load couchbase2 -s -P workloads/workloada +``` + +Then, you can run the workload: + +``` +bin/ycsb run couchbase2 -s -P workloads/workloada +``` + +Please see the general instructions in the `doc` folder if you are not sure how it all works. You can apply a property +(as seen in the next section) like this: + +``` +bin/ycsb run couchbase -s -P workloads/workloada -p couchbase.epoll=true +``` + +## N1QL Index Setup +In general, every time N1QL is used (either implicitly through using `workloade` or through setting `kv=false`) some +kind of index must be present to make it work. Depending on the workload and data size, choosing the right index is +crucial at runtime in order to get the best performance. If in doubt, please ask at the +[forums](http://forums.couchbase.com) or get in touch with our team at Couchbase. + +For `workloade` and the default `readallfields=true` we recommend creating the following index, and if using Couchbase +Server 4.5 or later with the "Memory Optimized Index" setting on the bucket. + +``` +CREATE PRIMARY INDEX ON `bucketname`; +``` + +Couchbase Server prior to 4.5 may need a slightly different index to deliver the best performance. In those releases +additional covering information may be added to the index with this form. + +``` +-CREATE INDEX wle_idx ON `bucketname`(meta().id); +``` + +For other workloads, different index setups might be even more performant. + +## Performance Considerations +As it is with any benchmark, there are lot of knobs to tune in order to get great or (if you are reading +this and trying to write a competitor benchmark ;-)) bad performance. + +The first setting you should consider, if you are running on Linux 64bit is setting `-p couchbase.epoll=true`. This will +then turn on the Epoll IO mechanisms in the underlying Netty library which provides better performance since it has less +synchronization to do than the NIO default. This only works on Linux, but you are benchmarking on the OS you are +deploying to, right? + +The second option, `boost`, sounds more magic than it actually is. By default this benchmark trades CPU for throughput, +but this can be disabled by setting `-p couchbase.boost=0`. This defaults to 3, and 3 is the number of event loops run +in the IO layer. 3 is a reasonable default but you should set it to the number of **physical** cores you have available +on the machine if you only plan to run one YCSB instance. Make sure (using profiling) to max out your cores, but don't +overdo it. + +## Sync vs Async +By default, since YCSB is sync the code will always wait for the operation to complete. In some cases it can be useful +to just "drive load" and disable the waiting. Note that when the "-p couchbase.syncMutationResponse=false" option is +used, the measured results by YCSB can basically be thrown away. Still helpful sometimes during load phases to speed +them up :) + +## Debugging Latency +The Couchbase Java SDK has the ability to collect and dump different kinds of metrics which allow you to analyze +performance during benchmarking and production. By default this option is disabled in the benchmark, but by setting +`couchbase.networkMetricsInterval` and/or `couchbase.runtimeMetricsInterval` to something greater than 0 it will +output the information as JSON into the configured logger. The number provides is the interval in seconds. If you are +unsure what interval to pick, start with 10 or 30 seconds, depending on your runtime length. + +This is how such logs look like: + +``` +INFO: {"heap.used":{"init":268435456,"used":36500912,"committed":232259584,"max":3817865216},"gc.ps marksweep.collectionTime":0,"gc.ps scavenge.collectionTime":54,"gc.ps scavenge.collectionCount":17,"thread.count":26,"offHeap.used":{"init":2555904,"used":30865944,"committed":31719424,"max":-1},"gc.ps marksweep.collectionCount":0,"heap.pendingFinalize":0,"thread.peakCount":26,"event":{"name":"RuntimeMetrics","type":"METRIC"},"thread.startedCount":28} +INFO: {"localhost/127.0.0.1:11210":{"BINARY":{"ReplaceRequest":{"SUCCESS":{"metrics":{"percentiles":{"50.0":102,"90.0":136,"95.0":155,"99.0":244,"99.9":428},"min":55,"max":1564,"count":35787,"timeUnit":"MICROSECONDS"}}},"GetRequest":{"SUCCESS":{"metrics":{"percentiles":{"50.0":74,"90.0":98,"95.0":110,"99.0":158,"99.9":358},"min":34,"max":2310,"count":35604,"timeUnit":"MICROSECONDS"}}},"GetBucketConfigRequest":{"SUCCESS":{"metrics":{"percentiles":{"50.0":462,"90.0":462,"95.0":462,"99.0":462,"99.9":462},"min":460,"max":462,"count":1,"timeUnit":"MICROSECONDS"}}}}},"event":{"name":"NetworkLatencyMetrics","type":"METRIC"}} +``` + +It is recommended to either feed it into a program which can analyze and visualize JSON or just dump it into a JSON +pretty printer and look at it manually. Since the output can be changed (only by changing the code at the moment), you +can even configure to put those messages into another couchbase bucket and then analyze it through N1QL! You can learn +more about this in general [in the official docs](http://developer.couchbase.com/documentation/server/4.0/sdks/java-2.2/event-bus-metrics.html). + + +## Configuration Options +Since no setup is the same and the goal of YCSB is to deliver realistic benchmarks, here are some setups that you can +tune. Note that if you need more flexibility (let's say a custom transcoder), you still need to extend this driver and +implement the facilities on your own. + +You can set the following properties (with the default settings applied): + + - couchbase.host=127.0.0.1: The hostname from one server. + - couchbase.bucket=default: The bucket name to use. + - couchbase.password=: The password of the bucket. + - couchbase.syncMutationResponse=true: If mutations should wait for the response to complete. + - couchbase.persistTo=0: Persistence durability requirement + - couchbase.replicateTo=0: Replication durability requirement + - couchbase.upsert=false: Use upsert instead of insert or replace. + - couchbase.adhoc=false: If set to true, prepared statements are not used. + - couchbase.kv=true: If set to false, mutation operations will also be performed through N1QL. + - couchbase.maxParallelism=1: The server parallelism for all n1ql queries. + - couchbase.kvEndpoints=1: The number of KV sockets to open per server. + - couchbase.queryEndpoints=5: The number of N1QL Query sockets to open per server. + - couchbase.epoll=false: If Epoll instead of NIO should be used (only available for linux. + - couchbase.boost=3: If > 0 trades CPU for higher throughput. N is the number of event loops, ideally + set to the number of physical cores. Setting higher than that will likely degrade performance. + - couchbase.networkMetricsInterval=0: The interval in seconds when latency metrics will be logged. + - couchbase.runtimeMetricsInterval=0: The interval in seconds when runtime metrics will be logged. + - couchbase.documentExpiry=0: Document Expiry is the amount of time(second) until a document expires in Couchbase. \ No newline at end of file diff --git a/couchbase3/pom.xml b/couchbase3/pom.xml new file mode 100644 index 0000000..8c7529f --- /dev/null +++ b/couchbase3/pom.xml @@ -0,0 +1,89 @@ + + + + + 4.0.0 + + site.ycsb + binding-parent + 0.18.0-SNAPSHOT + ../binding-parent + + + couchbase3-binding + Couchbase Java SDK 3.x Binding + jar + + + + com.couchbase.client + java-client + ${couchbase3.version} + + + site.ycsb + core + ${project.version} + provided + + + com.couchbase.client + couchbase-transactions + 1.2.4 + + + org.slf4j + slf4j-api + 2.0.9 + + + org.slf4j + slf4j-simple + 2.0.9 + + + io.projectreactor + reactor-core + 3.4.18 + + + io.projectreactor.addons + reactor-extra + 3.4.8 + + + io.projectreactor.addons + reactor-adapter + 3.4.8 + + + io.reactivex + rxjava-reactive-streams + 1.2.1 + + + ch.qos.logback + logback-classic + 1.2.11 + + + + diff --git a/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3Client.java b/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3Client.java new file mode 100644 index 0000000..b26bca7 --- /dev/null +++ b/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3Client.java @@ -0,0 +1,881 @@ +/* + * Copyright (c) 2019 Yahoo! Inc. All rights reserved. + * Copyright (c) 2023-204 benchANT GmbH. 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 agrlaw 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 site.ycsb.db.couchbase3; + +import static com.couchbase.client.java.kv.InsertOptions.insertOptions; +import static com.couchbase.client.java.kv.RemoveOptions.removeOptions; +import static com.couchbase.client.java.kv.ReplaceOptions.replaceOptions; +import static com.couchbase.client.java.kv.UpsertOptions.upsertOptions; + +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.atomic.AtomicInteger; + +import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.JsonNode; +import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.couchbase.client.core.env.IoConfig; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.core.env.TimeoutConfig; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.ReactiveBucket; +import com.couchbase.client.java.ReactiveCluster; +import com.couchbase.client.java.ReactiveCollection; +import com.couchbase.client.java.codec.RawJsonTranscoder; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.json.JacksonTransformers; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.kv.CommonDurabilityOptions; +import com.couchbase.client.java.kv.GetOptions; +import com.couchbase.client.java.kv.GetResult; +import com.couchbase.client.java.kv.InsertOptions; +import com.couchbase.client.java.kv.MutationResult; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.kv.UpsertOptions; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.query.QueryStatus; +import com.couchbase.client.java.query.ReactiveQueryResult; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import site.ycsb.ByteIterator; +import site.ycsb.DB; +import site.ycsb.DBException; +import site.ycsb.IndexableDB; +import site.ycsb.Status; +import site.ycsb.StringByteIterator; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +/** + * A class that wraps the 3.x Couchbase SDK to be used with YCSB. + * + *

The following options can be passed when using this database client to override the defaults. + * + *

    + *
  • couchbase.host=127.0.0.1 The hostname from one server.
  • + *
  • couchbase.bucket=ycsb The bucket name to use.
  • + *
  • couchbase.scope=_default The scope to use.
  • + *
  • couchbase.collection=_default The collection to use.
  • + *
  • couchbase.password= The password of the bucket.
  • + *
  • couchbase.durability= Durability level to use.
  • + *
  • couchbase.persistTo=0 Persistence durability requirement.
  • + *
  • couchbase.replicateTo=0 Replication durability requirement.
  • + *
  • couchbase.upsert=false Use upsert instead of insert or replace.
  • + *
  • couchbase.adhoc=false If set to true, prepared statements are not used.
  • + *
  • couchbase.maxParallelism=1 The server parallelism for all n1ql queries.
  • + *
  • couchbase.kvEndpoints=1 The number of KV sockets to open per server.
  • + *
  • couchbase.sslMode=false Set to true to use SSL to connect to the cluster.
  • + *
  • couchbase.sslNoVerify=true Set to false to check the SSL server certificate.
  • + *
  • couchbase.certificateFile= Path to file containing certificates to trust.
  • + *
+ */ + +public class Couchbase3Client extends DB implements IndexableDB { + private static final String MAX_RETRY_PROPERTY = "couchbase.maxretry"; + private static final String MAX_RETRY_DEFAULT = "0"; + public static final String INDEX_LIST_PROPERTY = "couchbase.indexlist"; + // private static final Logger LOGGER = LoggerFactory.getLogger(Couchbase3Client.class.getName()); + private static final String KEY_SEPARATOR = ":"; + private static final String KEYSPACE_SEPARATOR = "."; + private static volatile ClusterEnvironment environment; + private static final AtomicInteger OPEN_CLIENTS = new AtomicInteger(0); + private static final Object INIT_COORDINATOR = new Object(); + static volatile Cluster cluster; + private static volatile ReactiveCluster reactiveCluster; + private static volatile Bucket bucket; + private static volatile ClusterOptions clusterOptions; + private volatile DurabilityLevel durabilityLevel; + private volatile PersistTo persistTo; + private volatile ReplicateTo replicateTo; + private volatile boolean useDurabilityLevels; + private volatile ArrayList errors = new ArrayList<>(); + private boolean adhoc; + private int maxParallelism; + static String bucketName; + private String scopeName; + private String collectionName; + private static boolean collectionEnabled; + private static boolean scopeEnabled; + private static String username; + private static String password; + private static String hostname; + private static long kvTimeoutMillis; + private static long queryTimeoutMillis; + private static int kvEndpoints; + private boolean upsert; + private static boolean sslMode; + private static boolean sslNoVerify; + private String certificateFile; + private static String keyspaceName; + private static volatile AtomicInteger primaryKeySeq; + private static int numRetries; + private static boolean useTypedFields; + /** The batch size to use for inserts. */ + private static int batchSize; + /** The bulk inserts pending for the thread. */ + private final Map> bulkInserts = new HashMap>(); + private static boolean debug = false; + // private java.util.Base64.Encoder encoder = java.util.Base64.getEncoder(); + private static final int MAX_ERRORS = 1024; + + private void addToErrors(Throwable t) { + if(errors.size() < MAX_ERRORS) { + errors.add(t); + } + } + + @Override + public void init() throws DBException { + Properties props = getProperties(); + primaryKeySeq = new AtomicInteger(); + debug = Boolean.parseBoolean(getProperties().getProperty("debug", "false")); + bucketName = props.getProperty("couchbase.bucket", "ycsb"); + scopeName = props.getProperty("couchbase.scope", "_default"); + collectionName = props.getProperty("couchbase.collection", "_default"); + scopeEnabled = scopeName != "_default"; + collectionEnabled = collectionName != "_default"; + keyspaceName = getKeyspaceName(); + useTypedFields = "true".equalsIgnoreCase(props.getProperty(TYPED_FIELDS_PROPERTY)); + // Set insert batchsize, default 1 - to be YCSB-original equivalent + batchSize = Integer.parseInt(props.getProperty("batchsize", "1")); + numRetries = Integer.parseInt(props.getProperty(MAX_RETRY_PROPERTY, MAX_RETRY_DEFAULT)); + String rawDurabilityLevel = props.getProperty("couchbase.durability", null); + if (rawDurabilityLevel != null) { + if (props.containsKey("couchbase.persistTo") || props.containsKey("couchbase.replicateTo")) { + throw new DBException("Durability setting and persist/replicate settings are mutually exclusive."); + } + try { + durabilityLevel = parseDurabilityLevel(rawDurabilityLevel); + useDurabilityLevels = true; + } catch (DBException e) { + System.err.println("Failed to parse durability level using defaults"); + } + } else { + try { + persistTo = parsePersistTo(props.getProperty("couchbase.persistTo", "0")); + replicateTo = parseReplicateTo(props.getProperty("couchbase.replicateTo", "0")); + useDurabilityLevels = false; + } catch (DBException e) { + System.err.println("Failed to parse persist/replicate levels using defaults"); + } + } + + adhoc = props.getProperty("couchbase.adhoc", "false").equals("true"); + maxParallelism = Integer.parseInt(props.getProperty("couchbase.maxParallelism", "0")); + upsert = props.getProperty("couchbase.upsert", "false").equals("true"); + + hostname = props.getProperty("couchbase.host", "127.0.0.1"); + username = props.getProperty("couchbase.username", "Administrator"); + password = props.getProperty("couchbase.password", "password"); + + sslMode = props.getProperty("couchbase.sslMode", "false").equals("true"); + sslNoVerify = props.getProperty("couchbase.sslNoVerify", "true").equals("true"); + certificateFile = props.getProperty("couchbase.certificateFile", "none"); + + synchronized (INIT_COORDINATOR) { + if (environment == null) { + + boolean enableMutationToken = Boolean.parseBoolean(props.getProperty("couchbase.enableMutationToken", "false")); + + kvTimeoutMillis = Integer.parseInt(props.getProperty("couchbase.kvTimeout", "2000")); + queryTimeoutMillis = Integer.parseInt(props.getProperty("couchbase.queryTimeout", "14000")); + kvEndpoints = Integer.parseInt(props.getProperty("couchbase.kvEndpoints", "1")); + + if (sslMode) { + ClusterEnvironment.Builder clusterEnvironment = ClusterEnvironment + .builder() + .timeoutConfig( + TimeoutConfig.builder() + .kvTimeout(Duration.ofMillis(kvTimeoutMillis)) + .queryTimeout(Duration.ofMillis(queryTimeoutMillis)) + ) + .ioConfig(IoConfig.builder() + .enableMutationTokens(enableMutationToken) + .numKvConnections(kvEndpoints) + ); + + if (sslNoVerify) { + clusterEnvironment.securityConfig(SecurityConfig.enableTls(true) + .enableHostnameVerification(false) + .trustManagerFactory(InsecureTrustManagerFactory.INSTANCE)); + } else if (!certificateFile.equals("none")) { + clusterEnvironment.securityConfig(SecurityConfig.enableTls(true) + .trustCertificate(Paths.get(certificateFile))); + } else { + clusterEnvironment.securityConfig(SecurityConfig.enableTls(true)); + } + + environment = clusterEnvironment.build(); + } else { + environment = ClusterEnvironment + .builder() + .timeoutConfig( + TimeoutConfig.kvTimeout(Duration.ofMillis(kvTimeoutMillis))) + .ioConfig(IoConfig.enableMutationTokens(enableMutationToken) + .numKvConnections(kvEndpoints)) + .build(); + } + + clusterOptions = ClusterOptions.clusterOptions(username, password); + clusterOptions.environment(environment); + cluster = Cluster.connect(hostname, clusterOptions); + reactiveCluster = cluster.reactive(); + bucket = cluster.bucket(bucketName); + + List indexes = Couchbase3IndexHelper.getIndexList(props); + Couchbase3IndexHelper.setIndexes(props, indexes); + } + } + OPEN_CLIENTS.incrementAndGet(); + } + + /** + * Checks the replicate parameter value. + * @param property provided replicateTo parameter. + * @return ReplicateTo value. + */ + private static ReplicateTo parseReplicateTo(final String property) throws DBException { + int value = Integer.parseInt(property); + switch (value) { + case 0: + return ReplicateTo.NONE; + case 1: + return ReplicateTo.ONE; + case 2: + return ReplicateTo.TWO; + case 3: + return ReplicateTo.THREE; + default: + throw new DBException("\"couchbase.replicateTo\" must be between 0 and 3"); + } + } + + /** + * Checks the persist parameter value. + * @param property provided persistTo parameter. + * @return PersistTo value. + */ + private static PersistTo parsePersistTo(final String property) throws DBException { + int value = Integer.parseInt(property); + switch (value) { + case 0: + return PersistTo.NONE; + case 1: + return PersistTo.ONE; + case 2: + return PersistTo.TWO; + case 3: + return PersistTo.THREE; + case 4: + return PersistTo.FOUR; + default: + throw new DBException("\"couchbase.persistTo\" must be between 0 and 4"); + } + } + + /** + * Checks the durability parameter. + * @param property provided durability parameter. + * @return DurabilityLevel value. + */ + private static DurabilityLevel parseDurabilityLevel(final String property) throws DBException { + + int value = Integer.parseInt(property); + + switch(value){ + case 0: + return DurabilityLevel.NONE; + case 1: + return DurabilityLevel.MAJORITY; + case 2: + return DurabilityLevel.MAJORITY_AND_PERSIST_TO_ACTIVE; + case 3: + return DurabilityLevel.PERSIST_TO_MAJORITY; + default : + throw new DBException("\"couchbase.durability\" must be between 0 and 3"); + } + } + + @Override + public synchronized void cleanup() { + int clients = OPEN_CLIENTS.decrementAndGet(); + if (clients == 0 && environment != null) { + cluster.disconnect(); + environment.shutdown(); + environment = null; + } + System.err.println(Thread.currentThread().getName() + ": dumping errors"); + Iterator it = errors.iterator(); + while(it.hasNext()) { + Throwable t = (Throwable)it.next(); + t.printStackTrace(System.err); + } + } + + /** + * Helper function to generate the keyspace name. + * @return a string with the computed keyspace name + */ + private String getKeyspaceName() { + if (scopeEnabled || collectionEnabled) { + return bucketName + KEYSPACE_SEPARATOR + this.scopeName + KEYSPACE_SEPARATOR + this.collectionName; + } else { + return bucketName; + } + } + + /** + * Helper method to turn the prefix and key into a proper document ID. + * + * @param prefix the prefix (table). + * @param key the key itself. + * @return a document ID that can be used with Couchbase. + */ + private static String formatId(final String prefix, final String key) { + return prefix + KEY_SEPARATOR + key; + } + + private Status batchInsert(final String table, final String key, Map encoding) { + final String theId = formatId(table, key); + bulkInserts.put(theId, encoding); + if(bulkInserts.size() < batchSize) { + return Status.BATCHED_OK; + } + // System.exit(-1); + final Map> localInserts = new HashMap>(bulkInserts); + bulkInserts.clear(); + ReactiveBucket rBucket = bucket.reactive(); + ReactiveCollection collection = collectionEnabled + ? rBucket.scope(this.scopeName).collection(this.collectionName) + : rBucket.defaultCollection(); + // bulk inserts has right size, let's send it + // Iterate over a list of documents to insert. + CommonDurabilityOptions opts = upsert + ? upsertOptions() + : insertOptions(); + if (useDurabilityLevels) { + opts.durability(durabilityLevel); + } else { + opts.durability(persistTo, replicateTo); + } + while (true) { + int retryCount = 0; + final Map> failed = new HashMap>(); + List results = Flux.fromIterable(localInserts.keySet()) + .flatMap(id -> { + if(upsert) { + return collection + .upsert(id, localInserts.get(id), (UpsertOptions) opts ) + .onErrorResume(t -> { + addToErrors(t); + if(debug) { + System.err.println("one insert failed in batch upsert loop with exception"); + t.printStackTrace(System.err); + } + failed.put(id, encoding); return Mono.empty(); + }); + } + else{ + return collection + .insert(id, localInserts.get(id), (InsertOptions) opts ) + .onErrorResume(t -> { + addToErrors(t); + if(debug) { + System.err.println("one insert failed in batch insert loop with exception :"); + t.printStackTrace(System.err); + } + failed.put(id, encoding); return Mono.empty(); + }); + } + }).collectList() + .block(); // Wait until all operations have completed. + if(failed.isEmpty()) { + return Status.OK; + } + // at least one document failed + if (retryCount == numRetries) { + return Status.ERROR; + } else { + Couchbase3QueryHelper.retryWait(retryCount); + } + retryCount++; + localInserts.clear();; + localInserts.putAll(failed); + } + } + /** + * Insert a record. + * @param table The name of the table. + * @param key The record key of the record to insert. + * @param values A HashMap of field/value pairs to insert in the record. + */ + @Override + public Status insert(final String table, final String key, List values_) { + int retryCount = 0; + Map encoding = useTypedFields + ? Couchbase3QueryHelper.encodeWithTypes(values_) + : Couchbase3QueryHelper.encode(DB.fieldListAsIteratorMap(values_)); + if(batchSize > 1) { + return batchInsert(table, key, (Map )encoding); + } + while (true) { + try { + Collection collection = collectionEnabled ? + bucket.scope(this.scopeName).collection(this.collectionName) : bucket.defaultCollection(); + // not adding "record_id" as other implementations will need to runs scans as well + // values.put("record_id", new StringByteIterator(String.valueOf(primaryKeySeq.incrementAndGet()))); + if (useDurabilityLevels) { + if (upsert) { + collection.upsert(formatId(table, key), encoding, upsertOptions().durability(durabilityLevel)); + } else { + collection.insert(formatId(table, key), encoding, insertOptions().durability(durabilityLevel)); + } + } else { + if (upsert) { + collection.upsert(formatId(table, key), encoding, upsertOptions().durability(persistTo, replicateTo)); + } else { + collection.insert(formatId(table, key), encoding, insertOptions().durability(persistTo, replicateTo)); + } + } + return Status.OK; + } catch (Throwable t) { + if (retryCount == numRetries) { + addToErrors(t); + if(debug) { + System.err.println("insert failed with exception :"); + t.printStackTrace(System.err); + } + return Status.ERROR; + } else { + Couchbase3QueryHelper.retryWait(retryCount); + retryCount++; + } + } + } + } + + /** + * Perform key/value read ("get"). + * @param table The name of the table. + * @param key The record key of the record to read. + * @param fields The list of fields to read, or null for all of them. + * @param result A HashMap of field/value pairs for the result. + */ + @Override + public Status read(final String table, final String key, final Set fields, + final Map result) { + int retryCount = 0; + while (true) { + try { + Collection collection = collectionEnabled ? + bucket.scope(this.scopeName).collection(this.collectionName) : bucket.defaultCollection(); + GetResult document = collection.get(formatId(table, key)); + Couchbase3QueryHelper.extractFields(document.contentAsObject(), fields, result); + return Status.OK; + } catch (DocumentNotFoundException e) { + return Status.NOT_FOUND; + } catch (Throwable t) { + if (retryCount == numRetries) { + addToErrors(t); + if(debug) { + System.err.println("read failed with exception : "); + t.printStackTrace(System.err); + } + return Status.ERROR; + } else { + retryCount++; + Couchbase3QueryHelper.retryWait(retryCount); + } + } + } + } + + /** + * Query for specific rows of data using SQL++. + * @param table The name of the table. + * @param startkey The record key of the first record to read. + * @param recordcount The number of records to read. + * @param fields The list of fields to read, or null for all of them. + * @param result A Vector of HashMaps, where each HashMap is a set field/value pairs for one record. + */ + @Override + public Status scan(final String table, final String startkey, final int recordcount, final Set fields, + final Vector> result) { + int retryCount = 0; + while (true) { + try { + if (fields == null || fields.isEmpty()) { + return scanAllFields(table, startkey, recordcount, result); + } else { + return scanSpecificFields(table, startkey, recordcount, fields, result); + } + } catch (Throwable t) { + if (retryCount == numRetries) { + addToErrors(t); + if(debug) { + System.err.println("scan failed with exception"); + t.printStackTrace(System.err); + } + return Status.ERROR; + } else { + ++retryCount; + Couchbase3QueryHelper.retryWait(retryCount); + } + } + } + } + + /** + * Performs the {@link #scan(String, String, int, Set, Vector)} operation for all fields. + * @param table The name of the table. + * @param startkey The record key of the first record to read. + * @param recordcount The number of records to read. + * @param result A Vector of HashMaps, where each HashMap is a set field/value pairs for one record. + */ + private Status scanAllFields(final String table, final String startkey, final int recordcount, + final Vector> result) { + + final List> data = new ArrayList>(recordcount); + final String query = "SELECT record_id FROM " + keyspaceName + + " WHERE record_id >= \"$1\" ORDER BY record_id LIMIT $2"; + QueryOptions scanQueryOptions = QueryOptions.queryOptions(); + + if (maxParallelism > 0) { + scanQueryOptions.maxParallelism(maxParallelism); + } + + cluster.reactive().query(query, + scanQueryOptions + .pipelineBatch(128) + .pipelineCap(1024) + .scanCap(1024) + .adhoc(adhoc) + .readonly(true) + .parameters(JsonArray.from(numericId(startkey), recordcount))) + .flatMapMany(ReactiveQueryResult::rowsAsObject) + .onErrorResume(e -> { + if(debug) { + System.err.println("Start Key: " + startkey + " Count: " + + recordcount + " Error:" + e.getClass() + " Info: " + e.getMessage()); + } + return Mono.empty(); + }) + .map(row -> { + HashMap tuple = new HashMap<>(); + tuple.put("record_id", new StringByteIterator(row.getString("record_id"))); + return tuple; + }) + .toStream() + .forEach(data::add); + + result.addAll(data); + return Status.OK; + } + /** + * Helper function to convert the key to a numeric value. + * @param key the key text + * @return a string with non-numeric characters removed + */ + private static String numericId(final String key) { + return key.replaceAll("[^\\d.]", ""); + } + /** + * Performs the {@link #scan(String, String, int, Set, Vector)} operation only for a subset of the fields. + * @param table The name of the table + * @param startkey The record key of the first record to read. + * @param recordcount The number of records to read + * @param fields The list of fields to read, or null for all of them + * @param result A Vector of HashMaps, where each HashMap is a set field/value pairs for one record + * @return The result of the operation. + */ + + private Status scanSpecificFields(final String table, final String startkey, final int recordcount, + final Set fields, final Vector> result) { + final Collection collection = bucket.defaultCollection(); + + final List> data = new ArrayList>(recordcount); + final String query = "SELECT RAW meta().id FROM " + keyspaceName + + " WHERE record_id >= $1 ORDER BY record_id LIMIT $2"; + final ReactiveCollection reactiveCollection = collection.reactive(); + QueryOptions scanQueryOptions = QueryOptions.queryOptions(); + + if (maxParallelism > 0) { + scanQueryOptions.maxParallelism(maxParallelism); + } + + reactiveCluster.query(query, + scanQueryOptions + .adhoc(adhoc) + .parameters(JsonArray.from(numericId(startkey), recordcount))) + .flatMapMany(res -> { + return res.rowsAs(String.class); + }) + .flatMap(id -> { + return reactiveCollection + .get(id, GetOptions.getOptions().transcoder(RawJsonTranscoder.INSTANCE)); + }) + .map(getResult -> { + HashMap tuple = new HashMap<>(); + decodeStringSource(getResult.contentAs(String.class), fields, tuple); + return tuple; + }) + .toStream() + .forEach(data::add); + + result.addAll(data); + return Status.OK; + } + /** + * Get string values from fields. + * @param source JSON source data. + * @param fields Fields to return. + * @param dest Map of Strings where each value is a requested field. + */ + private void decodeStringSource(final String source, final Set fields, + final Map dest) { + try { + JsonNode json = JacksonTransformers.MAPPER.readTree(source); + boolean checkFields = fields != null && !fields.isEmpty(); + for (Iterator> jsonFields = json.fields(); jsonFields.hasNext();) { + Map.Entry jsonField = jsonFields.next(); + String name = jsonField.getKey(); + if (checkFields && !fields.contains(name)) { + continue; + } + JsonNode jsonValue = jsonField.getValue(); + if (jsonValue != null && !jsonValue.isNull()) { + dest.put(name, new StringByteIterator(jsonValue.asText())); + } + } + } catch (Exception e) { + if(debug) { + System.err.println("Could not decode JSON response from scanSpecificFields"); + } + } + } + /** + * Update record. + * @param table The name of the table. + * @param key The record key of the record to write. + * @param values A HashMap of field/value pairs to update in the record. + */ + @Override + public Status update(final String table, final String key, final Map values) { + int retryCount = 0; + while (true) { + try { + Collection collection = collectionEnabled ? + bucket.scope(this.scopeName).collection(this.collectionName) : bucket.defaultCollection(); + values.put("record_id", new StringByteIterator(String.valueOf(primaryKeySeq.incrementAndGet()))); + if (useDurabilityLevels) { + collection.replace(formatId(table, key), + Couchbase3QueryHelper.encode(values), replaceOptions().durability(durabilityLevel)); + } else { + collection.replace(formatId(table, key), + Couchbase3QueryHelper.encode(values), replaceOptions().durability(persistTo, replicateTo)); + } + return Status.OK; + } catch (Throwable t) { + if (retryCount == numRetries) { + addToErrors(t); + if(debug) { + System.err.println("update failed with exception"); + t.printStackTrace(System.err); + } + return Status.ERROR; + } else { + retryCount++; + Couchbase3QueryHelper.retryWait(retryCount); + } + } + } + } + + /** + * Remove a record. + * @param table The name of the table. + * @param key The record key of the record to delete. + */ + @Override + public Status delete(final String table, final String key) { + int retryCount = 0; + while (true) { + try { + Collection collection = collectionEnabled ? + bucket.scope(this.scopeName).collection(this.collectionName) : bucket.defaultCollection(); + if (useDurabilityLevels) { + collection.remove(formatId(table, key), removeOptions().durability(durabilityLevel)); + } else { + collection.remove(formatId(table, key), removeOptions().durability(persistTo, replicateTo)); + } + return Status.OK; + } catch (DocumentNotFoundException dnf) { + return Status.NOT_FOUND; + } catch (Throwable t) { + if (retryCount == numRetries) { + addToErrors(t); + if(debug) { + System.err.println("delete failed with exception"); + t.printStackTrace(System.err); + } + return Status.ERROR; + } else { + retryCount++; + Couchbase3QueryHelper.retryWait(retryCount); + } + } + } + } + @Override + public Status findOne(String table, List filters, Set fields, + Map result) { + if(filters == null || filters.size() == 0) { + throw new NullPointerException(); + } + if(fields != null) { + throw new UnsupportedOperationException("cannot read results by field"); + } + + String query = Couchbase3QueryBuilder.buildFindOnePlaceholderQuery(keyspaceName, filters); + JsonArray params = JsonArray.create(); + Couchbase3QueryBuilder.bindFindOneQuery(params, filters); + if(debug) { + System.err.println("sending query and params:\n\t" + query + "\n\t" + params.toString()); + } + int retryCount = 0; + while (true) { + try { + QueryOptions options = QueryOptions.queryOptions(); + QueryResult qResult = cluster.query(query, options + .adhoc(adhoc) + .readonly(true) + .parameters(params) + .maxParallelism(maxParallelism) + // .metrics(true) + ); + if(qResult.metaData().status() != QueryStatus.SUCCESS) { + if(debug) { + System.err.println("unexpected query status: " + qResult.metaData().status()); + } + return Status.UNEXPECTED_STATE; + } + // no other way found to get the amount of results + List returned = qResult.rowsAsObject(); + if(returned.size() == 0) { + return Status.NOT_FOUND; + } + if(returned.size() > 1) { + return Status.UNEXPECTED_STATE; + } + Couchbase3QueryHelper.extractTypedFields(returned.get(0), fields, result); + return Status.OK; + } catch (DocumentNotFoundException dnf) { + return Status.NOT_FOUND; + } catch (Throwable t) { + if (retryCount == numRetries) { + addToErrors(t); + if(debug) { + System.err.println("updateOne failed with exception"); + t.printStackTrace(System.err); + } + return Status.ERROR; + } else { + retryCount++; + Couchbase3QueryHelper.retryWait(retryCount); + } + } + } + } + @Override + public Status updateOne(String table, List filters, List fields) { + if(filters == null || filters.size() == 0) { + throw new NullPointerException(); + } + if(fields == null || fields.size() == 0) { + throw new NullPointerException(); + } + String query = Couchbase3QueryBuilder.buildUpdateOnePlaceholderQuery(keyspaceName, filters, fields); + JsonArray params = JsonArray.create(); + Couchbase3QueryBuilder.bindUpdateOneQuery(params, fields, filters); + if(debug) { + System.err.println("sending query and params:\n\t" + query + "\n\t" + params.toString()); + } + int retryCount = 0; + while (true) { + try { + QueryOptions options = QueryOptions.queryOptions(); + // QueryResult qResult = bucket.defaultScope().query(query, + QueryResult qResult = cluster.query(query, + options.adhoc(adhoc) + .parameters(params) + .readonly(false) + .maxParallelism(maxParallelism) + // .asTransaction(SingleQueryTransactionOptions.singleQueryTransactionOptions().durabilityLevel(DurabilityLevel.NONE)) + // .metrics(true) + ); + if(qResult.metaData().status() != QueryStatus.SUCCESS) { + if(debug) { + System.err.println("unexpected query status: " + qResult.metaData().status()); + } + return Status.UNEXPECTED_STATE; + } + List returned = qResult.rowsAsObject(); + // System.err.println("returnd: " + returned.size()); + if(returned.size() == 0) { + return Status.NOT_FOUND; + } + if(returned.size() > 1) { + return Status.UNEXPECTED_STATE; + } + // extractTypedFields(returned.get(0), fields, result); + return Status.OK; + } catch (DocumentNotFoundException dnf) { + return Status.NOT_FOUND; + } catch (Throwable t) { + if (retryCount == numRetries) { + addToErrors(t); + if(debug) { + System.err.println("updateOne failed with exception"); + t.printStackTrace(System.err); + } + return Status.ERROR; + } else { + retryCount++; + Couchbase3QueryHelper.retryWait(retryCount); + } + } + } + } +} \ No newline at end of file diff --git a/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3IndexHelper.java b/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3IndexHelper.java new file mode 100644 index 0000000..1a49e4a --- /dev/null +++ b/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3IndexHelper.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.db.couchbase3; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions; +import com.couchbase.client.java.manager.query.CreateQueryIndexOptions; + +final class Couchbase3IndexHelper { + + static List getIndexList(Properties props) { + String indexeslist = props.getProperty(Couchbase3Client.INDEX_LIST_PROPERTY); + if(indexeslist == null) { + return Collections.emptyList(); + } + JsonArray barray = JsonArray.fromJson(indexeslist); + List llist = new ArrayList<>(); + for(int i = 0; i < barray.size(); i++) { + // Object o = barray.get(i); + // System.err.println("JSON ARRAY: " + i + " --- " + o + "---" + o.getClass()); + llist.add(barray.getObject(i)); + } + // List> list = (List>) (Object) barray.toList(); + // System.err.println("JSON LIST: " + list.toString()); + return llist; + } + + static void setIndexes(Properties props, List indexes) { + if(indexes.size() == 0) { + return; + } + // final String table = props.getProperty(CoreWorkload.TABLENAME_PROPERTY, CoreWorkload.TABLENAME_PROPERTY_DEFAULT); + for(JsonObject jo : indexes) { + System.err.println("get my index: " + jo); + if(jo.getBoolean("isPrimary") == Boolean.TRUE) { + CreatePrimaryQueryIndexOptions options = CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions(); + options.deferred(false); + options.ignoreIfExists(true); + Couchbase3Client.cluster.queryIndexes().createPrimaryIndex(Couchbase3Client.bucketName, options); + } else { + final String indexName = jo.getString("name"); + final JsonArray fieldArray = jo.getArray("fields"); + final String[] fields = new String[fieldArray.size()]; + for(int i = 0 ; i < fieldArray.size(); i++) { + fields[i] = fieldArray.getString(i); + } + CreateQueryIndexOptions options = CreateQueryIndexOptions.createQueryIndexOptions(); + options.deferred(false); + options.ignoreIfExists(true); + Couchbase3Client.cluster.queryIndexes().createIndex(Couchbase3Client.bucketName, indexName, Arrays.asList(fields), options); + } + } + System.err.println("created indexes"); + } + + private Couchbase3IndexHelper() { + + } +} diff --git a/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3QueryBuilder.java b/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3QueryBuilder.java new file mode 100644 index 0000000..00720ba --- /dev/null +++ b/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3QueryBuilder.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2019 Yahoo! Inc. All rights reserved. + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.db.couchbase3; + +import java.util.ArrayList; +import java.util.List; + +import com.couchbase.client.java.json.JsonArray; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +final class Couchbase3QueryBuilder { + + static void bindUpdateOneQuery(JsonArray params, List fields, List filters) { + bindSetPortion(params, fields); + FilterBuilder.bindFilters(params, filters); + } + + static String buildUpdateOnePlaceholderQuery(String keyspaceName, List filters, + List fields) { + String setPortion = buildPlaceholderTypedSetPortion(fields); + String filterPortion = FilterBuilder.buildConcatenatedPlaceholderFilter(filters); + String filter = " WHERE " + filterPortion + " LIMIT 1"; + String query = "UPDATE " + keyspaceName + " AS d SET " + + setPortion + " " + filter + " RETURNING d"; + return query; + } + + static String buildFindOnePlaceholderQuery(String keyspaceName, List filters) { + String filterPortion = FilterBuilder.buildConcatenatedPlaceholderFilter(filters); + String query = "SELECT * FROM " + keyspaceName + " WHERE " + filterPortion + " LIMIT 1"; + return query; + } + + static void bindFindOneQuery(JsonArray params, List filters) { + FilterBuilder.bindFilters(params, filters); + } + + private static String buildPlaceholderTypedSetPortion(List fields) { + return innerBuildTypedSetPortion(fields, true); + } + + private static String buildTypedSetPortion(List fields) { + return innerBuildTypedSetPortion(fields, false); + } + + private static String innerBuildTypedSetPortion(List fields, boolean placeholder) { + List parts = new ArrayList<>(fields.size()); + for(DatabaseField field : fields){ + buildTypedSetString("", field, parts, placeholder); + } + return String.join(", ", parts); + } + + private static void bindSetPortion(JsonArray params, List fields) { + for(DatabaseField field : fields){ + DataWrapper wrapper = field.getContent(); + if(wrapper.isNested()) { + List innerFields = wrapper.asNested(); + bindSetPortion(params, innerFields); + } else if(wrapper.isTerminal()) { + if(wrapper.isInteger()) { + params.add(wrapper.asInteger()); + } else if (wrapper.isLong()) { + params.add(wrapper.asLong()); + } else if(wrapper.isString()) { + params.add(wrapper.asString()); + } else { + params.add(new String(wrapper.asIterator().toArray())); + } + } else if(wrapper.isArray()) { + throw new IllegalArgumentException("setting arrays or array content is currently not supported"); + } else { + throw new IllegalStateException("neither terminal, nor array, nor nested"); + } + } + } + + private static void buildTypedSetString(String prefix, DatabaseField field, + List parts, boolean placeholder) { + DataWrapper wrapper = field.getContent(); + if(wrapper.isTerminal()) { + String value = ""; + if(wrapper.isInteger() || wrapper.isLong()) { + value = placeholder ? " ? " : wrapper.asString(); + } else if(wrapper.isString()) { + value = placeholder ? " ? " : ("\"" + wrapper.asString() + "\""); + } else { + // value = "\"" + encoder.encodeToString(wrapper.asIterator().toArray()) + "\""; + value = placeholder ? " ? " : "\"" + new String(wrapper.asIterator().toArray()) + "\""; + } + parts.add(prefix + field.getFieldname() + " = " + value); + } else if(wrapper.isNested()) { + String fieldPrefix = prefix + field.getFieldname() + "."; + List innerFields = wrapper.asNested(); + for(DatabaseField iF : innerFields) { + buildTypedSetString(fieldPrefix, iF, parts, placeholder); + } + } else if(wrapper.isArray()) { + throw new IllegalArgumentException("setting arrays or array content is currently not supported"); + } else { + throw new IllegalStateException("neither terminal, nor array, nor nested"); + } + } + private Couchbase3QueryBuilder() { + + } +} diff --git a/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3QueryHelper.java b/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3QueryHelper.java new file mode 100644 index 0000000..f88a361 --- /dev/null +++ b/couchbase3/src/main/java/site/ycsb/db/couchbase3/Couchbase3QueryHelper.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.db.couchbase3; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; + +import site.ycsb.ByteArrayByteIterator; +import site.ycsb.ByteIterator; +import site.ycsb.StringByteIterator; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +public final class Couchbase3QueryHelper { + + /** + * Helper function to wait before a retry. + */ + static void retryWait(int count) { + try { + Thread.sleep(count * 200L); + } catch(InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + static void extractTypedFields(final JsonObject content, + Set fields, final Map result) { + if (fields == null || fields.isEmpty()) { + fields = content.getNames(); + } + // a very sloppy way to transform the data + for (String field : fields) { + Object o = content.get(field); + if(o instanceof String) { + result.put(field, new StringByteIterator((String) o)); + } else if(o instanceof Number) { + result.put(field, new StringByteIterator(((Number) o).toString())); + } else if(o instanceof JsonArray) { + result.put(field, new ByteArrayByteIterator(((JsonArray) o).toBytes())); + } else if(o instanceof JsonObject) { + result.put(field, new ByteArrayByteIterator(((JsonObject) o).toBytes())); + } + } + } + static void extractFields(final JsonObject content, Set fields, + final Map result) { + if (fields == null || fields.isEmpty()) { + fields = content.getNames(); + } + + for (String field : fields) { + result.put(field, new StringByteIterator(content.getString(field))); + } + } + + /** + * Helper method to turn the passed in iterator values into a map we can encode to json. + * + * @param values the values to encode. + * @return the map of encoded values. + */ + static Map encodeWithTypes(final List values) { + Map toInsert = new HashMap<>(values.size()); + for (DatabaseField field : values) { + fillDocument(field, toInsert); + } + return toInsert; + } + + /** + * Helper method to turn the passed in iterator values into a map we can encode to json. + * + * @param values the values to encode. + * @return the map of encoded values. + */ + static Map encode(final Map values) { + Map result = new HashMap<>(values.size()); + for (Map.Entry value : values.entrySet()) { + result.put(value.getKey(), value.getValue().toString()); + } + return result; + } + + static void fillDocument(DatabaseField field, Map toInsert) { + DataWrapper wrapper = field.getContent(); + Object content = null; + if(wrapper.isTerminal() || wrapper.isArray()) { + // this WILL BREAK if content is a nested + // document within the array + content = wrapper.asObject(); + } else if(wrapper.isNested()) { + Map inner = new HashMap<>(); + List innerFields = wrapper.asNested(); + for(DatabaseField iF : innerFields) { + fillDocument(iF, inner); + } + content = inner; + } else { + throw new IllegalStateException("neither terminal, nor array, nor nested"); + } + toInsert.put( + field.getFieldname(), + content + ); + } + + private Couchbase3QueryHelper(){ + + } +} \ No newline at end of file diff --git a/couchbase3/src/main/java/site/ycsb/db/couchbase3/FilterBuilder.java b/couchbase3/src/main/java/site/ycsb/db/couchbase3/FilterBuilder.java new file mode 100644 index 0000000..cb3696e --- /dev/null +++ b/couchbase3/src/main/java/site/ycsb/db/couchbase3/FilterBuilder.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2023-204 benchANT GmbH. 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 site.ycsb.db.couchbase3; + +import java.util.ArrayList; +import java.util.List; + +import com.couchbase.client.java.json.JsonArray; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.ComparisonOperator; + +public final class FilterBuilder { + + static void bindFilters(JsonArray params, List filters) { + for(Comparison c : filters) { + Comparison d = c; + while(d.isSimpleNesting()) { + d = d.getSimpleNesting(); + } + if(d.comparesStrings()) { + params.add(d.getOperandAsString()); + } else if(d.comparesInts()) { + params.add(d.getOperandAsInt()); + } else { + throw new IllegalStateException(); + } + } + } + + static String buildConcatenatedPlaceholderFilter(List filters) { + return innerBuildConcatenatedFilter(filters, true); + } + + static String buildConcatenatedFilter(List filters) { + return innerBuildConcatenatedFilter(filters, false); + } + + private static String innerBuildConcatenatedFilter(List filters, boolean placeholder) { + // Bson[] bFilters = new Bson[filters.size()]; Arrays.asList(bFilters); + List lFilters = new ArrayList<>(filters.size()); + for(Comparison c : filters) { + Comparison d = c; + String fieldName = d.getFieldname(); + while(d.isSimpleNesting()) { + d = d.getSimpleNesting(); + fieldName = fieldName + "." + d.getFieldname(); + } + if(d.comparesStrings()) { + lFilters.add( + FilterBuilder.buildStringFilter( + fieldName, + d.getOperator(), + placeholder ? null : d.getOperandAsString() + )); + } else if(d.comparesInts()) { + lFilters.add( + FilterBuilder.buildIntFilter( + fieldName, + d.getOperator(), + placeholder ? null : d.getOperandAsInt() + )); + } else { + throw new IllegalStateException(); + } + } + return and(lFilters); + } + + public static String buildStringFilter(String fieldName, ComparisonOperator op, String value) { + String operand = value == null ? " ? " : " \"" + value + "\" "; + switch (op) { + case STRING_EQUAL: + return fieldName + " LIKE " + operand; + default: + throw new IllegalArgumentException("no string operator"); + } + } + + public static String buildIntFilter(String fieldName, ComparisonOperator op, Integer value) { + String operand = value == null ? " ? " : value.toString(); + switch (op) { + case INT_LTE: + return fieldName + " <= " + operand; + default: + throw new IllegalArgumentException("no int operator"); + } + } + + public static String and(List args){ + String result = args.get(0); + for(int i = 1; i < args.size(); i++) { + result = result + " AND " + args.get(i); + } + return result; + } + private FilterBuilder() {} +} diff --git a/couchbase3/src/main/java/site/ycsb/db/couchbase3/package-info.java b/couchbase3/src/main/java/site/ycsb/db/couchbase3/package-info.java new file mode 100644 index 0000000..c19b004 --- /dev/null +++ b/couchbase3/src/main/java/site/ycsb/db/couchbase3/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2015 - 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. + */ + +/** + * The YCSB binding for Couchbase, new driver. + */ +package site.ycsb.db.couchbase3; + diff --git a/couchbase3/src/main/resources/simplelogger.properties b/couchbase3/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..9aacb8f --- /dev/null +++ b/couchbase3/src/main/resources/simplelogger.properties @@ -0,0 +1,34 @@ +# SLF4J's SimpleLogger configuration file +# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. + +# Default logging detail level for all instances of SimpleLogger. +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, defaults to "info". +org.slf4j.simpleLogger.defaultLogLevel=error + +# Logging detail level for a SimpleLogger instance named "xxxxx". +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, the default logging detail level is used. +#org.slf4j.simpleLogger.log.xxxxx= + +# Set to true if you want the current date and time to be included in output messages. +# Default is false, and will output the number of milliseconds elapsed since startup. +#org.slf4j.simpleLogger.showDateTime=false + +# The date and time format to be used in the output messages. +# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. +# If the format is not specified or is invalid, the default format is used. +# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. +#org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z + +# Set to true if you want to output the current thread name. +# Defaults to true. +#org.slf4j.simpleLogger.showThreadName=true + +# Set to true if you want the Logger instance name to be included in output messages. +# Defaults to true. +#org.slf4j.simpleLogger.showLogName=true + +# Set to true if you want the last component of the name to be included in output messages. +# Defaults to false. +#org.slf4j.simpleLogger.showShortLogName=false \ No newline at end of file diff --git a/distribution/pom.xml b/distribution/pom.xml new file mode 100644 index 0000000..1f02a77 --- /dev/null +++ b/distribution/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + + site.ycsb + root + 0.18.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. + + + + site.ycsb + core + ${project.version} + + + site.ycsb + couchbase3-binding + ${project.version} + + + site.ycsb + dynamodb-binding + ${project.version} + + + site.ycsb + jdbc-binding + ${project.version} + + + site.ycsb + mongodb-binding + ${project.version} + + + site.ycsb + scylla-binding + ${project.version} + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven.assembly.version} + + + src/main/assembly/distribution.xml + + false + posix + + + + package + + single + + + + + + + + + + ycsb-release + + true + + + + + + diff --git a/distribution/src/main/assembly/distribution.xml b/distribution/src/main/assembly/distribution.xml new file mode 100644 index 0000000..e527f17 --- /dev/null +++ b/distribution/src/main/assembly/distribution.xml @@ -0,0 +1,106 @@ + + + + package + + tar.gz + + true + + + .. + . + 0644 + + README + LICENSE.txt + NOTICE.txt + + + + ../bin + bin + 0755 + + ycsb* + + + + ../bin + bin + 0644 + + bindings.properties + + + + ../workloads + workloads + 0644 + + + + + lib + + site.ycsb:core + + runtime + false + false + true + true + + + + + true + true + + site.ycsb:core + site.ycsb:binding-parent + site.ycsb:datastore-specific-descriptor + site.ycsb:ycsb + site.ycsb:root + + + + + + README.md + + + + conf + src/main/conf + + + lib + target/dependency + + + + + false + ${module.artifactId}/lib + false + + + + diff --git a/doc/coreproperties.html b/doc/coreproperties.html new file mode 100644 index 0000000..40a9d6a --- /dev/null +++ b/doc/coreproperties.html @@ -0,0 +1,48 @@ + + + + +YCSB - Core workload package properties + + +

Yahoo! Cloud Serving Benchmark

+

Version 0.1.2

+
+Home - Core workloads - Tips and FAQ +
+

Core workload package properties

+The property files used with the core workload generator can specify values for the following properties:

+

    +
  • fieldcount: the number of fields in a record (default: 10) +
  • fieldlength: the size of each field (default: 100) +
  • readallfields: should reads read all fields (true) or just one (false) (default: true) +
  • readproportion: what proportion of operations should be reads (default: 0.95) +
  • updateproportion: what proportion of operations should be updates (default: 0.05) +
  • insertproportion: what proportion of operations should be inserts (default: 0) +
  • scanproportion: what proportion of operations should be scans (default: 0) +
  • readmodifywriteproportion: what proportion of operations should be read a record, modify it, write it back (default: 0) +
  • requestdistribution: what distribution should be used to select the records to operate on - uniform, zipfian or latest (default: uniform) +
  • maxscanlength: for scans, what is the maximum number of records to scan (default: 1000) +
  • scanlengthdistribution: for scans, what distribution should be used to choose the number of records to scan, for each scan, between 1 and maxscanlength (default: uniform) +
  • insertorder: should records be inserted in order by key ("ordered"), or in hashed order ("hashed") (default: hashed) +
  • fieldnameprefix: string prefix for the field name (default: “field”) +
+
+YCSB - Yahoo! Research - Contact cooperb@yahoo-inc.com. + + diff --git a/doc/coreworkloads.html b/doc/coreworkloads.html new file mode 100644 index 0000000..e6f195a --- /dev/null +++ b/doc/coreworkloads.html @@ -0,0 +1,76 @@ + + + + +YCSB - Core workloads + + +

Yahoo! Cloud Serving Benchmark

+

Version 0.1.2

+
+Home - Core workloads - Tips and FAQ +
+

Core workloads

+YCSB includes a set of core workloads that define a basic benchmark for cloud systems. Of course, you can define your own workloads, as described here. However, +the core workloads are a useful first step, and obtaining these benchmark numbers for a variety of different systems would allow you to understand the performance +tradeoffs of different systems. +

+The core workloads consist of six different workloads: +

+Workload A: Update heavy workload +

+This workload has a mix of 50/50 reads and writes. An application example is a session store recording recent actions. +

+Workload B: Read mostly workload +

+This workload has a 95/5 reads/write mix. Application example: photo tagging; add a tag is an update, but most operations are to read tags. +

+Workload C: Read only +

+This workload is 100% read. Application example: user profile cache, where profiles are constructed elsewhere (e.g., Hadoop). +

+Workload D: Read latest workload +

+In this workload, new records are inserted, and the most recently inserted records are the most popular. Application example: user status updates; people want to read the latest. +

+Workload E: Short ranges +

+In this workload, short ranges of records are queried, instead of individual records. Application example: threaded conversations, where each scan is for the posts in a given thread (assumed to be clustered by thread id). +

+Workload F: Read-modify-write +

+In this workload, the client will read a record, modify it, and write back the changes. Application example: user database, where user records are read and modified by the user or to record user activity. + +


+

Running the workloads

+All six workloads have a data set which is similar. Workloads D and E insert records during the test run. Thus, to keep the database size consistent, we recommend the following sequence: +
    +
  1. Load the database, using workload A's parameter file (workloads/workloada) and the "-load" switch to the client. +
  2. Run workload A (using workloads/workloada and "-t") for a variety of throughputs. +
  3. Run workload B (using workloads/workloadb and "-t") for a variety of throughputs. +
  4. Run workload C (using workloads/workloadc and "-t") for a variety of throughputs. +
  5. Run workload F (using workloads/workloadf and "-t") for a variety of throughputs. +
  6. Run workload D (using workloads/workloadd and "-t") for a variety of throughputs. This workload inserts records, increasing the size of the database. +
  7. Delete the data in the database. +
  8. Reload the database, using workload E's parameter file (workloads/workloade) and the "-load switch to the client. +
  9. Run workload E (using workloads/workloadd and "-t") for a variety of throughputs. This workload inserts records, increasing the size of the database. +
+
+YCSB - Yahoo! Research - Contact cooperb@yahoo-inc.com. + + diff --git a/doc/dblayer.html b/doc/dblayer.html new file mode 100644 index 0000000..5944265 --- /dev/null +++ b/doc/dblayer.html @@ -0,0 +1,115 @@ + + + + +YCSB - DB Interface Layer + + +

Yahoo! Cloud Serving Benchmark

+

Version 0.1.2

+
+Home - Core workloads - Tips and FAQ +
+

Implementing a database interface layer - overview

+The database interface layer hides the details of the specific database you are benchmarking from the YCSB Client. This +allows the client to generate operations like "read record" or "update record" without having to understand +the specific API of your database. Thus, it is very easy to benchmark new database systems; once you have +created the database interface layer, the rest of the benchmark framework runs without having to change. +

+The database interface layer is a simple abstract class that provides read, insert, update, delete and scan operations for your +database. Implementing a database interface layer for your database means filling out the body of each of those methods. Once you +have compiled your layer, you can specify the name of your implemented class on the command line (or as a property) to the YCSB Client. +The YCSB Client will load your implementation dynamically when it starts. Thus, you do not need to recompile the YCSB Client itself +to add or change a database interface layer. +


+

Creating a new layer step-by-step

+

Step 1 - Extend site.ycsb.DB

+The base class of all database interface layer implementations is site.ycsb.DB. This is an abstract class, so you need to create a new +class which extends the DB class. Your class must have a public no-argument constructor, because the instances will be constructed inside a factory +which will use the no-argument constructor. +

+The YCSB Client framework will create one instance of your DB class per worker thread, but there might be multiple worker threads generating the workload, +so there might be multiple instances of your DB class created. + +

Step 2 - Implement init() if necessary

+You can perform any initialization of your DB object by implementing the following method +
 
+public void init() throws DBException
+
+to perform any initialization actions. The init() method will be called once per DB instance; so if there are multiple threads, each DB instance will have init() +called separately. +

+The init() method should be used to set up the connection to the database and do any other initialization. In particular, you can configure your database layer +using properties passed to the YCSB Client at runtime. In fact, the YCSB Client will pass to the DB interface layer +all of the +properties specified in all parameter files specified when the Client starts up. Thus, you can create new properties for configuring your DB interface layer, +set them in your parameter files (or on the command line), and +then retrieve them inside your implementation of the DB interface layer. +

+These properties will be passed to the DB instance after the constructor, so it is important to retrieve them only in the init() method and not the +constructor. You can get the set of properties using the +

+public Properties getProperties()
+
+method which is already implemented and inherited from the DB base class. + +

Step 3 - Implement the database query and update methods

+ +The methods that you need to implement are: + +
+  //Read a single record
+  public int read(String table, String key, Set fields, HashMap result);
+
+  //Perform a range scan
+  public int scan(String table, String startkey, int recordcount, Set fields, Vector> result);
+	
+  //Update a single record
+  public int update(String table, String key, HashMap values);
+
+  //Insert a single record
+  public int insert(String table, String key, HashMap values);
+
+  //Delete a single record
+  public int delete(String table, String key);
+
+In each case, the method takes a table name and record key. (In the case of scan, the record key is the first key in the range to scan.) For the +read methods (read() and scan()) the methods additionally take a set of fields to be read, and provide a structure (HashMap or Vector of HashMaps) to store +the returned data. For the write methods (insert() and update()) the methods take HashMap which maps field names to values. +

+The database should have the appropriate tables created before you run the benchmark. So you can assume in your implementation of the above methods +that the appropriate tables already exist, and just write code to read or write from the tables named in the "table" parameter. +

Step 4 - Compile your database interface layer

+Your code can be compiled separately from the compilation of the YCSB Client and framework. In particular, you can make changes to your DB class and +recompile without having to recompile the YCSB Client. +

Step 5 - Use it with the YCSB Client

+Make sure that the classes for your implementation (or a jar containing those classes) are available on your CLASSPATH, as well as any libraries/jar files used +by your implementation. Now, when you run the YCSB Client, specify the "-db" argument on the command line and provide the fully qualified classname of your +DB class. For example, to run workloada with your DB class: +
+%  java -cp build/ycsb.jar:yourjarpath site.ycsb.Client -t -db com.foo.YourDBClass -P workloads/workloada -P large.dat -s > transactions.dat
+
+ +You can also specify the DB interface layer using the DB property in your parameter file: +
+db=com.foo.YourDBClass
+
+
+YCSB - Yahoo! Research - Contact cooperb@yahoo-inc.com. + + diff --git a/doc/images/ycsb.jpg b/doc/images/ycsb.jpg new file mode 100644 index 0000000..793a689 Binary files /dev/null and b/doc/images/ycsb.jpg differ diff --git a/doc/images/ycsblogo-small.png b/doc/images/ycsblogo-small.png new file mode 100644 index 0000000..499eb41 Binary files /dev/null and b/doc/images/ycsblogo-small.png differ diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 0000000..e00f213 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,90 @@ + + + + +YCSB - Yahoo! Cloud Serving Benchmark + + +

Yahoo! Cloud Serving Benchmark

+

Version 0.1.2

+
+Home - Core workloads - Tips and FAQ +
+ +
+ +

Overview

+There are many new serving databases available, including: +
+It is difficult to decide which system is right for your application, partially because the features differ between +systems, and partially because there is not an easy way to compare the performance of one system versus another. +

+The goal of the YCSB project is to develop a framework and common set of workloads for evaluating the performance of +different "key-value" and "cloud" serving stores. The project comprises two things: +

    +
  • The YCSB Client, an extensible workload generator +
  • The Core workloads, a set of workload scenarios to be executed by the generator +
+Although the core workloads provide a well rounded picture of a system's performance, the Client is extensible so that +you can define new and different workloads to examine system aspects, or application scenarios, not adequately covered by +the core workload. Similarly, the Client is extensible to support benchmarking different databases. Although we include +sample code for benchmarking HBase and Cassandra, it is straightforward to write a new interface layer to benchmark +your favorite database. +

+A common use of the tool is to benchmark multiple systems and compare them. For example, you can install multiple systems +on the same hardward configuration, and run the same workloads against each system. Then you can plot the performance +of each system (for example, as latency versus throughput curves) to see when one system does better than another. +


+ +

Download YCSB

+YCSB is available +at
http://wiki.github.com/brianfrankcooper/YCSB/. +
+ +

Getting started

+Detailed instructions for using YCSB are available on the GitHub wiki: +
http://wiki.github.com/brianfrankcooper/YCSB/getting-started. +
+ +

Extending YCSB

+YCSB is designed to be extensible. It is easy to add a new database interface layer to support benchmarking a new database. It is also easy to define new workloads. +
+More details about the entire class structure of YCSB is available here: + +
+YCSB - Yahoo! Research - Contact cooperb@yahoo-inc.com. + + diff --git a/doc/parallelclients.html b/doc/parallelclients.html new file mode 100644 index 0000000..3de79ca --- /dev/null +++ b/doc/parallelclients.html @@ -0,0 +1,63 @@ + + + + +YCSB - Parallel clients + + +

Yahoo! Cloud Serving Benchmark

+

Version 0.1.2

+
+Home - Core workloads - Tips and FAQ +
+

Running multiple clients in parallel

+It is straightforward to run the transaction phase of the workload from multiple servers - just start up clients on different servers, each running the same workload. Each client will +produce performance statistics when it is done, and you'll have to aggregate these individual files into a single set of results. +

+In some cases it makes sense to load the database using multiple servers. In this case, you will want to partition the records to be loaded among the clients. Normally, YCSB just loads +all of the records (as defined by the recordcount property). However, if you want to partition the load you need to additionally specify two other properties for each client: +

    +
  • insertstart: The index of the record to start at. +
  • insertcount: The number of records to insert. +
+These properties can be specified in a property file or on the command line using the -p option. +

+For example, imagine you want to load 100 million records (so recordcount=100000000). Imagine you want to load with four clients. For the first client: +

+insertstart=0
+insertcount=25000000
+
+For the second client: +
+insertstart=25000000
+insertcount=25000000
+
+For the third client: +
+insertstart=50000000
+insertcount=25000000
+
+And for the fourth client: +
+insertstart=75000000
+insertcount=25000000
+
+
+YCSB - Yahoo! Research - Contact cooperb@yahoo-inc.com. + + diff --git a/doc/tipsfaq.html b/doc/tipsfaq.html new file mode 100644 index 0000000..3bd5a59 --- /dev/null +++ b/doc/tipsfaq.html @@ -0,0 +1,48 @@ + + + + +YCSB - Tips and FAQ + + +

Yahoo! Cloud Serving Benchmark

+

Version 0.1.2

+
+Home - Core workloads - Tips and FAQ +
+

Tips

+Tip 1 - Carefully adjust the number of threads +

+The number of threads determines how much workload you can generate against the database. Imagine that you are trying to run a test with 10,000 operations per second, +but you are only achieving 8,000 operations per second. Is this because the database can't keep up with the load? Not necessarily. Imagine that you are running with 100 +client threads (e.g. "-threads 100") and each operation is taking 12 milliseconds on average. Each thread will only be able to generate 83 operations per second, because each +thread operates sequentially. Over 100 threads, your client will only generate 8300 operations per second, even if the database can support more. Increasing the number of threads +ensures there are enough parallel clients hitting the database so that the database, not the client, is the bottleneck. +

+To calculate the number of threads needed, you should have some idea of the expected latency. For example, at 10,000 operations per second, we might expect the database +to have a latency of 10-30 milliseconds on average. So you to generate 10,000 operations per second, you will need (Ops per sec / (1000 / avg latency in ms) ), or (10000/(1000/30))=300 threads. +In fact, to be conservative, you might consider having 400 threads. Although this is a lot of threads, each thread will spend most of its time waiting for the database to respond, +so the context switching overhead will be low. +

+Experiment with increasing the number of threads, especially if you find you are not reaching your target throughput. Eventually, of course, you will saturate the database +and there will be no way to increase the number of threads to get more throughput (in fact, increasing the number of client threads may make things worse) but you need to have +enough threads to ensure it is the database, not the client, that is the bottleneck. +


+YCSB - Yahoo! Research - Contact cooperb@yahoo-inc.com. + + diff --git a/doc/workload.html b/doc/workload.html new file mode 100644 index 0000000..199839f --- /dev/null +++ b/doc/workload.html @@ -0,0 +1,146 @@ + + + + +YCSB - Implementing new workloads + + +

Yahoo! Cloud Serving Benchmark

+

Version 0.1.2

+
+Home - Core workloads - Tips and FAQ +
+

Implementing new workloads - overview

+A workload represents the load that a given application will put on the database system. For benchmarking purposes, we must define +workloads that are relatively simple compared to real applications, so that we can better reason about the benchmarking results +we get. However, a workload should be detailed enough so that once we measure the database's performance, we know what kinds of applications +might experience similar performance. +

+In the context of YCSB, a workload defines both a data set, which is a set of records to be loaded into the database, and a transaction set, +which are the set of read and write operations against the database. Creating the transactions requires understanding the structure of the records, which +is why both the data and the transactions must be defined in the workload. +

+For a complete benchmark, multiple important (but distinct) workloads might be grouped together into a workload package. The CoreWorkload +package included with the YCSB client is an example of such a collection of workloads. +

+Typically a workload consists of two files: +

    +
  • A java class which contains the code to create data records and generate transactions against them +
  • A parameter file which tunes the specifics of the workload +
+For example, a workload class file might generate some combination of read and update operations against the database. The parameter +file might specify whether the mix of reads and updates is 50/50, 80/20, etc. +

+There are two ways to create a new workload or package of workloads. +

+

Option 1: new parameter files

+

+The core workloads included with YCSB are defined by a set of parameter files (workloada, workloadb, etc.) You can create your own parameter file with new values +for the read/write mix, request distribution, etc. For example, the workloada file has the following contents: + +

+workload=site.ycsb.workloads.CoreWorkload
+
+readallfields=false
+
+readproportion=0.5
+updateproportion=0.5
+scanproportion=0
+insertproportion=0
+
+requestdistribution=zipfian
+
+ +Creating a new file that changes any of these values will produce a new workload with different characteristics. The set of properties that can be specified is here. +

+

Option 2: new java class

+

+The workload java class will be created by the YCSB Client at runtime, and will use an instance of the DB interface layer +to generate the actual operations against the database. Thus, the java class only needs to decide (based on settings in the parameter file) what records +to create for the data set, and what reads, updates etc. to generate for the transaction phase. The YCSB Client will take care of creating the workload java class, +passing it to a worker thread for executing, deciding how many records to create or how many operations to execute, and measuring the resulting +performance. +

+If the CoreWorkload (or some other existing package) does not have the ability to generate the workload you desire, you can create a new workload java class. +This is done using the following steps: +

Step 1. Extend site.ycsb.Workload

+The base class of all workload classes is site.ycsb.Workload. This is an abstract class, so you create a new workload that extends this base class. Your +class must have a public no-argument constructor, because the workload will be created in a factory using the no-argument constructor. The YCSB Client will +create one Workload object for each worker thread, so if you run the Client with multiple threads, multiple workload objects will be created. +

Step 2. Write code to initialize your workload class

+The parameter fill will be passed to the workload object after the constructor has been called, so if you are using any parameter properties, you must +use them to initialize your workload using either the init() or initThread() methods. +
    +
  • init() - called once for all workload instances. Used to initialize any objects shared by all threads. +
  • initThread() - called once per workload instance in the context of the worker thread. Used to initialize any objects specific to a single Workload instance +and single worker thread. +
+In either case, you can access the parameter properties using the Properties object passed in to both methods. These properties will include all properties defined +in any property file passed to the YCSB Client or defined on the client command line. +

Step 3. Write any cleanup code

+The cleanup() method is called once for all workload instances, after the workload has completed. +

Step 4. Define the records to be inserted

+The YCSB Client will call the doInsert() method once for each record to be inserted into the database. So you should implement this method +to create and insert a single record. The DB object you can use to perform the insert will be passed to the doInsert() method. +

Step 5. Define the transactions

+The YCSB Client will call the doTransaction() method once for every transaction that is to be executed. So you should implement this method to execute +a single transaction, using the DB object passed in to access the database. Your implementation of this method can choose between different types of +transactions, and can make multiple calls to the DB interface layer. However, each invocation of the method should be a logical transaction. In particular, when you run the client, +you'll specify the number of operations to execute; if you request 1000 operations then doTransaction() will be executed 1000 times. +

+Note that you do not have to do any throttling of your transactions (or record insertions) to achieve the target throughput. The YCSB Client will do the throttling +for you. +

+Note also that it is allowable to insert records inside the doTransaction() method. You might do this if you wish the database to grow during the workload. In this case, +the initial dataset will be constructed using calls to the doInsert() method, while additional records would be inserted using calls to the doTransaction() method. +

Step 6 - Measure latency, if necessary

+The YCSB client will automatically measure the latency and throughput of database operations, even for workloads that you define. However, the client will only measure +the latency of individual calls to the database, not of more complex transactions. Consider for example a workload that reads a record, modifies it, and writes +the changes back to the database. The YCSB client will automatically measure the latency of the read operation to the database; and separately will automatically measure the +latency of the update operation. However, if you would like to measure the latency of the entire read-modify-write transaction, you will need to add an additional timing step to your +code. +

+Measurements are gathered using the Measurements.measure() call. There is a singleton instance of Measurements, which can be obtained using the +Measurements.getMeasurements() static method. For each metric you are measuring, you need to assign a string tag; this tag will label the resulting +average, min, max, histogram etc. measurements output by the tool at the end of the workload. For example, consider the following code: + +

+long st=System.currentTimeMillis();
+db.read(TABLENAME,keyname,fields,new HashMap());
+db.update(TABLENAME,keyname,values);
+long en=System.currentTimeMillis();
+Measurements.getMeasurements().measure("READ-MODIFY-WRITE", (int)(en-st));
+
+ +In this code, the calls to System.currentTimeMillis() are used to time the read and write transaction. Then, the call to measure() reports the latency to the +measurement component. +

+Using this pattern, your custom measurements will be gathered and aggregated using the same mechanism that is used to gather measurements for individual READ, UPDATE etc. operations. + +

Step 7 - Use it with the YCSB Client

+Make sure that the classes for your implementation (or a jar containing those classes) are available on your CLASSPATH, as well as any libraries/jar files used +by your implementation. Now, when you run the YCSB Client, specify the "workload" property to provide the fully qualified classname of your +DB class. For example: + +
+workload=com.foo.YourWorkloadClass
+
+
+YCSB - Yahoo! Research - Contact cooperb@yahoo-inc.com. + + diff --git a/dynamodb/README.md b/dynamodb/README.md new file mode 100644 index 0000000..61455ed --- /dev/null +++ b/dynamodb/README.md @@ -0,0 +1,76 @@ + + +# DynamoDB Binding + +http://aws.amazon.com/documentation/dynamodb/ + +## Configure + + YCSB_HOME - YCSB home directory + DYNAMODB_HOME - Amazon DynamoDB package files + +Please refer to https://github.com/brianfrankcooper/YCSB/wiki/Using-the-Database-Libraries +for more information on setup. + +# Benchmark + + $YCSB_HOME/bin/ycsb load dynamodb -P workloads/workloada -P dynamodb.properties + $YCSB_HOME/bin/ycsb run dynamodb -P workloads/workloada -P dynamodb.properties + +# Properties + + $DYNAMODB_HOME/conf/dynamodb.properties + $DYNAMODB_HOME/conf/AWSCredentials.properties + +# FAQs +* Why is the recommended workload distribution set to 'uniform'? + This is to conform with the best practices for using DynamoDB - uniform, +evenly distributed workload is the recommended pattern for scaling and +getting predictable performance out of DynamoDB + +For more information refer to +http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/BestPractices.html + +* How does workload size affect provisioned throughput? + The default payload size requires double the provisioned throughput to execute +the workload. This translates to double the provisioned throughput cost for testing. +The default item size in YCSB are 1000 bytes plus metadata overhead, which makes the +item exceed 1024 bytes. DynamoDB charges one capacity unit per 1024 bytes for read +or writes. An item that is greater than 1024 bytes but less than or equal to 2048 bytes +would cost 2 capacity units. With the change in payload size, each request would cost +1 capacity unit as opposed to 2, saving the cost of running the benchmark. + +For more information refer to +http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/WorkingWithDDTables.html + +* How do you know if DynamoDB throttling is affecting benchmarking? + Monitor CloudWatch for ThrottledRequests and if ThrottledRequests is greater +than zero, either increase the DynamoDB table provisioned throughput or reduce +YCSB throughput by reducing YCSB target throughput, adjusting the number of YCSB +client threads, or combination of both. + +For more information please refer to +https://github.com/brianfrankcooper/YCSB/blob/master/doc/tipsfaq.html + +When requests are throttled, latency measurements by YCSB can increase. + +Please refer to http://aws.amazon.com/dynamodb/faqs/ for more information. + +Please refer to Amazon DynamoDB docs here: +http://aws.amazon.com/documentation/dynamodb/ diff --git a/dynamodb/conf/AWSCredentials.properties b/dynamodb/conf/AWSCredentials.properties new file mode 100644 index 0000000..e337b39 --- /dev/null +++ b/dynamodb/conf/AWSCredentials.properties @@ -0,0 +1,19 @@ +# Copyright (c) 2012 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. + +# Fill in your AWS Access Key ID and Secret Access Key +# http://aws.amazon.com/security-credentials +#accessKey = +#secretKey = diff --git a/dynamodb/conf/dynamodb.properties b/dynamodb/conf/dynamodb.properties new file mode 100644 index 0000000..666f33d --- /dev/null +++ b/dynamodb/conf/dynamodb.properties @@ -0,0 +1,100 @@ +# Copyright (c) 2012 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. + +# +# Sample property file for Amazon DynamoDB database client + +## Mandatory parameters + +# AWS credentials associated with your aws account. +#dynamodb.awsCredentialsFile = + +# Primarykey of table 'usertable' +#dynamodb.primaryKey = + +# If you set dynamodb.primaryKeyType to HASH_AND_RANGE, you must specify the +# hash key name of your primary key here. (see documentation below for details) +#dynamodb.hashKeyName = + +## Optional parameters + +# The property "primaryKeyType" below specifies the type of primary key +# you have setup for the test table. There are two choices: +# - HASH (default) +# - HASH_AND_RANGE +# +# When testing the DB in HASH mode (which is the default), your table's +# primary key must be of the "HASH" key type, and the name of the primary key +# is specified via the dynamodb.primaryKey property. In this mode, all +# keys from YCSB are hashed across multiple hash partitions and +# performance of individual operations are good. However, query across +# multiple items is eventually consistent in this mode and relies on the +# global secondary index. +# +# +# When testing the DB in HASH_AND_RANGE mode, your table's primary key must be +# of the "HASH_AND_RANGE" key type. You need to specify the name of the +# hash key via the "dynamodb.hashKeyName" property and you also need to +# specify the name of the range key via the "dynamodb.primaryKey" property. +# In this mode, keys supplied by YCSB will be used as the range part of +# the primary key and the hash part of the primary key will have a fixed value. +# Optionally you can designate the value used in the hash part of the primary +# key via the dynamodb.hashKeyValue. +# +# The purpose of the HASH_AND_RANGE mode is to benchmark the performance +# characteristics of a single logical hash partition. This is useful because +# so far the only practical way to do strongly consistent query is to do it +# in a single hash partition (Whole table scan can be consistent but it becomes +# less practical when the table is really large). Therefore, for users who +# really want to have strongly consistent query, it's important for them to +# know the performance capabilities of a single logical hash partition so +# they can plan their application accordingly. + +#dynamodb.primaryKeyType = HASH + +#Optionally you can specify a value for the hash part of the primary key +#when testing in HASH_AND_RANG mode. +#dynamodb.hashKeyValue = + +# AWS Region code to connect to: +# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions +# Set this parameter, unless you are using the default value ('us-east-1). +#dynamodb.region = us-east-1 + +# Endpoint to connect to. If not set, the endpoint will be set automatically +# based on the region and for HTTP connections. When using a non-standard +# endpoint (such as a proxy), the region parameter is still required to generate +# the proper message's signature. +#dynamodb.endpoint = http://dynamodb.us-east-1.amazonaws.com + +# Strongly recommended to set to uniform.Refer FAQs in README +#requestdistribution = uniform + +# Enable/disable debug messages.Defaults to false +# "true" or "false" +#dynamodb.debug = false + +# Maximum number of concurrent connections +#dynamodb.connectMax = 50 + +# Read consistency.Consistent reads are expensive and consume twice +# as many resources as eventually consistent reads. Defaults to false. +# "true" or "false" +#dynamodb.consistentReads = false + +# Workload size has implications on provisioned read and write +# capacity units.Refer FAQs in README +#fieldcount = 10 +#fieldlength = 90 diff --git a/dynamodb/pom.xml b/dynamodb/pom.xml new file mode 100644 index 0000000..824b9c9 --- /dev/null +++ b/dynamodb/pom.xml @@ -0,0 +1,50 @@ + + + + + 4.0.0 + + site.ycsb + binding-parent + 0.18.0-SNAPSHOT + ../binding-parent + + + dynamodb-binding + DynamoDB DB Binding + + + + com.amazonaws + aws-java-sdk + 1.11.812 + + + log4j + log4j + 1.2.17 + + + site.ycsb + core + ${project.version} + provided + + + diff --git a/dynamodb/src/main/java/site/ycsb/db/DynamoDBClient.java b/dynamodb/src/main/java/site/ycsb/db/DynamoDBClient.java new file mode 100644 index 0000000..ef334e1 --- /dev/null +++ b/dynamodb/src/main/java/site/ycsb/db/DynamoDBClient.java @@ -0,0 +1,611 @@ +/* + * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2015-2016 YCSB Contributors. All Rights Reserved. + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +package site.ycsb.db; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.Vector; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.auth.PropertiesCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; +import com.amazonaws.services.dynamodbv2.document.Index; +import com.amazonaws.services.dynamodbv2.document.Item; +import com.amazonaws.services.dynamodbv2.document.QueryOutcome; +import com.amazonaws.services.dynamodbv2.document.Table; +import com.amazonaws.services.dynamodbv2.document.internal.IteratorSupport; +import com.amazonaws.services.dynamodbv2.document.DynamoDB; +import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.AttributeValue; +import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate; +import com.amazonaws.services.dynamodbv2.model.BatchWriteItemRequest; +import com.amazonaws.services.dynamodbv2.model.BatchWriteItemResult; +import com.amazonaws.services.dynamodbv2.model.CreateGlobalSecondaryIndexAction; +import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; +import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; +import com.amazonaws.services.dynamodbv2.model.GetItemRequest; +import com.amazonaws.services.dynamodbv2.model.GetItemResult; +import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndexDescription; +import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndexUpdate; +import com.amazonaws.services.dynamodbv2.model.IndexStatus; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.PutItemRequest; +import com.amazonaws.services.dynamodbv2.model.PutRequest; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import com.amazonaws.services.dynamodbv2.model.ScanRequest; +import com.amazonaws.services.dynamodbv2.model.ScanResult; +import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest; +import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest; +import com.amazonaws.services.dynamodbv2.model.UpdateTableResult; +import com.amazonaws.services.dynamodbv2.model.WriteRequest; + +import site.ycsb.ByteIterator; +import site.ycsb.DB; +import site.ycsb.DBException; +import site.ycsb.IndexableDB; +import site.ycsb.Status; +import site.ycsb.workloads.core.CoreConstants; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +/** + * DynamoDB client for YCSB. + */ + +public class DynamoDBClient extends DB implements IndexableDB { + static class IndexDescriptor { + String name; + String hashKeyAttribute; + int readCap; + int writeCap; + List sortsKeyAttributes = new ArrayList<>(); + @Override + public String toString() { + return "IndexDescriptor [name=" + name + ", hashKeyAttribute=" + hashKeyAttribute + ", readCap=" + readCap + + ", writeCap=" + writeCap + ", sortsKeyAttributes=" + sortsKeyAttributes + "]"; + } + } + /** + * Defines the primary key type used in this particular DB instance. + *

+ * By default, the primary key type is "HASH". Optionally, the user can + * choose to use hash_and_range key type. See documentation in the + * DynamoDB.Properties file for more details. + */ + private enum PrimaryKeyType { + HASH, + HASH_AND_RANGE + } + public static final String INDEX_LIST_PROPERTY = "dynamodb.indexlist"; + public static final String INDEX_READ_CAP_PROPERTY = "dynamodb.indexreadcap"; + public static final String INDEX_READ_CAP_DEFAULT = "5"; + public static final String INDEX_WRITE_CAP_PROPERTY = "dynamodb.indexwritecap"; + public static final String INDEX_WRITE_CAP_DEFAULT = "5"; + private AmazonDynamoDB dynamoDB; + String primaryKeyName; + PrimaryKeyType primaryKeyType = PrimaryKeyType.HASH; + + // If the user choose to use HASH_AND_RANGE as primary key type, then + // the following two variables become relevant. See documentation in the + // DynamoDB.Properties file for more details. + private String hashKeyValue; + private String hashKeyName; + + private boolean consistentRead = false; + private String region = "us-east-1"; + private String endpoint = null; + private int maxConnects = 50; + private int maxRetries = -1; + static final Logger LOGGER = Logger.getLogger(DynamoDBClient.class); + private static final Status CLIENT_ERROR = new Status("CLIENT_ERROR", "An error occurred on the client."); + private static final String DEFAULT_HASH_KEY_VALUE = "YCSB_0"; + private static boolean useTypedFields; + /** The batch size to use for inserts. */ + private static int batchSize; + private static final int MAX_RETRY = 3; + private static int defaultReadCap; + private static int defaultWriteCap; + private static List indexes = null; + /** The bulk inserts pending for the thread. */ + private final List bulkInserts = new ArrayList(); + + @Override + public void init() throws DBException { + String debug = getProperties().getProperty("dynamodb.debug", null); + + if (null != debug && "true".equalsIgnoreCase(debug)) { + LOGGER.setLevel(Level.DEBUG); + } + + batchSize = Integer.parseInt(getProperties().getProperty("batchsize", "1")); + String configuredEndpoint = getProperties().getProperty("dynamodb.endpoint", null); + String credentialsFile = getProperties().getProperty("dynamodb.awsCredentialsFile", null); + String primaryKey = getProperties().getProperty("dynamodb.primaryKey", null); + String primaryKeyTypeString = getProperties().getProperty("dynamodb.primaryKeyType", null); + String consistentReads = getProperties().getProperty("dynamodb.consistentReads", null); + String connectMax = getProperties().getProperty("dynamodb.connectMax", null); + maxRetries = Integer.parseInt(getProperties().getProperty("dynamodb.maxRetries", "0")); + String configuredRegion = getProperties().getProperty("dynamodb.region", null); + + if (null != connectMax) { + this.maxConnects = Integer.parseInt(connectMax); + } + + if (null != consistentReads && "true".equalsIgnoreCase(consistentReads)) { + this.consistentRead = true; + } + + if (null != configuredEndpoint) { + this.endpoint = configuredEndpoint; + } + + if (null == primaryKey || primaryKey.length() < 1) { + throw new DBException("Missing primary key attribute name, cannot continue"); + } + + if (null != primaryKeyTypeString) { + try { + this.primaryKeyType = PrimaryKeyType.valueOf(primaryKeyTypeString.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new DBException("Invalid primary key mode specified: " + primaryKeyTypeString + + ". Expecting HASH or HASH_AND_RANGE."); + } + } + useTypedFields = "true".equalsIgnoreCase(getProperties().getProperty(TYPED_FIELDS_PROPERTY)); + if (this.primaryKeyType == PrimaryKeyType.HASH_AND_RANGE) { + // When the primary key type is HASH_AND_RANGE, keys used by YCSB + // are range keys so we can benchmark performance of individual hash + // partitions. In this case, the user must specify the hash key's name + // and optionally can designate a value for the hash key. + + String configuredHashKeyName = getProperties().getProperty("dynamodb.hashKeyName", null); + if (null == configuredHashKeyName || configuredHashKeyName.isEmpty()) { + throw new DBException("Must specify a non-empty hash key name when the primary key type is HASH_AND_RANGE."); + } + this.hashKeyName = configuredHashKeyName; + this.hashKeyValue = getProperties().getProperty("dynamodb.hashKeyValue", DEFAULT_HASH_KEY_VALUE); + } + + if (null != configuredRegion && configuredRegion.length() > 0) { + region = configuredRegion; + } + if(batchSize > 25) { + throw new DBException("batch site must be <= 25 for dynamodb."); + } + try { + AmazonDynamoDBClientBuilder dynamoDBBuilder = AmazonDynamoDBClientBuilder.standard(); + dynamoDBBuilder = null == endpoint ? + dynamoDBBuilder.withRegion(this.region) : + dynamoDBBuilder.withEndpointConfiguration( + new AwsClientBuilder.EndpointConfiguration(this.endpoint, this.region) + ); + dynamoDBBuilder + .withClientConfiguration( + new ClientConfiguration() + .withTcpKeepAlive(true) + .withMaxConnections(this.maxConnects) + .withMaxErrorRetry(maxRetries) + ); + if(credentialsFile != null) { + dynamoDBBuilder.withCredentials(new AWSStaticCredentialsProvider(new PropertiesCredentials(new File(credentialsFile)))); + } else { + dynamoDBBuilder.withCredentials(DefaultAWSCredentialsProviderChain.getInstance()); + LOGGER.info("using credentials from environment variables"); + } + dynamoDB = dynamoDBBuilder.build(); + primaryKeyName = primaryKey; + LOGGER.info("dynamodb connection created with " + this.endpoint); + } catch (Exception e1) { + LOGGER.error("DynamoDBClient.init(): Could not initialize DynamoDB client.", e1); + } + synchronized(DynamoDBClient.class) { + if(indexes == null) { + final String table = getProperties().getProperty(CoreConstants.TABLENAME_PROPERTY, CoreConstants.TABLENAME_PROPERTY_DEFAULT); + defaultReadCap = Integer.parseInt(getProperties().getProperty(INDEX_READ_CAP_PROPERTY, INDEX_READ_CAP_DEFAULT)); + defaultWriteCap = Integer.parseInt(getProperties().getProperty(INDEX_WRITE_CAP_PROPERTY, INDEX_WRITE_CAP_DEFAULT)); + List localIndexes = DynamoDBInitHelper.getIndexList(getProperties(), defaultReadCap, defaultWriteCap); + LOGGER.info("indexes loaded from config: " + localIndexes.toString()); + setIndexes(table, getProperties(), localIndexes); + indexes = loadRemoteIndexes(table); + LOGGER.info("loaded remote idexes: " + indexes.toString()); + } + } + } + + private List loadRemoteIndexes(String table) { + DescribeTableResult tableDesc = dynamoDB.describeTable(table); + List indexes = tableDesc.getTable().getGlobalSecondaryIndexes(); + if(indexes == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(indexes.size()); + for(GlobalSecondaryIndexDescription index : indexes) { + IndexDescriptor desc = new IndexDescriptor(); + desc.name = index.getIndexName(); + List schema = index.getKeySchema(); + for(KeySchemaElement el : schema) { + if(KeyType.HASH.toString().equals(el.getKeyType())) { + desc.hashKeyAttribute = el.getAttributeName(); + } else if(KeyType.RANGE.toString().equals(el.getKeyType())) { + desc.sortsKeyAttributes.add(el.getAttributeName()); + } + } + result.add(desc); + } + return result; + } + + private void waitForIndexCreation(String table, String name) { + LOGGER.info("initial wait"); + try { + Thread.sleep(10000); + } catch(InterruptedException ex) { + // ignore for now + } + while(true) { + DescribeTableResult result = dynamoDB.describeTable(table); + List indexes = result.getTable().getGlobalSecondaryIndexes(); + GlobalSecondaryIndexDescription myIndex = null; + String names = ""; + for(GlobalSecondaryIndexDescription index : indexes) { + names = names + index.getIndexName() + ", "; + if(name.equals(index.getIndexName())) { + myIndex = index; + } + } + if(myIndex == null) { + throw new IllegalStateException("Newly created index " + name + " was not in index list: " + names); + } + final String status = myIndex.getIndexStatus(); + LOGGER.info("status of newly created index " + name + " " + status); + if(IndexStatus.CREATING.toString().equals(status) || + IndexStatus.UPDATING.toString().equals(status)) { + LOGGER.info("waiting more"); + try { + Thread.sleep(10000); + } catch(InterruptedException ex) { + // ignore for now + } + } else { + LOGGER.info("let's move on"); + break; + } + } + } + + private void setIndexes(String table, Properties props, List indexes) { + // we do not return here, as it is beneficial to set table properties + final boolean setProperties = props.getProperty("dynamodb.settableproperties", "false").equals("true"); + if(indexes.size() == 0 && !setProperties) { + return; + } + List attributes = DynamoDBInitHelper.getFullAttributeDefinitionList(); + attributes.add(new AttributeDefinition().withAttributeName(primaryKeyName).withAttributeType(ScalarAttributeType.S)); + if(indexes.size() == 0 && setProperties) { + UpdateTableRequest req = new UpdateTableRequest() + .withAttributeDefinitions(attributes) + .withTableName(table); + UpdateTableResult result = dynamoDB.updateTable(req); + System.err.println("updated table"); + } + for(IndexDescriptor idx : indexes) { + CreateGlobalSecondaryIndexAction action = DynamoDBInitHelper.getCreateSecondaryIndexAction(idx); + GlobalSecondaryIndexUpdate up = new GlobalSecondaryIndexUpdate().withCreate(action); + UpdateTableRequest req = new UpdateTableRequest() + .withGlobalSecondaryIndexUpdates(up) + .withAttributeDefinitions(attributes) + .withTableName(table); + UpdateTableResult result = dynamoDB.updateTable(req); + waitForIndexCreation(table, idx.name); + System.err.println("prepared index creation: " + idx); + } + System.err.println("all indexes created"); + } + + @Override + public Status read(String table, String key, Set fields, Map result) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("readkey: " + key + " from table: " + table); + } + + GetItemRequest req = new GetItemRequest(table, createPrimaryKey(key)); + req.setAttributesToGet(fields); + req.setConsistentRead(consistentRead); + GetItemResult res; + + try { + res = dynamoDB.getItem(req); + } catch (AmazonServiceException ex) { + LOGGER.error(ex); + return Status.ERROR; + } catch (AmazonClientException ex) { + LOGGER.error(ex); + return CLIENT_ERROR; + } + + if (null != res.getItem()) { + result.putAll(DynamoDBQueryParameterHelper.extractResult(res.getItem())); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Result: " + res.toString()); + } + } + return Status.OK; + } + + @Override + public Status scan(String table, String startkey, int recordcount, + Set fields, Vector> result) { + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("scan " + recordcount + " records from key: " + startkey + " on table: " + table); + } + /* + * on DynamoDB's scan, startkey is *exclusive* so we need to + * getItem(startKey) and then use scan for the res + */ + GetItemRequest greq = new GetItemRequest(table, createPrimaryKey(startkey)); + greq.setAttributesToGet(fields); + + GetItemResult gres; + try { + gres = dynamoDB.getItem(greq); + } catch (AmazonServiceException ex) { + LOGGER.error(ex); + return Status.ERROR; + } catch (AmazonClientException ex) { + LOGGER.error(ex); + return CLIENT_ERROR; + } + if (null != gres.getItem()) { + result.add(DynamoDBQueryParameterHelper.extractResult(gres.getItem())); + } + + int count = 1; // startKey is done, rest to go. + Map startKey = createPrimaryKey(startkey); + ScanRequest req = new ScanRequest(table); + req.setAttributesToGet(fields); + while (count < recordcount) { + req.setExclusiveStartKey(startKey); + req.setLimit(recordcount - count); + ScanResult res; + try { + res = dynamoDB.scan(req); + } catch (AmazonServiceException ex) { + LOGGER.error(ex); + return Status.ERROR; + } catch (AmazonClientException ex) { + LOGGER.error(ex); + return CLIENT_ERROR; + } + + count += res.getCount(); + for (Map items : res.getItems()) { + result.add(DynamoDBQueryParameterHelper.extractResult(items)); + } + startKey = res.getLastEvaluatedKey(); + } + return Status.OK; + } + + @Override + public Status update(String table, String key, Map values) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("updatekey: " + key + " from table: " + table); + } + + Map attributes = new HashMap<>(values.size()); + for (Entry val : values.entrySet()) { + AttributeValue v = new AttributeValue(val.getValue().toString()); + attributes.put(val.getKey(), new AttributeValueUpdate().withValue(v).withAction("PUT")); + } + + return internalUpdateItem(table, key, attributes); + } + + @Override + public Status insert(String table, String key, List values) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("insertkey: " + primaryKeyName + "-" + key + " from table: " + table); + } + + Map attributes = useTypedFields + ? DynamoDBQueryParameterHelper.createTypedAttributes(values) + : DynamoDBQueryParameterHelper.createAttributes(fieldListAsIteratorMap(values)); + // adding primary key + attributes.put(primaryKeyName, new AttributeValue(key)); + if (primaryKeyType == PrimaryKeyType.HASH_AND_RANGE) { + // If the primary key type is HASH_AND_RANGE, then what has been put + // into the attributes map above is the range key part of the primary + // key, we still need to put in the hash key part here. + attributes.put(hashKeyName, new AttributeValue(hashKeyValue)); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("insertkey: sending attributes " + attributes); + } + try { + if(batchSize < 2) { + PutItemRequest putItemRequest = new PutItemRequest(table, attributes); + putItemRequest.setConditionExpression("attribute_not_exists(" + primaryKeyName + ")"); + dynamoDB.putItem(putItemRequest); + } else { + WriteRequest ww = new WriteRequest(new PutRequest(attributes)); + bulkInserts.add(ww); + if(bulkInserts.size() < batchSize) { return Status.BATCHED_OK; } + List local = new ArrayList<>(bulkInserts); + bulkInserts.clear(); + for(int retries = 0; retries < MAX_RETRY && local.size() > 0; retries++) { + BatchWriteItemRequest batch = new BatchWriteItemRequest(); + batch.addRequestItemsEntry(table, local); + BatchWriteItemResult result = dynamoDB.batchWriteItem(batch); + local = result.getUnprocessedItems().get(table); + if(local == null || local.size() == 0) { return Status.OK; } + LOGGER.error("could not process all requests in a batch: " + local.size() + " requests missing; retrying."); + } + return Status.ERROR; + } + } catch (AmazonServiceException ex) { + LOGGER.error(ex); + return Status.ERROR; + } catch (AmazonClientException ex) { + LOGGER.error(ex); + return CLIENT_ERROR; + } + return Status.OK; + } + + @Override + public Status delete(String table, String key) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("deletekey: " + key + " from table: " + table); + } + DeleteItemRequest req = new DeleteItemRequest(table, createPrimaryKey(key)); + try { + dynamoDB.deleteItem(req); + } catch (AmazonServiceException ex) { + LOGGER.error(ex); + return Status.ERROR; + } catch (AmazonClientException ex) { + LOGGER.error(ex); + return CLIENT_ERROR; + } + return Status.OK; + } + + private IndexDescriptor findIndexMatchingFilter(List filters) { + for(Comparison c : filters) { + for(IndexDescriptor idx : indexes) { + if(c.getFieldname().equals(idx.hashKeyAttribute)) { + return idx; + } + // FIXME: to be really correct, the filter does have to + // check for equality as well + } + } + return null; + } + + public Status internalFindOne(String table, List filters, + Set fields,List resultList) { + IndexDescriptor desc = findIndexMatchingFilter(filters); + if(desc == null) { + LOGGER.error("did not find a matching index for filter: " + filters); + return Status.ERROR; + } else { + if(LOGGER.isDebugEnabled()) { + LOGGER.debug("found matching index for filters: " + filters + " => " + desc); + } + } + Table dbTable = new DynamoDB(dynamoDB).getTable(table); + Index idx = dbTable.getIndex(desc.name); + QuerySpec query = new QuerySpec().withConsistentRead(consistentRead); + DynamoDBQueryHelper.buildPreparedQuery(query, desc.hashKeyAttribute, filters); + DynamoDBQueryHelper.bindPreparedQuery(query, filters); + if(LOGGER.isDebugEnabled()) { + LOGGER.debug("query spec: " + query.getRequest()); + } + IteratorSupport col = idx.query(query).iterator(); + if(!col.hasNext()) { + return Status.NOT_FOUND; + } + Item item = col.next(); + if(col.hasNext()) { + LOGGER.error("retrieved more than one element"); + return Status.ERROR; + } + resultList.add(item); + return Status.OK; + } + + @Override + public Status findOne(String table, List filters, Set fields, + Map result) { + if(fields != null) { + throw new IllegalArgumentException("fields can only be null by now"); + } + List resultList = new ArrayList<>(1); + Status proxy = internalFindOne(table, filters, fields, resultList); + if(proxy != Status.OK) return proxy; + result.putAll(DynamoDBQueryParameterHelper.extractResultFromItem(resultList.get(0))); + return Status.OK; + } + + private Status internalUpdateItem(String table, String localPrimaryKey, Map attributes){ + Map primaryKey = createPrimaryKey(localPrimaryKey); + UpdateItemRequest req = new UpdateItemRequest(table, primaryKey, attributes); + + try { + dynamoDB.updateItem(req); + } catch (AmazonServiceException ex) { + LOGGER.error(ex); + return Status.ERROR; + } catch (AmazonClientException ex) { + LOGGER.error(ex); + return CLIENT_ERROR; + } + return Status.OK; + } + + @Override + public Status updateOne(String table, List filters, List fields) { + List resultList = new ArrayList<>(1); + Status found = internalFindOne(table, filters, null, resultList); + if(found != Status.OK) return found; + Item it = resultList.get(0); + String id = it.getString(primaryKeyName); + Map attributes = new HashMap<>(); + Map plainAttributes = DynamoDBQueryParameterHelper.createTypedAttributes(fields); + for(Map.Entry e : plainAttributes.entrySet()) { + attributes.put(e.getKey(), new AttributeValueUpdate().withValue(e.getValue()).withAction("PUT")); + } + if(LOGGER.isDebugEnabled()) { + LOGGER.debug("updating item: " + id + " with " + plainAttributes); + } + return internalUpdateItem(table, id, attributes); + } + + private Map createPrimaryKey(String key) { + Map k = new HashMap<>(); + if (primaryKeyType == PrimaryKeyType.HASH) { + k.put(primaryKeyName, new AttributeValue().withS(key)); + } else if (primaryKeyType == PrimaryKeyType.HASH_AND_RANGE) { + k.put(hashKeyName, new AttributeValue().withS(hashKeyValue)); + k.put(primaryKeyName, new AttributeValue().withS(key)); + } else { + throw new RuntimeException("Assertion Error: impossible primary key type"); + } + return k; + } +} \ No newline at end of file diff --git a/dynamodb/src/main/java/site/ycsb/db/DynamoDBInitHelper.java b/dynamodb/src/main/java/site/ycsb/db/DynamoDBInitHelper.java new file mode 100644 index 0000000..f921666 --- /dev/null +++ b/dynamodb/src/main/java/site/ycsb/db/DynamoDBInitHelper.java @@ -0,0 +1,187 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.CreateGlobalSecondaryIndexAction; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.Projection; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.node.IntNode; + +import site.ycsb.db.DynamoDBClient.IndexDescriptor; +import site.ycsb.workloads.schema.SchemaHolder; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumn; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnKind; + +public final class DynamoDBInitHelper { + + static List getIndexList(Properties props, int defaultReadCap, int defaultWriteCap) { + String indexeslist = props.getProperty(DynamoDBClient.INDEX_LIST_PROPERTY); + if(indexeslist == null) { + return Collections.emptyList(); + } + DynamoDBClient.LOGGER.info("raw index property: " + indexeslist); + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = null; + try { + root = mapper.readTree(indexeslist); + } catch(IOException ex) { + throw new RuntimeException(ex); + } + DynamoDBClient.LOGGER.info("parsed index property: " + root.toString()); + if(!root.isArray()) { + throw new IllegalArgumentException("index specification must be a JSON array"); + } + ArrayNode array = (ArrayNode) root; + if(array.size() == 0) { + return Collections.emptyList(); + } + DynamoDBClient.LOGGER.info("parsed index array with size: " + array.size()); + List retVal = new ArrayList<>(); + for(int i = 0; i < array.size(); i++) { + JsonNode el = array.get(i); + if(!el.isObject()) { + throw new IllegalArgumentException("index elements must be a JSON object"); + } + IndexDescriptor desc = new IndexDescriptor(); + ObjectNode object = (ObjectNode) el; + JsonNode name = object.get("name"); + if(name == null || !name.isTextual()) { + throw new IllegalArgumentException("index elements must be a JSON object with 'name' of type string"); + } + desc.name = ((TextNode) name).asText(); + + JsonNode hash = object.get("hashAttribute"); + if(hash != null && !hash.isTextual()){ + throw new IllegalArgumentException("index elements must be a JSON object. If 'hashAttribute' is set, it must be textual"); + } else if(hash != null) { + desc.hashKeyAttribute = ((TextNode) hash).asText(); + } + + JsonNode sorts = object.get("sortAttributes"); + if(sorts != null && !sorts.isArray()) { + ArrayNode sortArray = (ArrayNode) sorts; + for(int j = 0; j < sortArray.size(); j++) { + JsonNode sortEl = sortArray.get(j); + if(sortEl == null || !sortEl.isTextual()) { + throw new IllegalArgumentException("index elements must be a JSON object with 'sortAttributes' set as an array of strings"); + } + desc.sortsKeyAttributes.add(((TextNode) sortEl).asText()); + } + } + + JsonNode readCap = object.get("readCap"); + if(readCap == null || !readCap.isNumber()) { + desc.readCap = defaultReadCap; + } else { + desc.readCap = ((IntNode) readCap).asInt(); + } + + JsonNode writeCap = object.get("writeCap"); + if(writeCap == null || !writeCap.isNumber()) { + desc.writeCap = defaultWriteCap; + } else { + desc.writeCap = ((IntNode) writeCap).asInt(); + } + + DynamoDBClient.LOGGER.info("parsed array[" + i + "]: " + desc); + retVal.add(desc); + } + return retVal; + } + + private static AttributeDefinition attributeDefinitionForColumn(SchemaColumn c) { + // fixme: consider nested columns + if(c.getColumnKind() == SchemaColumnKind.SCALAR) { + AttributeDefinition def = new AttributeDefinition(); + def.setAttributeName(c.getColumnName()); + switch(c.getColumnType()) { + case BYTES: + def.setAttributeType(ScalarAttributeType.B); + break; + case INT: + case LONG: + def.setAttributeType(ScalarAttributeType.N); + break; + case TEXT: + def.setAttributeType(ScalarAttributeType.S); + break; + case CUSTOM: + default: + throw new IllegalArgumentException("not supported: " + c.getColumnType()); + } + return def; + } else if(c.getColumnKind() == SchemaColumnKind.NESTED) { + if(c.getColumnName() == "airline") { + return new AttributeDefinition() + .withAttributeName("airline.alias") + .withAttributeType("S"); + } + return null; + } else if(c.getColumnKind() == SchemaColumnKind.ARRAY) { + return null; + } + throw new IllegalArgumentException("not supported: " + c.getColumnType()); + } + + static List getFullAttributeDefinitionList() { + List l = SchemaHolder.INSTANCE.getOrderedListOfColumns(); + List fields = new ArrayList<>(); + for(SchemaColumn c : l) { + AttributeDefinition d = attributeDefinitionForColumn(c); + if(d != null) fields.add(d); + } + return fields; + } + + static CreateGlobalSecondaryIndexAction getCreateSecondaryIndexAction(IndexDescriptor idx) { + // GlobalSecondaryIndex + CreateGlobalSecondaryIndexAction action = new CreateGlobalSecondaryIndexAction(); + action.setIndexName(idx.name); + action.setProjection(new Projection().withProjectionType("ALL")); + List schema = new ArrayList<>(); + if(idx.hashKeyAttribute != null) { + schema.add(new KeySchemaElement(idx.hashKeyAttribute, KeyType.HASH)); + } + for(String s : idx.sortsKeyAttributes) { + schema.add(new KeySchemaElement(s, KeyType.RANGE)); + } + action.setKeySchema(schema); + action.setProvisionedThroughput( + new ProvisionedThroughput() + .withReadCapacityUnits(Long.valueOf(idx.readCap)) + .withWriteCapacityUnits(Long.valueOf(idx.writeCap)) + ); + return action; + } + + private DynamoDBInitHelper() { + + } +} diff --git a/dynamodb/src/main/java/site/ycsb/db/DynamoDBQueryHelper.java b/dynamodb/src/main/java/site/ycsb/db/DynamoDBQueryHelper.java new file mode 100644 index 0000000..674be44 --- /dev/null +++ b/dynamodb/src/main/java/site/ycsb/db/DynamoDBQueryHelper.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db; + +import java.util.List; + +import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec; +import com.amazonaws.services.dynamodbv2.document.utils.ValueMap; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.ComparisonOperator; + +public final class DynamoDBQueryHelper { + + private static void bindQueryString(ValueMap m, Comparison c) { + Comparison d = c; + String fieldName = c.getFieldname(); + while(d.isSimpleNesting()) { + d = d.getSimpleNesting(); + fieldName = fieldName + "." + d.getFieldname(); + } + if(d.comparesInts()) { + m.withInt(":v_"+ fieldName, d.getOperandAsInt()); + } else if(d.comparesStrings()) { + m.withString(":v_"+ fieldName, d.getOperandAsString()); + } else { + throw new IllegalArgumentException("unknown: " + c); + } + } + + private static String buildQueryString(Comparison c) { + Comparison d = c; + String fieldName = c.getFieldname(); + while(d.isSimpleNesting()) { + d = d.getSimpleNesting(); + fieldName = fieldName + "." + d.getFieldname(); + } + if(d.comparesInts()) { + ComparisonOperator co = d.getOperator(); + if(co == ComparisonOperator.INT_LTE) { + return fieldName + " <= " + ":v_"+ fieldName; + } else { + throw new IllegalArgumentException("unknown: " + co); + } + } else if(d.comparesStrings()) { + ComparisonOperator co = d.getOperator(); + if(co == ComparisonOperator.STRING_EQUAL) { + return fieldName + " = " + ":v_"+ fieldName; + } else { + throw new IllegalArgumentException("unknown: " + co); + } + } else { + throw new IllegalArgumentException("unknown: " + c); + } + } + + static void bindPreparedQuery(QuerySpec query, List filters) { + ValueMap m = new ValueMap(); + for(Comparison c : filters) { + bindQueryString(m, c); + } + query.withValueMap(m); + } + + static void buildPreparedQuery(QuerySpec query, String indexField, List filters) { + query.withMaxResultSize(1); + for(Comparison c : filters) { + if(indexField.equals(c.getFieldname())) { + query.withKeyConditionExpression(buildQueryString(c)); + } else { + String f = query.getFilterExpression(); + String g = buildQueryString(c); + String h = f == null ? g : f + " AND " + g; + query.withFilterExpression(h); + } + } + } + + private DynamoDBQueryHelper() { + + } +} diff --git a/dynamodb/src/main/java/site/ycsb/db/DynamoDBQueryParameterHelper.java b/dynamodb/src/main/java/site/ycsb/db/DynamoDBQueryParameterHelper.java new file mode 100644 index 0000000..6c32e3d --- /dev/null +++ b/dynamodb/src/main/java/site/ycsb/db/DynamoDBQueryParameterHelper.java @@ -0,0 +1,129 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.amazonaws.services.dynamodbv2.document.Item; +import com.amazonaws.services.dynamodbv2.model.AttributeValue; + +import site.ycsb.ByteIterator; +import site.ycsb.StringByteIterator; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +public final class DynamoDBQueryParameterHelper { + + private static AttributeValue fillAttribute(Object o) { + AttributeValue content = new AttributeValue(); + if(String.class.isInstance(o)) { + content.setS((String) o); + } else if( + Integer.class.isInstance(o) || int.class.isInstance(o) || + Long.class.isInstance(o) || long.class.isInstance(o) || + Double.class.isInstance(o) || double.class.isInstance(o) || + Float.class.isInstance(o) || float.class.isInstance(o)) { + content.setN(o.toString()); + } else { + throw new IllegalArgumentException("what? " + o); + } + return content; + } + private static void fillDocument(DatabaseField field, Map toInsert) { + DataWrapper wrapper = field.getContent(); + AttributeValue content; + if(wrapper.isTerminal()){ + content = new AttributeValue(); + if(wrapper.isInteger() || wrapper.isLong()) { + content.setN(wrapper.asObject().toString()); + } else if(wrapper.isString()) { + content.setS(wrapper.asString()); + } else { + // as iterator + Object o = field.getContent().asObject(); + byte[] b = (byte[]) o; + content.setS(new String(b)); + } + } else if (wrapper.isArray()) { + final List oa = (List) wrapper.asObject(); + List elements = new ArrayList<>(oa.size()); + for(int i = 0; i < oa.size(); i++) { + // this WILL BREAK if content is a nested + // document within the array + elements.add(fillAttribute(oa.get(i))); + } + content = new AttributeValue(); + content.setL(elements); + } else if(wrapper.isNested()) { + content = new AttributeValue(); + Map inner = new HashMap<>(); + List innerFields = wrapper.asNested(); + for(DatabaseField iF : innerFields) { + fillDocument(iF, inner); + } + content.setM(inner); + } else { + throw new IllegalStateException("neither terminal, nor array, nor nested"); + } + toInsert.put( + field.getFieldname(), + content + ); + } + + static Map createTypedAttributes(List values) { + Map toInsert = new HashMap<>(values.size() + 1); + for (DatabaseField field : values) { + fillDocument(field, toInsert); + } + return toInsert; + } + + static Map createAttributes(Map values) { + Map attributes = new HashMap<>(values.size() + 1); + for (Entry val : values.entrySet()) { + attributes.put(val.getKey(), new AttributeValue(val.getValue().toString())); + } + return attributes; + } + + static HashMap extractResultFromItem(Item item) { + System.err.println("extracting item: " + item.toJSONPretty()); + return new HashMap<>(); + } + + static HashMap extractResult(Map item) { + if (null == item) { + return null; + } + HashMap rItems = new HashMap<>(item.size()); + + for (Entry attr : item.entrySet()) { + if (DynamoDBClient.LOGGER.isDebugEnabled()) { + DynamoDBClient.LOGGER.debug(String.format("Result- key: %s, value: %s", attr.getKey(), attr.getValue())); + } + rItems.put(attr.getKey(), new StringByteIterator(attr.getValue().getS())); + } + return rItems; + } + + private DynamoDBQueryParameterHelper() { + // random + } +} diff --git a/dynamodb/src/main/java/site/ycsb/db/package-info.java b/dynamodb/src/main/java/site/ycsb/db/package-info.java new file mode 100644 index 0000000..21216b1 --- /dev/null +++ b/dynamodb/src/main/java/site/ycsb/db/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2015-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. + */ + +/** + * The YCSB binding for DynamoDB. + */ +package site.ycsb.db; + diff --git a/dynamodb/src/main/resources/log4j.properties b/dynamodb/src/main/resources/log4j.properties new file mode 100644 index 0000000..a9f3d66 --- /dev/null +++ b/dynamodb/src/main/resources/log4j.properties @@ -0,0 +1,25 @@ +# Copyright (c) 2012 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. + +#define the console appender +log4j.appender.consoleAppender = org.apache.log4j.ConsoleAppender + +# now define the layout for the appender +log4j.appender.consoleAppender.layout = org.apache.log4j.PatternLayout +log4j.appender.consoleAppender.layout.ConversionPattern=%-4r [%t] %-5p %c %x -%m%n + +# now map our console appender as a root logger, means all log messages will go +# to this appender +log4j.rootLogger = INFO, consoleAppender diff --git a/jdbc/README.md b/jdbc/README.md new file mode 100644 index 0000000..97b0543 --- /dev/null +++ b/jdbc/README.md @@ -0,0 +1,135 @@ + + +# JDBC Driver for YCSB +This driver enables YCSB to work with databases accessible via the JDBC protocol. + +## Getting Started +### 1. Start your database +This driver will connect to databases that use the JDBC protocol, please refer to your databases documentation on information on how to install, configure and start your system. + +### 2. Set up YCSB +You can clone the YCSB project and compile it to stay up to date with the latest changes. Or you can just download the latest release and unpack it. Either way, instructions for doing so can be found here: https://github.com/brianfrankcooper/YCSB. + +### 3. Configure your database and table. +You can name your database what ever you want, you will need to provide the database name in the JDBC connection string. + +You can name your table whatever you like also, but it needs to be specified using the YCSB core properties, the default is to just use 'usertable' as the table name. + +The expected table schema will look similar to the following, syntactical differences may exist with your specific database: + +```sql +CREATE TABLE usertable ( + YCSB_KEY VARCHAR(255) PRIMARY KEY, + FIELD0 TEXT, FIELD1 TEXT, + FIELD2 TEXT, FIELD3 TEXT, + FIELD4 TEXT, FIELD5 TEXT, + FIELD6 TEXT, FIELD7 TEXT, + FIELD8 TEXT, FIELD9 TEXT +); +``` + +Key take aways: + +* The primary key field needs to be named YCSB_KEY +* The other fields need to be prefixed with FIELD and count up starting from 1 +* Add the same number of FIELDs as you specify in the YCSB core properties, default is 10. +* The type of the fields is not so important as long as they can accept strings of the length that you specify in the YCSB core properties, default is 100. + +#### JdbcDBCreateTable Utility +YCSB has a utility to help create your SQL table. NOTE: It does not support all databases flavors, if it does not work for you, you will have to create your table manually with the schema given above. An example usage of the utility: + +```sh +java -cp YCSB_HOME/jdbc-binding/lib/jdbc-binding-0.4.0.jar:mysql-connector-java-5.1.37-bin.jar site.ycsb.db.JdbcDBCreateTable -P db.properties -n usertable +``` + +Hint: you need to include your Driver jar in the classpath as well as specify JDBC connection information via a properties file, and a table name with ```-n```. + +Simply executing the JdbcDBCreateTable class without any other parameters will print out usage information. + +### 4. Configure YCSB connection properties +You need to set the following connection configurations: + +```sh +db.driver=com.mysql.jdbc.Driver +db.url=jdbc:mysql://127.0.0.1:3306/ycsb +db.user=admin +db.passwd=admin +``` + +Be sure to use your driver class, a valid JDBC connection string, and credentials to your database. + +For connection fail-over in a DBMS cluster specify the connection string as follows (example based on Postgres): + +```sh +db.driver=org.postgresql.Driver +db.url=jdbc:postgresql://IP1:PORT1,IP2:PORT2,IP3:PORT3/ycsb +db.user=admin +db.passwd=admin +``` + +For using multiple shards in a DBMS cluster specify the connection string as follows by using `;`as delimiter (example based on PostgreSQL): + +```sh +db.driver=org.postgresql.Driver +db.url=jdbc:postgresql://host1:port1/ycsb;jdbc:postgresql://host2:port2/ycsb +db.user=admin +db.passwd=admin +``` + +You can add these to your workload configuration or a separate properties file and specify it with ```-P``` or you can add the properties individually to your ycsb command with ```-p```. + +### 5. Add your JDBC Driver to the classpath +There are several ways to do this, but a couple easy methods are to put a copy of your Driver jar in ```YCSB_HOME/jdbc-binding/lib/``` or just specify the path to your Driver jar with ```-cp``` in your ycsb command. + +### 6. Running a workload +Before you can actually run the workload, you need to "load" the data first. + +```sh +bin/ycsb load jdbc -P workloads/workloada -P db.properties -cp mysql-connector-java.jar +``` + +Then, you can run the workload: + +```sh +bin/ycsb run jdbc -P workloads/workloada -P db.properties -cp mysql-connector-java.jar +``` + +## Configuration Properties + +```sh +db.driver=com.mysql.jdbc.Driver # The JDBC driver class to use. +db.url=jdbc:mysql://127.0.0.1:3306/ycsb # The Database connection URL. +db.user=admin # User name for the connection. +db.passwd=admin # Password for the connection. +db.batchsize=1000 # The batch size for doing batched inserts. Defaults to 0. Set to >0 to use batching. +jdbc.fetchsize=10 # The JDBC fetch size hinted to the driver. +jdbc.autocommit=true # The JDBC connection auto-commit property for the driver. +jdbc.batchupdateapi=false # Use addBatch()/executeBatch() JDBC methods instead of executeUpdate() for writes (default: false) +db.batchsize=1000 # The number of rows to be batched before commit (or executeBatch() when jdbc.batchupdateapi=true) +``` + +Please refer to https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties for all other YCSB core properties. + +## JDBC Parameter to Improve Insert Performance + +Some JDBC drivers support re-writing batched insert statements into multi-row insert statements. This technique can yield order of magnitude improvement in insert statement performance. To enable this feature: +- **db.batchsize** must be greater than 0. The magniute of the improvement can be adjusted by varying **batchsize**. Start with a small number and increase at small increments until diminishing return in the improvement is observed. +- set **jdbc.batchupdateapi=true** to enable batching. +- set JDBC driver specific connection parameter in **db.url** to enable the rewrite as shown in the examples below: + * MySQL [rewriteBatchedStatements=true](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html) with `db.url=jdbc:mysql://127.0.0.1:3306/ycsb?rewriteBatchedStatements=true` + * Postgres [reWriteBatchedInserts=true](https://jdbc.postgresql.org/documentation/head/connect.html#connection-parameters) with `db.url=jdbc:postgresql://127.0.0.1:5432/ycsb?reWriteBatchedInserts=true` diff --git a/jdbc/pom.xml b/jdbc/pom.xml new file mode 100644 index 0000000..bb262c6 --- /dev/null +++ b/jdbc/pom.xml @@ -0,0 +1,57 @@ + + + + + 4.0.0 + + site.ycsb + binding-parent + 0.18.0-SNAPSHOT + ../binding-parent + + + jdbc-binding + JDBC DB Binding + jar + + + + org.apache.openjpa + openjpa-jdbc + ${openjpa.jdbc.version} + + + site.ycsb + core + ${project.version} + provided + + + junit + junit + 4.12 + test + + + org.hsqldb + hsqldb + 2.3.3 + test + + + diff --git a/jdbc/src/main/conf/db.properties b/jdbc/src/main/conf/db.properties new file mode 100644 index 0000000..e12cc1e --- /dev/null +++ b/jdbc/src/main/conf/db.properties @@ -0,0 +1,22 @@ +# Copyright (c) 2012 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. + +# Properties file that contains database connection information. + +db.driver=org.h2.Driver +# jdbc.fetchsize=20 +db.url=jdbc:h2:tcp://foo.com:9092/~/h2/ycsb +db.user=sa +db.passwd= diff --git a/jdbc/src/main/conf/h2.properties b/jdbc/src/main/conf/h2.properties new file mode 100644 index 0000000..cfde14c --- /dev/null +++ b/jdbc/src/main/conf/h2.properties @@ -0,0 +1,21 @@ +# Copyright (c) 2012 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. + +# Properties file that contains database connection information. + +db.driver=org.h2.Driver +db.url=jdbc:h2:tcp://foo.com:9092/~/h2/ycsb +db.user=sa +db.passwd= diff --git a/jdbc/src/main/java/site/ycsb/db/FilterBuilder.java b/jdbc/src/main/java/site/ycsb/db/FilterBuilder.java new file mode 100644 index 0000000..a806c67 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/FilterBuilder.java @@ -0,0 +1,230 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import site.ycsb.ByteIterator; +import site.ycsb.NumericByteIterator; +import site.ycsb.StringByteIterator; +import site.ycsb.workloads.schema.SchemaHolder; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumn; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnType; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.ComparisonOperator; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +public final class FilterBuilder { + + private static java.util.Base64.Encoder encoder = java.util.Base64.getEncoder(); + + public static String buildConcatenatedPlaceholderSet(List fields) { + return innerBuildConcatenatedSet(fields, true); + } + + public static String buildConcatenatedSet(List fields) { + return innerBuildConcatenatedSet(fields, false); + } + + private static String innerBuildConcatenatedSet(List fields, boolean placeholder) { + List parts = new ArrayList<>(); + for (DatabaseField value: fields) { + DataWrapper w = value.getContent(); + if(w.isTerminal()) { + if(w.isInteger()) { + parts.add(value.getFieldname() + " = " + (placeholder ? "?" : w.asInteger())); + } else if(w.isLong()) { + parts.add(value.getFieldname() + " = " + (placeholder ? "?" : w.asLong())); + } else if(w.isString()) { + if(placeholder) { + parts.add(value.getFieldname() + " = ? "); + } else { + parts.add(value.getFieldname() + " = '" + w.asString() + "'"); + } + } else { + // assuming this is an iterator + // which is the only remaining terminal + if(placeholder) { + parts.add(value.getFieldname() + " = ? "); + } else { + ByteIterator bi = w.asIterator(); + bi.reset(); + parts.add(value.getFieldname() + " = '" + encoder.encodeToString(bi.toArray()) + "'"); + } + } + } else if(w.isArray()) { + throw new UnsupportedOperationException("array type columns not supported (yet)"); + } else if(w.isNested()) { + throw new UnsupportedOperationException("nested type columns not supported (yet)"); + } else { + // do nothing; something broke that we ignore for now + } + } + return concat(parts); + } + + static int bindConcatenatedSet(PreparedStatement statement, int startIndex, List fields) throws SQLException { + int currentIndex = startIndex; + for (DatabaseField value: fields) { + DataWrapper w = value.getContent(); + if(w.isTerminal()) { + if(w.isInteger()) { + statement.setInt(currentIndex, w.asInteger()); + } else if(w.isLong()) { + statement.setLong(currentIndex, w.asLong()); + } else if(w.isString()) { + statement.setString(currentIndex, w.asString()); + } else { + ByteIterator bi = w.asIterator(); + bi.reset(); + statement.setString(currentIndex, encoder.encodeToString(bi.toArray())); + // assuming this is an iterator + // which is the only remaining terminal + } + } else if(w.isArray()) { + throw new UnsupportedOperationException("array type columns not supported (yet)"); + } else if(w.isNested()) { + throw new UnsupportedOperationException("nested type columns not supported (yet)"); + } else { + // do nothing; something broke that we ignore for now + } + currentIndex++; + } + return currentIndex; + } + + static int bindFilterValues(PreparedStatement statement, int startIndex, List filters) throws SQLException { + int currentIndex = startIndex; + for(Comparison c : filters) { + if(c.isSimpleNesting()) { + throw new IllegalArgumentException("nestings not supported for JDBC"); + } + if(c.comparesStrings()) { + statement.setString(currentIndex, c.getOperandAsString()); + } else if(c.comparesInts()) { + statement.setInt(currentIndex, c.getOperandAsInt()); + } else { + throw new IllegalStateException(); + } + currentIndex++; + } + return currentIndex; + } + + public static String buildConcatenatedPlaceholderFilter(List filters) { + return buildConcatenatedInnerFilter(filters, true); + } + + static String buildConcatenatedFilter(List filters) { + return buildConcatenatedInnerFilter(filters, false); + } + + static String buildConcatenatedInnerFilter(List filters, boolean placeholder) { + List lFilters = new ArrayList<>(filters.size()); + for(Comparison c : filters) { + Comparison d = c; + String fieldName = d.getFieldname(); + if(d.isSimpleNesting()) { + throw new IllegalArgumentException("nestings not supported for JDBC"); + } + if(d.comparesStrings()) { + lFilters.add( + FilterBuilder.buildStringFilter( + fieldName, + d.getOperator(), + placeholder ? null : d.getOperandAsString() + )); + } else if(d.comparesInts()) { + lFilters.add( + FilterBuilder.buildIntFilter( + fieldName, + d.getOperator(), + placeholder ? null : d.getOperandAsInt() + )); + } else { + throw new IllegalStateException(); + } + } + return and(lFilters); + } + + static String buildStringFilter(String fieldName, ComparisonOperator op, String value) { + String operand = value == null ? "? " : "'" + value + "' "; + switch (op) { + case STRING_EQUAL: + return fieldName + " LIKE " + operand; + default: + throw new IllegalArgumentException("no string operator"); + } + } + + static String buildIntFilter(String fieldName, ComparisonOperator op, Integer value) { + String operand = value == null ? "? " : value.toString(); + switch (op) { + case INT_LTE: + return fieldName + " <= " + operand; + default: + throw new IllegalArgumentException("no int operator"); + } + } + + static String concat(List args){ + String result = args.get(0); + for(int i = 1; i < args.size(); i++) { + result = result + " , " + args.get(i); + } + return result; + } + + static String and(List args){ + String result = args.get(0); + for(int i = 1; i < args.size(); i++) { + result = result + " AND " + args.get(i); + } + return result; + } + + static void drainTypedResult(ResultSet result, Map resultBuffer) throws SQLException { + final SchemaHolder schema = SchemaHolder.INSTANCE; + for(SchemaColumn c : schema.getOrderedListOfColumns()) { + String cName = c.getColumnName(); + if(c.getColumnType() == SchemaColumnType.TEXT) { + String value = result.getString(cName); + resultBuffer.put(cName, new StringByteIterator(value)); + } else if(c.getColumnType() == SchemaColumnType.LONG) { + long value = result.getLong(cName); + resultBuffer.put(cName, new NumericByteIterator(value)); + } else if(c.getColumnType() == SchemaColumnType.INT) { + int value = result.getInt(cName); + resultBuffer.put(cName, new NumericByteIterator(value)); + } else if(c.getColumnType() == SchemaColumnType.BYTES) { + byte[] value = result.getBytes(cName); + resultBuffer.put(cName, new StringByteIterator(new String(value))); + } else { + throw new IllegalArgumentException("not supported"); + } + } + } + + private FilterBuilder () { + // no instances + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/IndexDescriptor.java b/jdbc/src/main/java/site/ycsb/db/IndexDescriptor.java new file mode 100644 index 0000000..a404c25 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/IndexDescriptor.java @@ -0,0 +1,12 @@ +package site.ycsb.db; + +import java.util.ArrayList; +import java.util.List; + +public final class IndexDescriptor { + public String name; + public String method; + public String order; + public boolean concurrent; + public List columnNames = new ArrayList<>(); +} \ No newline at end of file diff --git a/jdbc/src/main/java/site/ycsb/db/JdbcDBCli.java b/jdbc/src/main/java/site/ycsb/db/JdbcDBCli.java new file mode 100644 index 0000000..7dcd160 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/JdbcDBCli.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2010 - 2016 Yahoo! Inc. All rights reserved. + * Copyright 2023-2024 benchANT GmbH. 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 site.ycsb.db; + +import java.io.FileInputStream; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Enumeration; +import java.util.Properties; + +/** + * Execute a JDBC command line. + * + * @author sudipto + */ +public final class JdbcDBCli { + + private static void usageMessage() { + System.out.println("JdbcCli. Options:"); + System.out.println(" -p key=value properties defined."); + System.out.println(" -P location of the properties file to load."); + System.out.println(" -c SQL command to execute."); + } + + private static void executeCommand(Properties props, String sql) throws SQLException { + String driver = props.getProperty(JdbcDBConstants.DRIVER_CLASS); + String username = props.getProperty(JdbcDBConstants.CONNECTION_USER); + String password = props.getProperty(JdbcDBConstants.CONNECTION_PASSWD, ""); + String url = props.getProperty(JdbcDBConstants.CONNECTION_URL); + if (driver == null || username == null || url == null) { + throw new SQLException("Missing connection information."); + } + + Connection conn = null; + + try { + Class.forName(driver); + + conn = DriverManager.getConnection(url, username, password); + Statement stmt = conn.createStatement(); + stmt.execute(sql); + System.out.println("Command \"" + sql + "\" successfully executed."); + } catch (ClassNotFoundException e) { + throw new SQLException("JDBC Driver class not found."); + } finally { + if (conn != null) { + System.out.println("Closing database connection."); + conn.close(); + } + } + } + + /** + * @param args + */ + public static void main(String[] args) { + + if (args.length == 0) { + usageMessage(); + System.exit(0); + } + + Properties props = new Properties(); + Properties fileprops = new Properties(); + String sql = null; + + // parse arguments + int argindex = 0; + while (args[argindex].startsWith("-")) { + if (args[argindex].compareTo("-P") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + String propfile = args[argindex]; + argindex++; + + Properties myfileprops = new Properties(); + try { + myfileprops.load(new FileInputStream(propfile)); + } catch (IOException e) { + System.out.println(e.getMessage()); + System.exit(0); + } + + // Issue #5 - remove call to stringPropertyNames to make compilable + // under Java 1.5 + for (Enumeration e = myfileprops.propertyNames(); e.hasMoreElements();) { + String prop = (String) e.nextElement(); + + fileprops.setProperty(prop, myfileprops.getProperty(prop)); + } + + } else if (args[argindex].compareTo("-p") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + int eq = args[argindex].indexOf('='); + if (eq < 0) { + usageMessage(); + System.exit(0); + } + + String name = args[argindex].substring(0, eq); + String value = args[argindex].substring(eq + 1); + props.put(name, value); + argindex++; + } else if (args[argindex].compareTo("-c") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + sql = args[argindex++]; + } else { + System.out.println("Unknown option " + args[argindex]); + usageMessage(); + System.exit(0); + } + + if (argindex >= args.length) { + break; + } + } + + if (argindex != args.length) { + usageMessage(); + System.exit(0); + } + + // overwrite file properties with properties from the command line + + // Issue #5 - remove call to stringPropertyNames to make compilable under + // Java 1.5 + for (Enumeration e = props.propertyNames(); e.hasMoreElements();) { + String prop = (String) e.nextElement(); + + fileprops.setProperty(prop, props.getProperty(prop)); + } + + if (sql == null) { + System.err.println("Missing command."); + usageMessage(); + System.exit(1); + } + + try { + executeCommand(fileprops, sql); + } catch (SQLException e) { + System.err.println("Error in executing command. " + e); + System.exit(1); + } + } + + /** + * Hidden constructor. + */ + private JdbcDBCli() { + super(); + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/JdbcDBClient.java b/jdbc/src/main/java/site/ycsb/db/JdbcDBClient.java new file mode 100644 index 0000000..c3521b4 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/JdbcDBClient.java @@ -0,0 +1,761 @@ +/** + * Copyright (c) 2010 - 2016 Yahoo! Inc., 2016, 2019 YCSB contributors. All rights reserved. + * Copyright 2023-2024 benchANT GmbH. 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 site.ycsb.db; + +import site.ycsb.DB; +import site.ycsb.DBException; +import site.ycsb.IndexableDB; +import site.ycsb.ByteIterator; +import site.ycsb.Status; +import site.ycsb.StringByteIterator; + +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import site.ycsb.db.flavors.DBFlavor; +import site.ycsb.workloads.core.CoreConstants; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +import static site.ycsb.db.JdbcDBConstants.*; + +/** + * A class that wraps a JDBC compliant database to allow it to be interfaced + * with YCSB. This class extends {@link DB} and implements the database + * interface used by YCSB client. + * + *
+ * Each client will have its own instance of this class. This client is not + * thread safe. + * + *
+ * This interface expects a schema ... All + * attributes are of type TEXT. All accesses are through the primary key. + * Therefore, only one index on the primary key is needed. + */ +public class JdbcDBClient extends DB implements IndexableDB { + + static enum TableInitStatus { + ERROR, + COMPLETE, + VOID, + } + private static volatile TableInitStatus tableInitStatus = TableInitStatus.VOID; + /** SQL:2008 standard: FETCH FIRST n ROWS after the ORDER BY. */ + private boolean sqlansiScans = false; + /** SQL Server before 2012: TOP n after the SELECT. */ + private boolean sqlserverScans = false; + + private List conns; + private boolean initialized = false; + private Properties props; + private int jdbcFetchSize; + private int batchSize; + private boolean droptable; + private boolean autoCommit; + private boolean batchUpdates; + private static final String DEFAULT_PROP = ""; + private ConcurrentMap cachedStatements; + private long numRowsInBatch = 0; + /** DB flavor defines DB-specific syntax and behavior for the + * particular database. Current database flavors are: {default, phoenix} */ + private DBFlavor dbFlavor; + private static boolean useTypedFields; + static boolean debug = false; + /** + * Ordered field information for insert and update statements. + * only used for untyped version of this driver + */ + private static class OrderedFieldInfo { + private String fieldKeys; + private List fieldValues; + + OrderedFieldInfo(String fieldKeys, List fieldValues) { + this.fieldKeys = fieldKeys; + this.fieldValues = fieldValues; + } + + String getFieldKeys() { + return fieldKeys; + } + + List getFieldValues() { + return fieldValues; + } + } + + /** + * For the given key, returns what shard contains data for this key. + * + * @param key Data key to do operation on + * @return Shard index + */ + private int getShardIndexByKey(String key) { + int ret = Math.abs(key.hashCode()) % conns.size(); + return ret; + } + + /** + * For the given key, returns Connection object that holds connection to the + * shard that contains this key. + * + * @param key Data key to get information for + * @return Connection object + */ + private Connection getShardConnectionByKey(String key) { + return conns.get(getShardIndexByKey(key)); + } + + private void cleanupAllConnections() throws SQLException { + for (Connection conn : conns) { + if (!autoCommit) { + conn.commit(); + } + conn.close(); + } + } + + /** Returns parsed int value from the properties if set, otherwise returns -1. */ + private static int getIntProperty(Properties props, String key) throws DBException { + String valueStr = props.getProperty(key); + if (valueStr != null) { + try { + return Integer.parseInt(valueStr); + } catch (NumberFormatException nfe) { + System.err.println("Invalid " + key + " specified: " + valueStr); + throw new DBException(nfe); + } + } + return -1; + } + + /** Returns parsed boolean value from the properties if set, otherwise returns defaultVal. */ + private static boolean getBoolProperty(Properties props, String key, boolean defaultVal) { + String valueStr = props.getProperty(key); + if (valueStr != null) { + return Boolean.parseBoolean(valueStr); + } + return defaultVal; + } + + @Override + public void init() throws DBException { + if (initialized) { + System.err.println("Client connection already initialized."); + return; + } + props = getProperties(); + int timeout = Integer.parseInt(props.getProperty(CONNECTION_TIMEOUT_PROPERTY, CONNECTION_TIMEOUT_DEFAULT)); + String urls = props.getProperty(CONNECTION_URL, DEFAULT_PROP); + String user = props.getProperty(CONNECTION_USER, DEFAULT_PROP); + String passwd = props.getProperty(CONNECTION_PASSWD, DEFAULT_PROP); + String driver = props.getProperty(DRIVER_CLASS); + + this.jdbcFetchSize = getIntProperty(props, JDBC_FETCH_SIZE); + this.batchSize = getIntProperty(props, DB_BATCH_SIZE); + + this.autoCommit = getBoolProperty(props, JDBC_AUTO_COMMIT, true); + this.batchUpdates = getBoolProperty(props, JDBC_BATCH_UPDATES, false); + + this.droptable = getBoolProperty(props, JDBC_DROP_TABLE, false); + System.err.println("droptable initialized to " + droptable); + try { +// The SQL Syntax for Scan depends on the DB engine +// - SQL:2008 standard: FETCH FIRST n ROWS after the ORDER BY +// - SQL Server before 2012: TOP n after the SELECT +// - others (MySQL,MariaDB, PostgreSQL before 8.4) +// TODO: check product name and version rather than driver name + if (driver != null) { + if (driver.contains("sqlserver")) { + sqlserverScans = true; + sqlansiScans = false; + } + if (driver.contains("oracle")) { + sqlserverScans = false; + sqlansiScans = true; + } + if (driver.contains("postgres")) { + sqlserverScans = false; + sqlansiScans = true; + } + Class.forName(driver); + } + int shardCount = 0; + conns = new ArrayList(3); + // for a longer explanation see the README.md + // semicolons aren't present in JDBC urls, so we use them to delimit + // multiple JDBC connections to shard across. + final String[] urlArr = urls.split(";"); + for (String url : urlArr) { + System.out.println("Adding shard node URL: " + url); + if(timeout > -1) { + System.out.println("setting timeout to: " + timeout); + DriverManager.setLoginTimeout(timeout); + } + Connection conn = DriverManager.getConnection(url, user, passwd); + + // Since there is no explicit commit method in the DB interface, all + // operations should auto commit, except when explicitly told not to + // (this is necessary in cases such as for PostgreSQL when running a + // scan workload with fetchSize) + conn.setAutoCommit(autoCommit); + + shardCount++; + conns.add(conn); + } + + System.out.println("Using shards: " + shardCount + ", batchSize:" + batchSize + ", fetchSize: " + jdbcFetchSize); + + cachedStatements = new ConcurrentHashMap(); + + this.dbFlavor = DBFlavor.fromJdbcUrl(urlArr[0]); + } catch (ClassNotFoundException e) { + System.err.println("Error in initializing the JDBS driver: " + e); + throw new DBException(e); + } catch (SQLException e) { + System.err.println("Error in database operation: " + e); + throw new DBException(e); + } catch (NumberFormatException e) { + System.err.println("Invalid value for fieldcount property. " + e); + throw new DBException(e); + } + boolean initDb = "true".equalsIgnoreCase(props.getProperty(JDBC_INIT_TABLE, "false")); + boolean createIndexes = "true".equalsIgnoreCase(props.getProperty(JDBC_INIT_INDEXES, "true")); + useTypedFields = "true".equalsIgnoreCase(props.getProperty(TYPED_FIELDS_PROPERTY)); + List indexes = JdbcDBInitHelper.getIndexList(getProperties()); + final String table = props.getProperty(CoreConstants.TABLENAME_PROPERTY, CoreConstants.TABLENAME_PROPERTY_DEFAULT); + synchronized(JdbcDBClient.class) { + debug = Boolean.parseBoolean(getProperties().getProperty("debug", "false")); + if(tableInitStatus == TableInitStatus.ERROR) { + throw new IllegalStateException("table initialization or index creation failed, terminating"); + } + if(tableInitStatus == TableInitStatus.VOID) { + // it is our task to init the table + try { + if(droptable) { + System.err.println("dropping table, because droptable is " + droptable); + JdbcDBInitHelper.dropTable(table, conns); + } + if(initDb) { + System.err.println("initializing table, because initDb is " + initDb); + dbFlavor.createDbAndSchema(table, conns); + } + if(createIndexes) { + // build indexes only once, but once per connection + List indexCommands = this.dbFlavor.buildIndexCommands(table, indexes); + for(Connection c : conns) { + for(String cmd : indexCommands) { + System.out.println("creating index started: " + cmd); + Statement stmt = c.createStatement(); + int ret = stmt.executeUpdate(cmd); + System.out.println("creating index done: " + ret); + } + } + } + tableInitStatus = TableInitStatus.COMPLETE; + } catch(SQLException ex) { + tableInitStatus = TableInitStatus.ERROR; + throw new IllegalStateException("could not create table or indexes, terminating", ex); + } + } + } + initialized = true; + } + + + @Override + public void cleanup() throws DBException { + if (batchSize > 0) { + try { + // commit un-finished batches + for (PreparedStatement st : cachedStatements.values()) { + if (!st.getConnection().isClosed() && !st.isClosed() && (numRowsInBatch % batchSize != 0)) { + st.executeBatch(); + } + } + } catch (SQLException e) { + System.err.println("Error in cleanup execution. " + e); + throw new DBException(e); + } + } + + try { + cleanupAllConnections(); + } catch (SQLException e) { + System.err.println("Error in closing the connection. " + e); + throw new DBException(e); + } + } + + private PreparedStatement createAndCacheInsertStatement(StatementType insertType, String key) + throws SQLException { + String insert = dbFlavor.createInsertStatement(insertType, key); + PreparedStatement insertStatement = getShardConnectionByKey(key).prepareStatement(insert); + PreparedStatement stmt = cachedStatements.putIfAbsent(insertType, insertStatement); + if (stmt == null) { + return insertStatement; + } + return stmt; + } + + private PreparedStatement createAndCacheReadStatement(StatementType readType, String key) + throws SQLException { + String read = dbFlavor.createReadStatement(readType, key); + PreparedStatement readStatement = getShardConnectionByKey(key).prepareStatement(read); + PreparedStatement stmt = cachedStatements.putIfAbsent(readType, readStatement); + if (stmt == null) { + return readStatement; + } + return stmt; + } + + private final ConcurrentHashMap UPDATE_ONE_STATEMENTS = new ConcurrentHashMap<>(); + static final class UpdateContainer { + final List filters; + final Set field; + public UpdateContainer(List filters, Set field) { + this.filters = filters; + this.field = field; + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((filters == null) ? 0 : filters.hashCode()); + result = prime * result + ((field == null) ? 0 : field.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + UpdateContainer other = (UpdateContainer) obj; + if (filters == null) { + if (other.filters != null) + return false; + } else if (!filters.equals(other.filters)) + return false; + if (field == null) { + if (other.field != null) + return false; + } else if (!field.equals(other.field)) + return false; + return true; + } + + } + private PreparedStatement createAndCacheUpdateOneStatement( + String tablename, List filters, List fields) + throws SQLException { + final List normalizedFilters = new ArrayList<>(); + for(Comparison c : filters) { + normalizedFilters.add(c.normalized()); + } + final Set updates = new HashSet<>(); + for(DatabaseField f: fields) { + if(f.getContent().isTerminal()) { + updates.add(f.getFieldname()); + } else { + throw new IllegalStateException("non-terminals not supported"); + } + } + UpdateContainer container = new UpdateContainer(normalizedFilters, updates); + PreparedStatement ret = UPDATE_ONE_STATEMENTS.get(container); + if(ret != null) return ret; + if(debug){ + System.err.println("updateOne query not found: creating it"); + } + String query = dbFlavor.createUpdateOneStatement(tablename, filters, fields); + synchronized(UPDATE_ONE_STATEMENTS) { + ret = UPDATE_ONE_STATEMENTS.get(container); + if(ret != null) return ret; + PreparedStatement prepStatement = conns.get(0).prepareStatement(query); + ret = UPDATE_ONE_STATEMENTS.putIfAbsent(container, prepStatement); + if (ret == null) { + ret = prepStatement; + } + } + return ret; + } + + private PreparedStatement createAndCacheDeleteStatement(StatementType deleteType, String key) + throws SQLException { + String delete = dbFlavor.createDeleteStatement(deleteType, key); + PreparedStatement deleteStatement = getShardConnectionByKey(key).prepareStatement(delete); + PreparedStatement stmt = cachedStatements.putIfAbsent(deleteType, deleteStatement); + if (stmt == null) { + return deleteStatement; + } + return stmt; + } + + private final ConcurrentHashMap, PreparedStatement> FIND_ONE_STATEMENTS = new ConcurrentHashMap<>(); + private PreparedStatement createAndCacheFindOneStatement( + String tablename, List filters, Set fields) + throws SQLException { + if(fields != null) { + throw new IllegalStateException("reading specific fields currently not supported by this driver"); + } + final List normalizedFilters = new ArrayList<>(); + for(Comparison c : filters) { + normalizedFilters.add(c.normalized()); + } + PreparedStatement ret = FIND_ONE_STATEMENTS.get(normalizedFilters); + if(ret != null) return ret; + if(debug){ + System.err.println("findOne query not found: creating it"); + } + String query = JdbcQueryHelper.createFindOneStatement(tablename, filters, fields); + synchronized(FIND_ONE_STATEMENTS) { + ret = FIND_ONE_STATEMENTS.get(normalizedFilters); + if(ret != null) return ret; + PreparedStatement prepStatement = conns.get(0).prepareStatement(query); + ret = FIND_ONE_STATEMENTS.putIfAbsent(normalizedFilters, prepStatement); + if (ret == null) { + ret = prepStatement; + } + } + return ret; + } + + private PreparedStatement createAndCacheUpdateStatement(StatementType updateType, String key) + throws SQLException { + String update = dbFlavor.createUpdateStatement(updateType, key); + PreparedStatement insertStatement = getShardConnectionByKey(key).prepareStatement(update); + PreparedStatement stmt = cachedStatements.putIfAbsent(updateType, insertStatement); + if (stmt == null) { + return insertStatement; + } + return stmt; + } + + private PreparedStatement createAndCacheScanStatement(StatementType scanType, String key) + throws SQLException { + String select = dbFlavor.createScanStatement(scanType, key, sqlserverScans, sqlansiScans); + PreparedStatement scanStatement = getShardConnectionByKey(key).prepareStatement(select); + if (this.jdbcFetchSize > 0) { + scanStatement.setFetchSize(this.jdbcFetchSize); + } + PreparedStatement stmt = cachedStatements.putIfAbsent(scanType, scanStatement); + if (stmt == null) { + return scanStatement; + } + return stmt; + } + + @Override + public Status read(String tableName, String key, Set fields, Map result) { + try { + StatementType type = new StatementType(StatementType.Type.READ, tableName, 1, "", getShardIndexByKey(key)); + PreparedStatement readStatement = cachedStatements.get(type); + if (readStatement == null) { + readStatement = createAndCacheReadStatement(type, key); + } + readStatement.setString(1, key); + ResultSet resultSet = readStatement.executeQuery(); + if (!resultSet.next()) { + resultSet.close(); + return Status.NOT_FOUND; + } + if (result != null && fields != null) { + for (String field : fields) { + String value = resultSet.getString(field); + result.put(field, new StringByteIterator(value)); + } + } + resultSet.close(); + return Status.OK; + } catch (SQLException e) { + System.err.println("Error in processing read of table " + tableName + ": " + e); + return Status.ERROR; + } + } + + @Override + public Status scan(String tableName, String startKey, int recordcount, Set fields, + Vector> result) { + try { + StatementType type = new StatementType(StatementType.Type.SCAN, tableName, 1, "", getShardIndexByKey(startKey)); + PreparedStatement scanStatement = cachedStatements.get(type); + if (scanStatement == null) { + scanStatement = createAndCacheScanStatement(type, startKey); + } + // SQL Server TOP syntax is at first + if (sqlserverScans) { + scanStatement.setInt(1, recordcount); + scanStatement.setString(2, startKey); + // FETCH FIRST and LIMIT are at the end + } else { + scanStatement.setString(1, startKey); + scanStatement.setInt(2, recordcount); + } + ResultSet resultSet = scanStatement.executeQuery(); + for (int i = 0; i < recordcount && resultSet.next(); i++) { + if (result != null && fields != null) { + HashMap values = new HashMap(); + for (String field : fields) { + String value = resultSet.getString(field); + values.put(field, new StringByteIterator(value)); + } + result.add(values); + } + } + resultSet.close(); + return Status.OK; + } catch (SQLException e) { + System.err.println("Error in processing scan of table: " + tableName + e); + return Status.ERROR; + } + } + + @Override + public Status update(String tableName, String key, Map values) { + try { + int numFields = values.size(); + OrderedFieldInfo fieldInfo = getFieldInfo(values); + StatementType type = new StatementType(StatementType.Type.UPDATE, tableName, + numFields, fieldInfo.getFieldKeys(), getShardIndexByKey(key)); + PreparedStatement updateStatement = cachedStatements.get(type); + if (updateStatement == null) { + updateStatement = createAndCacheUpdateStatement(type, key); + } + int index = 1; + for (String value: fieldInfo.getFieldValues()) { + updateStatement.setString(index++, value); + } + updateStatement.setString(index, key); + int result = updateStatement.executeUpdate(); + if (result == 1) { + return Status.OK; + } + return Status.UNEXPECTED_STATE; + } catch (SQLException e) { + System.err.println("Error in processing update to table: " + tableName + e); + return Status.ERROR; + } + } + private void buildTypedQuery(PreparedStatement insertStatement, List values) throws SQLException { + int index = 1; + for (DatabaseField value: values) { + // increase index count immediately + // as index 1 has already been taken + index++; + DataWrapper w = value.getContent(); + if(w.isTerminal()) { + if(w.isInteger()) { + insertStatement.setInt(index, w.asInteger()); + } else if(w.isLong()) { + insertStatement.setLong(index, w.asLong()); + } else if(w.isString()) { + insertStatement.setString(index, w.asString()); + } else { + // assuming this is an iterator + // which is the only remaining terminal + Object o = w.asObject(); + byte[] b = (byte[]) o; + insertStatement.setString(index, new String(b)); + } + } else if(w.isArray()) { + throw new UnsupportedOperationException("array type columns not supported (yet)"); + } else if(w.isNested()) { + throw new UnsupportedOperationException("nested type columns not supported (yet)"); + } else { + // do nothing; something broke that we ignore for now + } + } + } + private void buildLegacyQuery(PreparedStatement insertStatement, List values) throws SQLException { + int index = 2; + for (DatabaseField value: values) { + String v = value.getContent().asIterator().toString(); + insertStatement.setString(index++, v); + } + } + @Override + public Status insert(String tableName, String key, List values) { + try { + int numFields = values.size(); + String fieldNameString = getFieldNameString(values); + StatementType type = new StatementType(StatementType.Type.INSERT, tableName, + numFields, fieldNameString, getShardIndexByKey(key)); + PreparedStatement insertStatement = cachedStatements.get(type); + if (insertStatement == null) { + insertStatement = createAndCacheInsertStatement(type, key); + } + insertStatement.setString(1, key); + + if(useTypedFields) { + buildTypedQuery(insertStatement, values); + } else { + buildLegacyQuery(insertStatement, values); + } + // Using the batch insert API + if (batchUpdates) { + insertStatement.addBatch(); + // Check for a sane batch size + if (batchSize > 0) { + // Commit the batch after it grows beyond the configured size + if (++numRowsInBatch % batchSize == 0) { + int[] results = insertStatement.executeBatch(); + for (int r : results) { + // Acceptable values are 1 and SUCCESS_NO_INFO (-2) from reWriteBatchedInserts=true + if (r != 1 && r != -2) { + return Status.ERROR; + } + } + // If autoCommit is off, make sure we commit the batch + if (!autoCommit) { + getShardConnectionByKey(key).commit(); + } + return Status.OK; + } // else, the default value of -1 or a nonsense. Treat it as an infinitely large batch. + } // else, we let the batch accumulate + // Added element to the batch, potentially committing the batch too. + return Status.BATCHED_OK; + } else { + // Normal update + int result = insertStatement.executeUpdate(); + // If we are not autoCommit, we might have to commit now + if (!autoCommit) { + // Let updates be batcher locally + if (batchSize > 0) { + if (++numRowsInBatch % batchSize == 0) { + // Send the batch of updates + getShardConnectionByKey(key).commit(); + } + // uhh + return Status.OK; + } else { + // Commit each update + getShardConnectionByKey(key).commit(); + } + } + if (result == 1) { + return Status.OK; + } + } + return Status.UNEXPECTED_STATE; + } catch (SQLException e) { + System.err.println("Error in processing insert to table: " + tableName + e); + return Status.ERROR; + } + } + + @Override + public Status delete(String tableName, String key) { + try { + StatementType type = new StatementType(StatementType.Type.DELETE, tableName, 1, "", getShardIndexByKey(key)); + PreparedStatement deleteStatement = cachedStatements.get(type); + if (deleteStatement == null) { + deleteStatement = createAndCacheDeleteStatement(type, key); + } + deleteStatement.setString(1, key); + int result = deleteStatement.executeUpdate(); + if (result == 1) { + return Status.OK; + } + if(result == 0) { + return Status.NOT_FOUND; + } + return Status.UNEXPECTED_STATE; + } catch (SQLException e) { + System.err.println("Error in processing delete to table: " + tableName + e); + return Status.ERROR; + } + } + private String getFieldNameString(List values) { + List els = new ArrayList<>(); + values.forEach(v -> els.add(v.getFieldname())); + return String.join(",", els); + } + + private OrderedFieldInfo getFieldInfo(Map values) { + String fieldKeys = ""; + List fieldValues = new ArrayList<>(); + int count = 0; + for (Map.Entry entry : values.entrySet()) { + fieldKeys += entry.getKey(); + if (count < values.size() - 1) { + fieldKeys += ","; + } + fieldValues.add(count, entry.getValue().toString()); + count++; + } + + return new OrderedFieldInfo(fieldKeys, fieldValues); + } +@Override + public Status findOne(String tableName, List filters, Set fields, + Map result) { + if(!useTypedFields) { + throw new IllegalStateException("only works with typed fields"); + } + if(fields != null) { + throw new IllegalStateException("reading specific fields currently not supported by this driver"); + } + try { + PreparedStatement statement = createAndCacheFindOneStatement(tableName, filters, fields); + JdbcQueryHelper.bindFindOneStatement(statement, filters, fields); + ResultSet resultSet = statement.executeQuery(); + if (!resultSet.next()) { + resultSet.close(); + return Status.NOT_FOUND; + } + FilterBuilder.drainTypedResult(resultSet, result); + if(resultSet.next()) { + System.err.println("Error in processing find of table " + tableName + ": too many results"); + resultSet.close(); + return Status.ERROR; + } + resultSet.close(); + return Status.OK; + } catch (SQLException e) { + System.err.println("Error in processing find on table: " + tableName + e); + e.printStackTrace(System.err); + return Status.ERROR; + } + } + @Override + public Status updateOne(String table, List filters, List fields) { + if(!useTypedFields) { + throw new IllegalStateException("only works with typed fields"); + } + try { + PreparedStatement statement = createAndCacheUpdateOneStatement(table, filters, fields); + JdbcQueryHelper.bindUpdateOneStatement(statement, filters, fields); + int resultSet = statement.executeUpdate(); + if (resultSet == 0) { + return Status.NOT_FOUND; + } + if (resultSet > 1) { + return Status.UNEXPECTED_STATE; + } + return Status.OK; + } catch (SQLException e) { + System.err.println("Error in processing update on table: " + table + e); + return Status.ERROR; + } + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/JdbcDBConstants.java b/jdbc/src/main/java/site/ycsb/db/JdbcDBConstants.java new file mode 100644 index 0000000..b863672 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/JdbcDBConstants.java @@ -0,0 +1,61 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db; + +public final class JdbcDBConstants { + + public static final String INDEX_LIST_PROPERTY = "jdbc.indexlist"; + /** The class to use as the jdbc driver. */ + public static final String DRIVER_CLASS = "db.driver"; + + public static final String CONNECTION_TIMEOUT_PROPERTY = "db.connectionTimeout"; + public static final String CONNECTION_TIMEOUT_DEFAULT = "-1"; + + /** The URL to connect to the database. */ + public static final String CONNECTION_URL = "db.url"; + + /** The user name to use to connect to the database. */ + public static final String CONNECTION_USER = "db.user"; + + /** The password to use for establishing the connection. */ + public static final String CONNECTION_PASSWD = "db.passwd"; + + /** The batch size for batched inserts. Set to >0 to use batching */ + public static final String DB_BATCH_SIZE = "db.batchsize"; + + /** The JDBC fetch size hinted to the driver. */ + public static final String JDBC_FETCH_SIZE = "jdbc.fetchsize"; + + /** The JDBC fetch size hinted to the driver. */ + public static final String JDBC_INIT_TABLE = "jdbc.inittable"; + + /** Attempt to create indexes as specified. */ + public static final String JDBC_INIT_INDEXES = "jdbc.initindexes"; + + /** The JDBC connection auto-commit property for the driver. */ + public static final String JDBC_AUTO_COMMIT = "jdbc.autocommit"; + + public static final String JDBC_BATCH_UPDATES = "jdbc.batchupdateapi"; + + /** The primary key in the user table. */ + public static final String PRIMARY_KEY = "YCSB_KEY"; + + /** Clear database before re-initializing */ + public static final String JDBC_DROP_TABLE = "jdbc.droptable"; + + private JdbcDBConstants() { + // no instances + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/JdbcDBCreateTable.java b/jdbc/src/main/java/site/ycsb/db/JdbcDBCreateTable.java new file mode 100644 index 0000000..712ea4d --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/JdbcDBCreateTable.java @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2010 - 2016 Yahoo! Inc. All rights reserved. + * Copyright 2023-2024 benchANT GmbH. 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 site.ycsb.db; + +import java.io.FileInputStream; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Enumeration; +import java.util.Properties; + +/** + * Utility class to create the table to be used by the benchmark. + * + * @author sudipto + */ +public final class JdbcDBCreateTable { + + + /** The name of the property for the number of fields in a record. */ + public static final String FIELD_COUNT_PROPERTY = "fieldcount"; + + /** Default number of fields in a record. */ + public static final String FIELD_COUNT_PROPERTY_DEFAULT = "10"; + + private static void usageMessage() { + System.out.println("Create Table Client. Options:"); + System.out.println(" -p key=value properties defined."); + System.out.println(" -P location of the properties file to load."); + System.out.println(" -n name of the table."); + System.out.println(" -f number of fields (default 10)."); + } + + private static void createTable(Properties props, String tablename) throws SQLException { + String driver = props.getProperty(JdbcDBConstants.DRIVER_CLASS); + String username = props.getProperty(JdbcDBConstants.CONNECTION_USER); + String password = props.getProperty(JdbcDBConstants.CONNECTION_PASSWD, ""); + String url = props.getProperty(JdbcDBConstants.CONNECTION_URL); + int fieldcount = Integer.parseInt(props.getProperty(FIELD_COUNT_PROPERTY, + FIELD_COUNT_PROPERTY_DEFAULT)); + + if (driver == null || username == null || url == null) { + throw new SQLException("Missing connection information."); + } + + Connection conn = null; + + try { + Class.forName(driver); + + conn = DriverManager.getConnection(url, username, password); + Statement stmt = conn.createStatement(); + + StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS "); + sql.append(tablename); + sql.append(";"); + + stmt.execute(sql.toString()); + + sql = new StringBuilder("CREATE TABLE "); + sql.append(tablename); + sql.append(" (YCSB_KEY VARCHAR PRIMARY KEY"); + + for (int idx = 0; idx < fieldcount; idx++) { + sql.append(", FIELD"); + sql.append(idx); + sql.append(" TEXT"); + } + sql.append(");"); + + stmt.execute(sql.toString()); + + System.out.println("Table " + tablename + " created.."); + } catch (ClassNotFoundException e) { + throw new SQLException("JDBC Driver class not found."); + } finally { + if (conn != null) { + System.out.println("Closing database connection."); + conn.close(); + } + } + } + + /** + * @param args + */ + public static void main(String[] args) { + + if (args.length == 0) { + usageMessage(); + System.exit(0); + } + + String tablename = null; + int fieldcount = -1; + Properties props = new Properties(); + Properties fileprops = new Properties(); + + // parse arguments + int argindex = 0; + while (args[argindex].startsWith("-")) { + if (args[argindex].compareTo("-P") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + String propfile = args[argindex]; + argindex++; + + Properties myfileprops = new Properties(); + try { + myfileprops.load(new FileInputStream(propfile)); + } catch (IOException e) { + System.out.println(e.getMessage()); + System.exit(0); + } + + // Issue #5 - remove call to stringPropertyNames to make compilable + // under Java 1.5 + for (Enumeration e = myfileprops.propertyNames(); e.hasMoreElements();) { + String prop = (String) e.nextElement(); + + fileprops.setProperty(prop, myfileprops.getProperty(prop)); + } + + } else if (args[argindex].compareTo("-p") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + int eq = args[argindex].indexOf('='); + if (eq < 0) { + usageMessage(); + System.exit(0); + } + + String name = args[argindex].substring(0, eq); + String value = args[argindex].substring(eq + 1); + props.put(name, value); + argindex++; + } else if (args[argindex].compareTo("-n") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + tablename = args[argindex++]; + } else if (args[argindex].compareTo("-f") == 0) { + argindex++; + if (argindex >= args.length) { + usageMessage(); + System.exit(0); + } + try { + fieldcount = Integer.parseInt(args[argindex++]); + } catch (NumberFormatException e) { + System.err.println("Invalid number for field count"); + usageMessage(); + System.exit(1); + } + } else { + System.out.println("Unknown option " + args[argindex]); + usageMessage(); + System.exit(0); + } + + if (argindex >= args.length) { + break; + } + } + + if (argindex != args.length) { + usageMessage(); + System.exit(0); + } + + // overwrite file properties with properties from the command line + + // Issue #5 - remove call to stringPropertyNames to make compilable under + // Java 1.5 + for (Enumeration e = props.propertyNames(); e.hasMoreElements();) { + String prop = (String) e.nextElement(); + + fileprops.setProperty(prop, props.getProperty(prop)); + } + + props = fileprops; + + if (tablename == null) { + System.err.println("table name missing."); + usageMessage(); + System.exit(1); + } + + if (fieldcount > 0) { + props.setProperty(FIELD_COUNT_PROPERTY, String.valueOf(fieldcount)); + } + + try { + createTable(props, tablename); + } catch (SQLException e) { + System.err.println("Error in creating table. " + e); + System.exit(1); + } + } + + /** + * Hidden constructor. + */ + private JdbcDBCreateTable() { + super(); + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/JdbcDBInitHelper.java b/jdbc/src/main/java/site/ycsb/db/JdbcDBInitHelper.java new file mode 100644 index 0000000..9a87e61 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/JdbcDBInitHelper.java @@ -0,0 +1,152 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import org.apache.htrace.shaded.fasterxml.jackson.databind.JsonNode; +import org.apache.htrace.shaded.fasterxml.jackson.databind.ObjectMapper; +import org.apache.htrace.shaded.fasterxml.jackson.databind.node.ArrayNode; +import org.apache.htrace.shaded.fasterxml.jackson.databind.node.BooleanNode; +import org.apache.htrace.shaded.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.htrace.shaded.fasterxml.jackson.databind.node.TextNode; + +import site.ycsb.workloads.schema.SchemaHolder; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumn; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnKind; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnType; + +public final class JdbcDBInitHelper { + + private static final SchemaHolder schema = SchemaHolder.INSTANCE; + + public static void createTable(String tableName, List conns, String textKeyType) throws SQLException { + final StringBuilder b = new StringBuilder("CREATE TABLE "); + b.append(tableName).append(" ( "); + // this is YCSB nonetheless, so we need an ID + b.append( JdbcDBConstants.PRIMARY_KEY).append(" " + textKeyType + " PRIMARY KEY"); + for(SchemaColumn column : schema.getOrderedListOfColumns()) { + if(column.getColumnKind() == SchemaColumnKind.SCALAR) { + b.append(", ").append(column.getColumnName()).append(" "); + if(column.getColumnType() == SchemaColumnType.TEXT) b.append(" " + textKeyType + " "); + else if( column.getColumnType() == SchemaColumnType.INT) b.append(" INTEGER "); + else if( column.getColumnType() == SchemaColumnType.LONG) b.append(" BIGINT "); + } else if(column.getColumnKind() == SchemaColumnKind.ARRAY) { + throw new SQLException("array column types currently not supported"); + } else if(column.getColumnKind() == SchemaColumnKind.NESTED) { + throw new SQLException("nested column types currently not supported"); + } else { + throw new SQLException("other column types currently not supported"); + } + } + b.append(")"); + String sql = b.toString(); + for(Connection c : conns) { + Statement stmt = c.createStatement(); + stmt.executeUpdate(sql); + } + } + + public static void dropTable(String tableName, List conns) throws SQLException { + System.err.println("droppting table"); + String dropper = "DROP TABLE " + tableName; + for(Connection c : conns) { + Statement stmt = c.createStatement(); + stmt.executeUpdate(dropper); + } + } + + static List getIndexList(Properties props) { + String indexeslist = props.getProperty(JdbcDBConstants.INDEX_LIST_PROPERTY); + if(indexeslist == null) { + return Collections.emptyList(); + } + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = null; + try { + root = mapper.readTree(indexeslist); + } catch(IOException ex) { + throw new RuntimeException(ex); + } + if(!root.isArray()) { + throw new IllegalArgumentException("index specification must be a JSON array"); + } + ArrayNode array = (ArrayNode) root; + if(array.size() == 0) { + return Collections.emptyList(); + } + List retVal = new ArrayList<>(); + for(int i = 0; i < array.size(); i++) { + JsonNode el = array.get(i); + if(!el.isObject()) { + throw new IllegalArgumentException("index elements must be a JSON object"); + } + + IndexDescriptor desc = new IndexDescriptor(); + ObjectNode object = (ObjectNode) el; + JsonNode name = object.get("name"); + if(name == null || !name.isTextual()) { + throw new IllegalArgumentException("index elements must be a JSON object with 'name' of type string"); + } + desc.name = ((TextNode) name).asText(); + + JsonNode conc = object.get("concurrent"); + if(conc == null || !conc.isBoolean()) { + desc.concurrent = false; + } else { + desc.concurrent = ((BooleanNode) conc).asBoolean(); + } + + JsonNode type = object.get("method"); + if(type == null || !type.isTextual()) { + desc.method = "btree"; + } else { + desc.method = ((TextNode) type).asText(); + } + + JsonNode order = object.get("order"); + if(order == null || !order.isTextual()) { + desc.order = "ASC"; + } else { + desc.order = ((TextNode) order).asText(); + } + JsonNode sorts = object.get("columns"); + if(sorts == null || !sorts.isArray()) { + throw new IllegalArgumentException("index elements must be a JSON object with 'columns' set as an array of strings"); + } + ArrayNode columnsArray = (ArrayNode) sorts; + for(int j = 0; j < columnsArray.size(); j++) { + JsonNode sortEl = columnsArray.get(j); + if(sortEl == null || !sortEl.isTextual()) { + throw new IllegalArgumentException("index elements must be a JSON object with 'sortAttributes' set as an array of strings"); + } + desc.columnNames.add(((TextNode) sortEl).asText()); + } + retVal.add(desc); + } + return retVal; + } + + private JdbcDBInitHelper() { + // private constructor + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/JdbcQueryHelper.java b/jdbc/src/main/java/site/ycsb/db/JdbcQueryHelper.java new file mode 100644 index 0000000..c018b46 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/JdbcQueryHelper.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Set; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +public final class JdbcQueryHelper { + + static int bindFindOneStatement(PreparedStatement statement, List filters, Set fields) throws SQLException { + int currentIndex = 1; + if(fields != null) { + throw new IllegalStateException("reading specific fields currently not supported by this driver"); + } + return FilterBuilder.bindFilterValues(statement, currentIndex, filters); + } + + static String createFindOneStatement(String tablename, List filters, Set fields) { + if(fields != null) { + throw new IllegalStateException("reading specific fields currently not supported by this driver"); + } + final String template = "SELECT * FROM %1$s WHERE %2$s LIMIT 1"; + String filterString = FilterBuilder.buildConcatenatedPlaceholderFilter(filters); + return String.format(template, tablename, filterString); + } + + static int bindUpdateOneStatement(PreparedStatement statement, List filters, List fields) throws SQLException { + int currentIndex = 1; + currentIndex = FilterBuilder.bindConcatenatedSet(statement, currentIndex, fields); + return FilterBuilder.bindFilterValues(statement, currentIndex, filters); + } + + private JdbcQueryHelper() { + + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/StatementType.java b/jdbc/src/main/java/site/ycsb/db/StatementType.java new file mode 100644 index 0000000..95700ed --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/StatementType.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010 Yahoo! Inc., 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 site.ycsb.db; + +/** + * The statement type for the prepared statements. + */ +public class StatementType { + + enum Type { + INSERT(1), DELETE(2), READ(3), UPDATE(4), SCAN(5); + + private final int internalType; + + private Type(int type) { + internalType = type; + } + + int getHashCode() { + final int prime = 31; + int result = 1; + result = prime * result + internalType; + return result; + } + } + + private Type type; + private int shardIndex; + private int numFields; + private String tableName; + private String fieldString; + + public StatementType(Type type, String tableName, int numFields, String fieldString, int shardIndex) { + this.type = type; + this.tableName = tableName; + this.numFields = numFields; + this.fieldString = fieldString; + this.shardIndex = shardIndex; + } + + public String getTableName() { + return tableName; + } + + public String getFieldString() { + return fieldString; + } + + public int getNumFields() { + return numFields; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + numFields + 100 * shardIndex; + result = prime * result + ((tableName == null) ? 0 : tableName.hashCode()); + result = prime * result + ((type == null) ? 0 : type.getHashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + StatementType other = (StatementType) obj; + if (numFields != other.numFields) { + return false; + } + if (shardIndex != other.shardIndex) { + return false; + } + if (tableName == null) { + if (other.tableName != null) { + return false; + } + } else if (!tableName.equals(other.tableName)) { + return false; + } + if (type != other.type) { + return false; + } + if (!fieldString.equals(other.fieldString)) { + return false; + } + return true; + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/flavors/DBFlavor.java b/jdbc/src/main/java/site/ycsb/db/flavors/DBFlavor.java new file mode 100644 index 0000000..f22c06d --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/flavors/DBFlavor.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2016, 2019 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 site.ycsb.db.flavors; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import site.ycsb.db.IndexDescriptor; +import site.ycsb.db.StatementType; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +/** + * DBFlavor captures minor differences in syntax and behavior among JDBC implementations and SQL + * dialects. This class also acts as a factory to instantiate concrete flavors based on the JDBC URL. + */ +public abstract class DBFlavor { + + enum DBName { + DEFAULT, + MYSQL, + PHOENIX + } + + private final DBName dbName; + + public DBFlavor(DBName dbName) { + this.dbName = dbName; + } + + public static DBFlavor fromJdbcUrl(String url) { + if (url.startsWith("jdbc:phoenix")) { + System.err.println("DBFlavor: using PhoenixFlavor"); + return new PhoenixDBFlavor(); + } + if(url.startsWith("jdbc:mysql")) { + System.err.println("DBFlavor: using MySqlFlavor"); + return new MySqlFlavor(); + } + System.err.println("DBFlavor: using default flavor"); + return new DefaultDBFlavor(); + } + + public abstract void createDbAndSchema(String tableName, List conns) throws SQLException; + + /** + * Create and return a SQL statement for inserting data. + */ + public abstract String createInsertStatement(StatementType insertType, String key); + + /** + * Create and return a SQL statement for reading data. + */ + public abstract String createReadStatement(StatementType readType, String key); + + /** + * Create and return a SQL statement for deleting data. + */ + public abstract String createDeleteStatement(StatementType deleteType, String key); + + /** + * Create and return a SQL statement for updating data. + */ + public abstract String createUpdateStatement(StatementType updateType, String key); + + /** + * Create and return a SQL statement for scanning data. + */ + public abstract String createScanStatement(StatementType scanType, String key, + boolean sqlserverScans, boolean sqlansiScans); + + /* Create and return an SQL statement for updating one element */ + public abstract String createUpdateOneStatement(String tablename, List filters, List fields); + /** + * Create SQL statements for indexes + */ + public abstract List buildIndexCommands(String table, List indexes); +} diff --git a/jdbc/src/main/java/site/ycsb/db/flavors/DefaultDBFlavor.java b/jdbc/src/main/java/site/ycsb/db/flavors/DefaultDBFlavor.java new file mode 100644 index 0000000..630c108 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/flavors/DefaultDBFlavor.java @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2016, 2019 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 site.ycsb.db.flavors; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import site.ycsb.db.JdbcDBConstants; +import site.ycsb.db.JdbcDBInitHelper; +import site.ycsb.db.StatementType; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; +import site.ycsb.db.FilterBuilder; +import site.ycsb.db.IndexDescriptor; + +/** + * A default flavor for relational databases. + */ +public class DefaultDBFlavor extends DBFlavor { + public DefaultDBFlavor() { + super(DBName.DEFAULT); + } + public DefaultDBFlavor(DBName dbName) { + super(dbName); + } + + @Override + public void createDbAndSchema(String tableName, List conns) throws SQLException { + JdbcDBInitHelper.createTable(tableName, conns, "TEXT"); + } + + @Override + public String createInsertStatement(StatementType insertType, String key) { + StringBuilder insert = new StringBuilder("INSERT INTO "); + insert.append(insertType.getTableName()); + insert.append(" (" + JdbcDBConstants.PRIMARY_KEY + "," + insertType.getFieldString() + ")"); + insert.append(" VALUES(?"); + for (int i = 0; i < insertType.getNumFields(); i++) { + insert.append(",?"); + } + insert.append(")"); + return insert.toString(); + } + + @Override + public String createReadStatement(StatementType readType, String key) { + StringBuilder read = new StringBuilder("SELECT * FROM "); + read.append(readType.getTableName()); + read.append(" WHERE "); + read.append(JdbcDBConstants.PRIMARY_KEY); + read.append(" = "); + read.append("?"); + return read.toString(); + } + + @Override + public String createDeleteStatement(StatementType deleteType, String key) { + StringBuilder delete = new StringBuilder("DELETE FROM "); + delete.append(deleteType.getTableName()); + delete.append(" WHERE "); + delete.append(JdbcDBConstants.PRIMARY_KEY); + delete.append(" = ?"); + return delete.toString(); + } + + @Override + public String createUpdateStatement(StatementType updateType, String key) { + String[] fieldKeys = updateType.getFieldString().split(","); + StringBuilder update = new StringBuilder("UPDATE "); + update.append(updateType.getTableName()); + update.append(" SET "); + for (int i = 0; i < fieldKeys.length; i++) { + update.append(fieldKeys[i]); + update.append("=?"); + if (i < fieldKeys.length - 1) { + update.append(", "); + } + } + update.append(" WHERE "); + update.append(JdbcDBConstants.PRIMARY_KEY); + update.append(" = ?"); + return update.toString(); + } + + @Override + public String createScanStatement(StatementType scanType, String key, boolean sqlserverScans, boolean sqlansiScans) { + StringBuilder select; + if (sqlserverScans) { + select = new StringBuilder("SELECT TOP (?) * FROM "); + } else { + select = new StringBuilder("SELECT * FROM "); + } + select.append(scanType.getTableName()); + select.append(" WHERE "); + select.append(JdbcDBConstants.PRIMARY_KEY); + select.append(" >= ?"); + select.append(" ORDER BY "); + select.append(JdbcDBConstants.PRIMARY_KEY); + if (!sqlserverScans) { + if (sqlansiScans) { + select.append(" FETCH FIRST ? ROWS ONLY"); + } else { + select.append(" LIMIT ?"); + } + } + return select.toString(); + } + + @Override + public String createUpdateOneStatement(String tablename, List filters, List fields) { + final String template = "UPDATE %1$s SET %2$s WHERE %3$s = (%4$s)"; + // https://dba.stackexchange.com/questions/69471/postgres-update-limit-1 + final String innerTemplate = "SELECT %1$s from %2$s WHERE %3$s LIMIT 1 FOR UPDATE SKIP LOCKED"; + + String setString = FilterBuilder.buildConcatenatedPlaceholderSet(fields); + String filterString = FilterBuilder.buildConcatenatedPlaceholderFilter(filters); + final String inner = String.format(innerTemplate, JdbcDBConstants.PRIMARY_KEY, tablename, filterString); + return String.format(template, tablename, setString, JdbcDBConstants.PRIMARY_KEY, inner); + } + + public List buildIndexCommands(String table, List indexes) { + if(indexes.size() == 0) { + return Collections.emptyList(); + } + System.err.println("indexes: " + indexes.get(0).columnNames); + /* + * CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] name ] ON [ ONLY ] table_name [ USING method ] + * ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass [ ( opclass_parameter = value [, ... ] ) ] ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] ) + * [ INCLUDE ( column_name [, ...] ) ] + * [ NULLS [ NOT ] DISTINCT ] + * [ WITH ( storage_parameter [= value] [, ... ] ) ] + * [ TABLESPACE tablespace_name ] + * [ WHERE predicate ] + */ + List indexCommands = new ArrayList<>(); + for(IndexDescriptor idx : indexes) { + // Our index is not unique, this is not supported + StringBuilder b = new StringBuilder("CREATE INDEX "); + if(idx.concurrent) { + b.append(" CONCURRENTLY "); + } + if(idx.name != null) { + b.append(idx.name); + } + b.append(" ON ").append(table); + addAllIndexColumns(idx, table, b); + // USING method is not used; it is always the default + // INCLUDE is not used for now + b.append(" NULLS DISTINCT "); + // WITH is not used for now + // TABLESPACE is not used for now + // WHERE is not used for now + indexCommands.add(b.toString()); + } + System.err.println("Default DB Flavor: collected index commands"); + return indexCommands; + } + + protected static void addAllIndexColumns(IndexDescriptor idx, String column, StringBuilder b) { + // first colum, this is mandatory, we provoke an exception in case it is missing + b.append(" ( "); + addIndexColumn(idx, idx.columnNames.get(0), b); + for(int i = 1; i < idx.columnNames.size(); i++) { + b.append(", "); + addIndexColumn(idx, idx.columnNames.get(i), b); + } + b.append(" )"); + } + + protected static void addIndexColumn(IndexDescriptor idx, String column, StringBuilder b) { + b.append(column); + if(idx.order != null) { + b.append(" ").append(idx.order); + } + b.append(" "); + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/flavors/MySqlFlavor.java b/jdbc/src/main/java/site/ycsb/db/flavors/MySqlFlavor.java new file mode 100644 index 0000000..7c6130b --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/flavors/MySqlFlavor.java @@ -0,0 +1,64 @@ +package site.ycsb.db.flavors; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import site.ycsb.db.FilterBuilder; +import site.ycsb.db.IndexDescriptor; +import site.ycsb.db.JdbcDBInitHelper; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +public final class MySqlFlavor extends DefaultDBFlavor { + @Override + public void createDbAndSchema(String tableName, List conns) throws SQLException { + JdbcDBInitHelper.createTable(tableName, conns, "VARCHAR(255)"); + } + + @Override + public List buildIndexCommands(String table, List indexes) { + if(indexes.size() == 0) { + return Collections.emptyList(); + } + System.err.println("indexes: " + indexes.get(0).columnNames); + /* + * CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name + USING {BTREE | HASH} + ON tbl_name (key_part,...) + [index_option] + [algorithm_option | lock_option] ... + + key_part: {col_name [(length)] | (expr)} [ASC | DESC] + */ + List indexCommands = new ArrayList<>(); + for(IndexDescriptor idx : indexes) { + // Our index is not unique, this is not supported + StringBuilder b = new StringBuilder("CREATE INDEX "); + if(idx.concurrent) { + System.err.println("'CONCURRENT' mode not supported by MySQL, ignoring."); + } + if(idx.name != null) { + b.append(idx.name); + } + b.append(" ON ").append(table); + addAllIndexColumns(idx, table, b); + if(idx.method != null) { + b.append(" USING ").append(idx.method); + } + indexCommands.add(b.toString()); + } + System.err.println("MySQL Flavor: collected index commands"); + return indexCommands; + } + + @Override + public String createUpdateOneStatement(String tablename, List filters, List fields) { + final String template = "UPDATE %1$s SET %2$s WHERE %3$s LIMIT 1"; + String setString = FilterBuilder.buildConcatenatedPlaceholderSet(fields); + String filterString = FilterBuilder.buildConcatenatedPlaceholderFilter(filters); + return String.format(template, tablename, setString, filterString); + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/flavors/PhoenixDBFlavor.java b/jdbc/src/main/java/site/ycsb/db/flavors/PhoenixDBFlavor.java new file mode 100644 index 0000000..81b18a4 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/flavors/PhoenixDBFlavor.java @@ -0,0 +1,66 @@ +/** + * 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 site.ycsb.db.flavors; + +import static site.ycsb.db.JdbcDBConstants.PRIMARY_KEY; + +import site.ycsb.db.StatementType; + +/** + * Database flavor for Apache Phoenix. Captures syntax differences used by Phoenix. + */ +public class PhoenixDBFlavor extends DefaultDBFlavor { + public PhoenixDBFlavor() { + super(DBName.PHOENIX); + } + + @Override + public String createInsertStatement(StatementType insertType, String key) { + // Phoenix uses UPSERT syntax + StringBuilder insert = new StringBuilder("UPSERT INTO "); + insert.append(insertType.getTableName()); + insert.append(" (" + PRIMARY_KEY + "," + insertType.getFieldString() + ")"); + insert.append(" VALUES(?"); + for (int i = 0; i < insertType.getNumFields(); i++) { + insert.append(",?"); + } + insert.append(")"); + return insert.toString(); + } + + @Override + public String createUpdateStatement(StatementType updateType, String key) { + // Phoenix doesn't have UPDATE semantics, just re-use UPSERT VALUES on the specific columns + String[] fieldKeys = updateType.getFieldString().split(","); + StringBuilder update = new StringBuilder("UPSERT INTO "); + update.append(updateType.getTableName()); + update.append(" ("); + // Each column to update + for (int i = 0; i < fieldKeys.length; i++) { + update.append(fieldKeys[i]).append(","); + } + // And then set the primary key column + update.append(PRIMARY_KEY).append(") VALUES("); + // Add an unbound param for each column to update + for (int i = 0; i < fieldKeys.length; i++) { + update.append("?, "); + } + // Then the primary key column's value + update.append("?)"); + return update.toString(); + } +} diff --git a/jdbc/src/main/java/site/ycsb/db/flavors/package-info.java b/jdbc/src/main/java/site/ycsb/db/flavors/package-info.java new file mode 100644 index 0000000..2b24640 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/flavors/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. + */ +/** + * This package contains a collection of database-specific overrides. This accounts for the variance + * that can be present where JDBC does not explicitly define what a database must do or when a + * database has a non-standard SQL implementation. + */ +package site.ycsb.db.flavors; diff --git a/jdbc/src/main/java/site/ycsb/db/package-info.java b/jdbc/src/main/java/site/ycsb/db/package-info.java new file mode 100644 index 0000000..bdefd44 --- /dev/null +++ b/jdbc/src/main/java/site/ycsb/db/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2014 - 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. + */ + +/** + * The YCSB binding for stores that can be accessed via JDBC. + */ +package site.ycsb.db; + diff --git a/jdbc/src/main/resources/sql/README.md b/jdbc/src/main/resources/sql/README.md new file mode 100644 index 0000000..ec41b86 --- /dev/null +++ b/jdbc/src/main/resources/sql/README.md @@ -0,0 +1,18 @@ + +Contains all the SQL statements used by the JDBC client. diff --git a/jdbc/src/main/resources/sql/create_table.mysql b/jdbc/src/main/resources/sql/create_table.mysql new file mode 100644 index 0000000..a88a73a --- /dev/null +++ b/jdbc/src/main/resources/sql/create_table.mysql @@ -0,0 +1,27 @@ +-- Copyright (c) 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. + +-- Creates a Table. + +-- Drop the table if it exists; +DROP TABLE IF EXISTS usertable; + +-- Create the user table with 5 fields. +CREATE TABLE usertable(YCSB_KEY VARCHAR (255) PRIMARY KEY, + FIELD0 TEXT, FIELD1 TEXT, + FIELD2 TEXT, FIELD3 TEXT, + FIELD4 TEXT, FIELD5 TEXT, + FIELD6 TEXT, FIELD7 TEXT, + FIELD8 TEXT, FIELD9 TEXT); diff --git a/jdbc/src/main/resources/sql/create_table.sql b/jdbc/src/main/resources/sql/create_table.sql new file mode 100644 index 0000000..33158ac --- /dev/null +++ b/jdbc/src/main/resources/sql/create_table.sql @@ -0,0 +1,27 @@ +-- Copyright (c) 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. + +-- Creates a Table. + +-- Drop the table if it exists; +DROP TABLE IF EXISTS usertable; + +-- Create the user table with 5 fields. +CREATE TABLE usertable(YCSB_KEY VARCHAR PRIMARY KEY, + FIELD0 VARCHAR, FIELD1 VARCHAR, + FIELD2 VARCHAR, FIELD3 VARCHAR, + FIELD4 VARCHAR, FIELD5 VARCHAR, + FIELD6 VARCHAR, FIELD7 VARCHAR, + FIELD8 VARCHAR, FIELD9 VARCHAR); diff --git a/mongodb/README.md b/mongodb/README.md new file mode 100644 index 0000000..b248145 --- /dev/null +++ b/mongodb/README.md @@ -0,0 +1,159 @@ + + +## Quick Start + +This section describes how to run YCSB on MongoDB. + +### 1. Start MongoDB + +First, download MongoDB and start `mongod`. For example, to start MongoDB +on x86-64 Linux box: + + wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-x.x.x.tgz + tar xfvz mongodb-linux-x86_64-*.tgz + mkdir /tmp/mongodb + cd mongodb-linux-x86_64-* + ./bin/mongod --dbpath /tmp/mongodb + +Replace x.x.x above with the latest stable release version for MongoDB. +See http://docs.mongodb.org/manual/installation/ for installation steps for various operating systems. + +### 2. Install Java and Maven + +Go to http://www.oracle.com/technetwork/java/javase/downloads/index.html + +and get the url to download the rpm into your server. For example: + + wget http://download.oracle.com/otn-pub/java/jdk/7u40-b43/jdk-7u40-linux-x64.rpm?AuthParam=11232426132 -o jdk-7u40-linux-x64.rpm + rpm -Uvh jdk-7u40-linux-x64.rpm + +Or install via yum/apt-get + + sudo yum install java-devel + +Download MVN from http://maven.apache.org/download.cgi + + wget http://ftp.heanet.ie/mirrors/www.apache.org/dist/maven/maven-3/3.1.1/binaries/apache-maven-3.1.1-bin.tar.gz + sudo tar xzf apache-maven-*-bin.tar.gz -C /usr/local + cd /usr/local + sudo ln -s apache-maven-* maven + sudo vi /etc/profile.d/maven.sh + +Add the following to `maven.sh` + + export M2_HOME=/usr/local/maven + export PATH=${M2_HOME}/bin:${PATH} + +Reload bash and test mvn + + bash + mvn -version + +### 3. Set Up YCSB + +Download the YCSB zip file and compile: + + curl -O --location https://github.com/brianfrankcooper/YCSB/releases/download/0.5.0/ycsb-0.5.0.tar.gz + tar xfvz ycsb-0.5.0.tar.gz + cd ycsb-0.5.0 + +### 4. Run YCSB + +Now you are ready to run! First, use the asynchronous driver to load the data: + + ./bin/ycsb load mongodb-async -s -P workloads/workloada > outputLoad.txt + +Then, run the workload: + + ./bin/ycsb run mongodb-async -s -P workloads/workloada > outputRun.txt + +Similarly, to use the synchronous driver from MongoDB Inc. we load the data: + + ./bin/ycsb load mongodb -s -P workloads/workloada > outputLoad.txt + +Then, run the workload: + + ./bin/ycsb run mongodb -s -P workloads/workloada > outputRun.txt + +See the next section for the list of configuration parameters for MongoDB. + +## Log Level Control +Due to the mongodb driver defaulting to a log level of DEBUG, a logback.xml file is included with this module that restricts the org.mongodb logging to WARN. You can control this by overriding the logback.xml and defining it in your ycsb command by adding this flag: + +``` +bin/ycsb run mongodb -jvm-args="-Dlogback.configurationFile=/path/to/logback.xml" +``` + +## MongoDB Configuration Parameters + +- `mongodb.url` + - This should be a MongoDB URI or connection string. + - See http://docs.mongodb.org/manual/reference/connection-string/ for the standard options. + - For the complete set of options for the asynchronous driver see: + - http://www.allanbank.com/mongodb-async-driver/apidocs/index.html?com/allanbank/mongodb/MongoDbUri.html + - For the complete set of options for the synchronous driver see: + - http://api.mongodb.org/java/current/index.html?com/mongodb/MongoClientURI.html + - Default value is `mongodb://localhost:27017/ycsb?w=1` + - Default value of database is `ycsb` + +- `mongodb.batchsize` + - Useful for the insert workload as it will submit the inserts in batches inproving throughput. + - Default value is `1`. + +- `mongodb.upsert` + - Determines if the insert operation performs an update with the upsert operation or a insert. + Upserts have the advantage that they will continue to work for a partially loaded data set. + - Setting to `true` uses updates, `false` uses insert operations. + - Default value is `false`. + +- `mongodb.writeConcern` + - **Deprecated** - Use the `w` and `journal` options on the MongoDB URI provided by the `mongodb.url`. + - Allowed values are : + - `errors_ignored` + - `unacknowledged` + - `acknowledged` + - `journaled` + - `replica_acknowledged` + - `majority` + - Default value is `acknowledged`. + +- `mongodb.readPreference` + - **Deprecated** - Use the `readPreference` options on the MongoDB URI provided by the `mongodb.url`. + - Allowed values are : + - `primary` + - `primary_preferred` + - `secondary` + - `secondary_preferred` + - `nearest` + - Default value is `primary`. + +- `mongodb.maxconnections` + - **Deprecated** - Use the `maxPoolSize` options on the MongoDB URI provided by the `mongodb.url`. + - Default value is `100`. + +- `mongodb.threadsAllowedToBlockForConnectionMultiplier` + - **Deprecated** - Use the `waitQueueMultiple` options on the MongoDB URI provided by the `mongodb.url`. + - Default value is `5`. + +For example: + + ./bin/ycsb load mongodb-async -s -P workloads/workloada -p mongodb.url=mongodb://localhost:27017/ycsb?w=0 + +To run with the synchronous driver from MongoDB Inc.: + + ./bin/ycsb load mongodb -s -P workloads/workloada -p mongodb.url=mongodb://localhost:27017/ycsb?w=0 diff --git a/mongodb/pom.xml b/mongodb/pom.xml new file mode 100644 index 0000000..a115cc6 --- /dev/null +++ b/mongodb/pom.xml @@ -0,0 +1,91 @@ + + + + + 4.0.0 + + site.ycsb + binding-parent + 0.18.0-SNAPSHOT + ../binding-parent + + + mongodb-binding + MongoDB Binding + jar + + + + + org.mongodb + mongodb-driver-sync + ${mongodb.version} + + + site.ycsb + core + ${project.version} + provided + + + ch.qos.logback + logback-classic + 1.1.2 + runtime + + + + junit + junit + 4.12 + test + + + org.xerial.snappy + snappy-java + 1.1.7.1 + jar + compile + + + + + + true + always + warn + + + false + never + fail + + allanbank + Allanbank Releases + + http://www.allanbank.com/repo/ + default + + + diff --git a/mongodb/src/main/java/site/ycsb/db/FilterBuilder.java b/mongodb/src/main/java/site/ycsb/db/FilterBuilder.java new file mode 100644 index 0000000..e2f7077 --- /dev/null +++ b/mongodb/src/main/java/site/ycsb/db/FilterBuilder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db; + +import static com.mongodb.client.model.Filters.eq; +import static com.mongodb.client.model.Filters.lte; +import static com.mongodb.client.model.Filters.and; + +import java.util.ArrayList; +import java.util.List; + +import org.bson.conversions.Bson; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.ComparisonOperator; + +public final class FilterBuilder { + public static Bson buildConcatenatedFilter(List filters) { + // Bson[] bFilters = new Bson[filters.size()]; Arrays.asList(bFilters); + List lFilters = new ArrayList<>(filters.size()); + for(Comparison c : filters) { + Comparison d = c; + String fieldName = d.getFieldname(); + while(d.isSimpleNesting()) { + d = d.getSimpleNesting(); + fieldName = fieldName + "." + d.getFieldname(); + } + if(d.comparesStrings()) { + lFilters.add( + FilterBuilder.buildStringFilter( + fieldName, + d.getOperator(), + d.getOperandAsString() + )); + } else if(d.comparesInts()) { + lFilters.add( + FilterBuilder.buildIntFilter( + fieldName, + d.getOperator(), + d.getOperandAsInt() + )); + } else { + throw new IllegalStateException(); + } + } + return and(lFilters); + } + + public static Bson buildStringFilter(String fieldName, ComparisonOperator op, String operand) { + switch (op) { + case STRING_EQUAL: + return eq(fieldName, operand); + default: + throw new IllegalArgumentException("no string operator"); + } + } + + public static Bson buildIntFilter(String fieldName, ComparisonOperator op, int operand) { + switch (op) { + case INT_LTE: + return lte(fieldName, operand); + default: + throw new IllegalArgumentException("no string operator"); + } + } + private FilterBuilder() { + // no public constructor + } +} diff --git a/mongodb/src/main/java/site/ycsb/db/MongoDbClient.java b/mongodb/src/main/java/site/ycsb/db/MongoDbClient.java new file mode 100644 index 0000000..3b0e558 --- /dev/null +++ b/mongodb/src/main/java/site/ycsb/db/MongoDbClient.java @@ -0,0 +1,641 @@ +/** + * Copyright (c) 2012 - 2015 YCSB contributors. All rights reserved. + * Copyright (c) 2023 - 2024 benchANT GmbH. 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. + */ + +/* + * MongoDB client binding for YCSB. + * + * Submitted by Yen Pai on 5/11/2010. + * + * https://gist.github.com/000a66b8db2caf42467b#file_mongo_database.java + */ +package site.ycsb.db; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.IndexModel; +import com.mongodb.client.model.InsertManyOptions; +import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.client.model.UpdateOneModel; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.UpdateResult; + +import site.ycsb.ByteArrayByteIterator; +import site.ycsb.ByteIterator; +import site.ycsb.DB; +import site.ycsb.DBException; +import site.ycsb.IndexableDB; +import site.ycsb.Status; +import site.ycsb.workloads.core.CoreConstants; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +import org.bson.conversions.Bson; +import org.bson.BsonArray; +import org.bson.BsonValue; +import org.bson.Document; +import org.bson.types.Binary; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * MongoDB binding for YCSB framework using the MongoDB Inc. driver + *

+ * See the README.md for configuration information. + *

+ * + * @author ypai + * @see MongoDB Inc. + * driver + */ +public class MongoDbClient extends DB implements IndexableDB { + public static final String INDEX_LIST_PROPERTY = "mongodb.indexlist"; + + /** Used to include a field in a response. */ + private static final Integer INCLUDE = Integer.valueOf(1); + + /** The options to use for inserting many documents. */ + private static final InsertManyOptions INSERT_UNORDERED = + new InsertManyOptions().ordered(false); + + /** The options to use for inserting a single document. */ + private static final UpdateOptions UPDATE_WITH_UPSERT = new UpdateOptions() + .upsert(true); + + /** + * The database name to access. + */ + private static String databaseName; + + /** The database name to access. */ + private static MongoDatabase database; + + /** + * Count the number of times initialized to teardown on the last + * {@link #cleanup()}. + */ + private static final AtomicInteger INIT_COUNT = new AtomicInteger(0); + + /** A singleton Mongo instance. */ + private static MongoClient mongoClient; + + /** The default read preference for the test. */ + private static ReadPreference readPreference; + + /** The default write concern for the test. */ + private static WriteConcern writeConcern; + + /** The batch size to use for inserts. */ + private static int batchSize; + + /** If true then use updates with the upsert option for inserts. */ + private static boolean useUpsert; + + /** The bulk inserts pending for the thread. */ + private final List bulkInserts = new ArrayList(); + + private static boolean useTypedFields; + private static boolean debug = false; + /** + * Cleanup any state for this DB. Called once per DB instance; there is one DB + * instance per client thread. + */ + @Override + public void cleanup() throws DBException { + if (INIT_COUNT.decrementAndGet() == 0) { + try { + mongoClient.close(); + } catch (Exception e1) { + System.err.println("Could not close MongoDB connection pool: " + + e1.toString()); + e1.printStackTrace(); + return; + } finally { + database = null; + mongoClient = null; + } + } + } + + /** + * Delete a record from the database. + * + * @param table + * The name of the table + * @param key + * The record key of the record to delete. + * @return Zero on success, a non-zero error code on error. See the {@link DB} + * class's description for a discussion of error codes. + */ + @Override + public Status delete(String table, String key) { + try { + MongoCollection collection = database.getCollection(table); + + Document query = new Document("_id", key); + DeleteResult result = + collection.withWriteConcern(writeConcern).deleteOne(query); + if (result.wasAcknowledged() && result.getDeletedCount() == 0) { + if(debug) { + System.err.println("Nothing deleted for key " + key); + } + return Status.NOT_FOUND; + } + return Status.OK; + } catch (Exception e) { + System.err.println(e.toString()); + return Status.ERROR; + } + } + + private static List getIndexList(Properties props) { + String indexeslist = props.getProperty(INDEX_LIST_PROPERTY); + if(indexeslist == null) { + return Collections.emptyList(); + } + BsonArray barray = BsonArray.parse(indexeslist); + return barray.getValues(); + } + + private static void setIndexes(Properties props, List indexes, MongoDatabase database) { + if(indexes.size() == 0) { + return; + } + + final String table = props.getProperty(CoreConstants.TABLENAME_PROPERTY, CoreConstants.TABLENAME_PROPERTY_DEFAULT); + + List iModel = new ArrayList<>(); + for(BsonValue idx : indexes) { + if(!idx.isDocument()) { + System.err.println("illegal index format"); + System.exit(-2); + } + iModel.add(new IndexModel(idx.asDocument())); + } + MongoCollection collection = database.getCollection(table); + List names = collection.createIndexes(iModel); + System.err.println("created indexes: " + names); + } + /** + * Initialize any state for this DB. Called once per DB instance; there is one + * DB instance per client thread. + */ + @Override + public void init() throws DBException { + INIT_COUNT.incrementAndGet(); + synchronized (INCLUDE) { + if (mongoClient != null) { + return; + } + + Properties props = getProperties(); + debug = Boolean.parseBoolean(getProperties().getProperty("debug", "false")); + // Set insert batchsize, default 1 - to be YCSB-original equivalent + batchSize = Integer.parseInt(props.getProperty("batchsize", "1")); + + // Set is inserts are done as upserts. Defaults to false. + useUpsert = Boolean.parseBoolean( + props.getProperty("mongodb.upsert", "false")); + + // Just use the standard connection format URL + // http://docs.mongodb.org/manual/reference/connection-string/ + // to configure the client. + String url = props.getProperty("mongodb.url", null); + boolean defaultedUrl = false; + if (url == null) { + defaultedUrl = true; + url = "mongodb://localhost:27017/ycsb?w=1"; + } + + url = OptionsSupport.updateUrl(url, props); + + if (!url.startsWith("mongodb://") && !url.startsWith("mongodb+srv://")) { + System.err.println("ERROR: Invalid URL: '" + url + + "'. Must be of the form " + + "'mongodb://:,:/database?options' " + + "or 'mongodb+srv:///database?options'. " + + "http://docs.mongodb.org/manual/reference/connection-string/"); + System.exit(1); + } + + ConnectionString cs = new ConnectionString(url); + MongoClientSettings.Builder settingsBuilder = + MongoClientSettings.builder().applyConnectionString(cs); + readPreference = cs.getReadPreference(); + if(cs.getReadPreference() == null) { + readPreference = ReadPreference.primary(); + System.err.println("read preference not set, using default: " + readPreference); + settingsBuilder.readPreference(readPreference); + } else { + System.err.println("using user-defined read prefernence: " + readPreference); + } + writeConcern = cs.getWriteConcern(); + if(writeConcern == null) { + writeConcern = WriteConcern.MAJORITY; + System.err.println("write concern not set, using default: " + writeConcern); + settingsBuilder.writeConcern(writeConcern); + } else { + System.err.println("using user-defined write concern: " + writeConcern); + } + final String uriDb = cs.getDatabase(); + if (!defaultedUrl && (uriDb != null) && !uriDb.isEmpty() + && !"admin".equals(uriDb)) { + databaseName = uriDb; + System.err.println("using URI database name: " + databaseName); + } else { + // If no database is specified in URI, use "ycsb" + System.err.println("using default database name: ycsb"); + databaseName = "ycsb"; + } + + try { + mongoClient = MongoClients.create(settingsBuilder.build()); + database = + mongoClient.getDatabase(databaseName) + .withReadPreference(readPreference) + .withWriteConcern(writeConcern); + System.out.println("mongo client connection created with " + url); + } catch (Exception e1) { + System.err + .println("Could not initialize MongoDB connection pool for Loader: " + + e1.toString()); + e1.printStackTrace(); + return; + } + List indexes = getIndexList(props); + setIndexes(props, indexes, database); + useTypedFields = "true".equalsIgnoreCase(props.getProperty(TYPED_FIELDS_PROPERTY)); + } + } + + private Document buildLegacyDocument(String key, List values) { + Document toInsert = new Document("_id", key); + for (DatabaseField field : values) { + toInsert.put( + field.getFieldname(), + field.getContent().asIterator().toArray()); + } + return toInsert; + } + + private void fillDocument(DatabaseField field, Document toInsert) { + DataWrapper wrapper = field.getContent(); + Object content = null; + if(wrapper.isTerminal() || wrapper.isArray()) { + // this WILL BREAK if content is a nested + // document within the array + content = wrapper.asObject(); + } else if(wrapper.isNested()) { + Document inner = new Document(); + List innerFields = wrapper.asNested(); + for(DatabaseField iF : innerFields) { + fillDocument(iF, inner); + } + content = inner; + } else { + throw new IllegalStateException("neither terminal, nor array, nor nested"); + } + toInsert.put( + field.getFieldname(), + content + ); + } + + private Document buildKeylessTypedDocument(List values) { + Document toInsert = new Document(); + for (DatabaseField field : values) { + fillDocument(field, toInsert); + } + return toInsert; + } + private Document buildTypedDocument(String key, List values) { + Document toInsert = buildKeylessTypedDocument(values); + toInsert.put("_id", key); + return toInsert; + } + /** + * Insert a record in the database. Any field/value pairs in the specified + * values HashMap will be written into the record with the specified record + * key. + * + * @param table + * The name of the table + * @param key + * The record key of the record to insert. + * @param values + * A HashMap of field/value pairs to insert in the record + * @return Zero on success, a non-zero error code on error. See the {@link DB} + * class's description for a discussion of error codes. + */ + @Override + public Status insert(String table, String key, List values) { + final Document toInsert = useTypedFields + ? buildTypedDocument(key, values) + : buildLegacyDocument(key, values); + try { + MongoCollection collection = database.getCollection(table); + if (batchSize == 1) { + if (useUpsert) { + // this is effectively an insert, but using an upsert instead due + // to current inability of the framework to clean up after itself + // between test runs. + collection.replaceOne(new Document("_id", toInsert.get("_id")), + toInsert, new ReplaceOptions().upsert(true)); + } else { + collection.insertOne(toInsert); + } + } else { + bulkInserts.add(toInsert); + if (bulkInserts.size() >= batchSize) { + List local = new ArrayList<>(bulkInserts); + bulkInserts.clear(); + if (useUpsert) { + List> updates = + new ArrayList>(local.size()); + for (Document doc : local) { + updates.add(new UpdateOneModel( + new Document("_id", doc.get("_id")), + doc, UPDATE_WITH_UPSERT)); + } + collection.bulkWrite(updates); + } else { + collection.insertMany(local, INSERT_UNORDERED); + } + } else { + return Status.BATCHED_OK; + } + } + return Status.OK; + } catch (Exception e) { + if(debug) { + System.err.println("Exception while trying bulk insert with " + bulkInserts.size()); + e.printStackTrace(); + } + return Status.ERROR; + } + } + + @Override + public Status findOne(String table, List filters, + Set fields, Map result) { + if(filters == null || filters.size() == 0) { + throw new NullPointerException(); + } + // FIXME: this implementation does not support reading specific fields + if(fields != null) { + throw new UnsupportedOperationException("cannot read results by field"); + } + // building the filter + Bson query = FilterBuilder.buildConcatenatedFilter(filters); + try { + MongoCollection collection = database.getCollection(table); + Document queryResult = collection.find(query).first(); + if (queryResult != null) { + fillMap(result, queryResult); + return Status.OK; + } + if(debug) { + System.err.println("NOT FOUND: " + filters); + } + return Status.NOT_FOUND; + } catch (Exception e) { + System.err.println("Exception while findOne"); + e.printStackTrace(); + return Status.ERROR; + } + } + /*private Status findOneByFilter(String table) { + + }*/ + /** + * Read a record from the database. Each field/value pair from the result will + * be stored in a HashMap. + * + * @param table + * The name of the table + * @param key + * The record key of the record to read. + * @param fields + * The list of fields to read, or null for all of them + * @param result + * A HashMap of field/value pairs for the result + * @return Zero on success, a non-zero error code on error or "not found". + */ + @Override + public Status read(String table, String key, Set fields, + Map result) { + try { + MongoCollection collection = database.getCollection(table); + Document query = new Document("_id", key); + + FindIterable findIterable = collection.find(query); + + if (fields != null) { + Document projection = new Document(); + for (String field : fields) { + projection.put(field, INCLUDE); + } + findIterable.projection(projection); + } + + Document queryResult = findIterable.first(); + + if (queryResult != null) { + fillMap(result, queryResult); + } + return queryResult != null ? Status.OK : Status.NOT_FOUND; + } catch (Exception e) { + System.err.println(e.toString()); + return Status.ERROR; + } + } + + /** + * Perform a range scan for a set of records in the database. Each field/value + * pair from the result will be stored in a HashMap. + * + * @param table + * The name of the table + * @param startkey + * The record key of the first record to read. + * @param recordcount + * The number of records to read + * @param fields + * The list of fields to read, or null for all of them + * @param result + * A Vector of HashMaps, where each HashMap is a set field/value + * pairs for one record + * @return Zero on success, a non-zero error code on error. See the {@link DB} + * class's description for a discussion of error codes. + */ + @Override + public Status scan(String table, String startkey, int recordcount, + Set fields, Vector> result) { + MongoCursor cursor = null; + try { + MongoCollection collection = database.getCollection(table); + + Document scanRange = new Document("$gte", startkey); + Document query = new Document("_id", scanRange); + Document sort = new Document("_id", INCLUDE); + + FindIterable findIterable = + collection.find(query).sort(sort).limit(recordcount); + + if (fields != null) { + Document projection = new Document(); + for (String fieldName : fields) { + projection.put(fieldName, INCLUDE); + } + findIterable.projection(projection); + } + + cursor = findIterable.iterator(); + + if (!cursor.hasNext()) { + if(debug) { + System.err.println("Nothing found in scan for key " + startkey); + } + return Status.ERROR; + } + + result.ensureCapacity(recordcount); + + while (cursor.hasNext()) { + HashMap resultMap = + new HashMap(); + + Document obj = cursor.next(); + fillMap(resultMap, obj); + + result.add(resultMap); + } + + return Status.OK; + } catch (Exception e) { + System.err.println(e.toString()); + return Status.ERROR; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Update a record in the database. Any field/value pairs in the specified + * values HashMap will be written into the record with the specified record + * key, overwriting any existing values with the same field name. + * + * @param table + * The name of the table + * @param key + * The record key of the record to write. + * @param values + * A HashMap of field/value pairs to update in the record + * @return Zero on success, a non-zero error code on error. See this class's + * description for a discussion of error codes. + */ + @Override + public Status update(String table, String key, + Map values) { + try { + MongoCollection collection = database.getCollection(table); + + Document query = new Document("_id", key); + Document fieldsToSet = new Document(); + for (Map.Entry entry : values.entrySet()) { + fieldsToSet.put(entry.getKey(), entry.getValue().toArray()); + } + Document update = new Document("$set", fieldsToSet); + + UpdateResult result = collection.updateOne(query, update); + if (result.wasAcknowledged() && result.getMatchedCount() == 0) { + if(debug) { + System.err.println("Nothing updated for key " + key); + } + return Status.NOT_FOUND; + } + return Status.OK; + } catch (Exception e) { + System.err.println(e.toString()); + return Status.ERROR; + } + } + + @Override + public Status updateOne(String table, List filters, List fields) { + if(filters == null || filters.size() == 0) { + throw new NullPointerException(); + } + if(fields == null || fields.size() == 0) { + throw new NullPointerException(); + } + // building the filter + Bson query = FilterBuilder.buildConcatenatedFilter(filters); + Document update = new Document("$set", buildKeylessTypedDocument(fields)); + try { + MongoCollection collection = database.getCollection(table); + UpdateResult queryResult = collection.updateOne(query, update); + if (queryResult.wasAcknowledged() && queryResult.getMatchedCount() == 0) { + if(debug) { + System.err.println("Nothing updated for filters " + filters); + } + return Status.NOT_FOUND; + } + return Status.OK; + } catch (Exception e) { + System.err.println(e.toString()); + return Status.ERROR; + } + } + /** + * Fills the map with the values from the DBObject. + * + * @param resultMap + * The map to fill/ + * @param obj + * The object to copy values from. + */ + protected void fillMap(Map resultMap, Document obj) { + for (Map.Entry entry : obj.entrySet()) { + if (entry.getValue() instanceof Binary) { + resultMap.put(entry.getKey(), + new ByteArrayByteIterator(((Binary) entry.getValue()).getData())); + } + } + } +} diff --git a/mongodb/src/main/java/site/ycsb/db/OptionsSupport.java b/mongodb/src/main/java/site/ycsb/db/OptionsSupport.java new file mode 100644 index 0000000..f6a37d5 --- /dev/null +++ b/mongodb/src/main/java/site/ycsb/db/OptionsSupport.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2014, 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. + */ +package site.ycsb.db; + +import java.util.Properties; + +/** + * OptionsSupport provides methods for handling legacy options. + * + * @author rjm + */ +public final class OptionsSupport { + + /** Value for an unavailable property. */ + private static final String UNAVAILABLE = "n/a"; + + /** + * Updates the URL with the appropriate attributes if legacy properties are + * set and the URL does not have the property already set. + * + * @param url + * The URL to update. + * @param props + * The legacy properties. + * @return The updated URL. + */ + public static String updateUrl(String url, Properties props) { + String result = url; + + // max connections. + final String maxConnections = + props.getProperty("mongodb.maxconnections", UNAVAILABLE).toLowerCase(); + if (!UNAVAILABLE.equals(maxConnections)) { + result = addUrlOption(result, "maxPoolSize", maxConnections); + } + + // Blocked thread multiplier. + final String threadsAllowedToBlockForConnectionMultiplier = + props + .getProperty( + "mongodb.threadsAllowedToBlockForConnectionMultiplier", + UNAVAILABLE).toLowerCase(); + if (!UNAVAILABLE.equals(threadsAllowedToBlockForConnectionMultiplier)) { + result = + addUrlOption(result, "waitQueueMultiple", + threadsAllowedToBlockForConnectionMultiplier); + } + + // write concern + String writeConcernType = + props.getProperty("mongodb.writeConcern", UNAVAILABLE).toLowerCase(); + if (!UNAVAILABLE.equals(writeConcernType)) { + if ("errors_ignored".equals(writeConcernType)) { + result = addUrlOption(result, "w", "0"); + } else if ("unacknowledged".equals(writeConcernType)) { + result = addUrlOption(result, "w", "0"); + } else if ("acknowledged".equals(writeConcernType)) { + result = addUrlOption(result, "w", "1"); + } else if ("journaled".equals(writeConcernType)) { + result = addUrlOption(result, "journal", "true"); // this is the + // documented option + // name + result = addUrlOption(result, "j", "true"); // but keep this until + // MongoDB Java driver + // supports "journal" option + } else if ("replica_acknowledged".equals(writeConcernType)) { + result = addUrlOption(result, "w", "2"); + } else if ("majority".equals(writeConcernType)) { + result = addUrlOption(result, "w", "majority"); + } else { + System.err.println("WARNING: Invalid writeConcern: '" + + writeConcernType + "' will be ignored. " + + "Must be one of [ unacknowledged | acknowledged | " + + "journaled | replica_acknowledged | majority ]"); + } + } + + // read preference + String readPreferenceType = + props.getProperty("mongodb.readPreference", UNAVAILABLE).toLowerCase(); + if (!UNAVAILABLE.equals(readPreferenceType)) { + if ("primary".equals(readPreferenceType)) { + result = addUrlOption(result, "readPreference", "primary"); + } else if ("primary_preferred".equals(readPreferenceType)) { + result = addUrlOption(result, "readPreference", "primaryPreferred"); + } else if ("secondary".equals(readPreferenceType)) { + result = addUrlOption(result, "readPreference", "secondary"); + } else if ("secondary_preferred".equals(readPreferenceType)) { + result = addUrlOption(result, "readPreference", "secondaryPreferred"); + } else if ("nearest".equals(readPreferenceType)) { + result = addUrlOption(result, "readPreference", "nearest"); + } else { + System.err.println("WARNING: Invalid readPreference: '" + + readPreferenceType + "' will be ignored. " + + "Must be one of [ primary | primary_preferred | " + + "secondary | secondary_preferred | nearest ]"); + } + } + + return result; + } + + /** + * Adds an option to the url if it does not already contain the option. + * + * @param url + * The URL to append the options to. + * @param name + * The name of the option. + * @param value + * The value for the option. + * @return The updated URL. + */ + private static String addUrlOption(String url, String name, String value) { + String fullName = name + "="; + if (!url.contains(fullName)) { + if (url.contains("?")) { + return url + "&" + fullName + value; + } + return url + "?" + fullName + value; + } + return url; + } + + /** + * Hidden Constructor. + */ + private OptionsSupport() { + // Nothing. + } +} diff --git a/mongodb/src/main/java/site/ycsb/db/package-info.java b/mongodb/src/main/java/site/ycsb/db/package-info.java new file mode 100644 index 0000000..50f40c0 --- /dev/null +++ b/mongodb/src/main/java/site/ycsb/db/package-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2014, 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. + */ + +/** + * The YCSB binding for MongoDB. + * For additional details on using and configuring the binding see the + * accompanying README.md. + *

+ * A YCSB binding is provided for both the the + * Asynchronous + * Java Driver and the MongoDB Inc. + * driver. + *

+ */ +package site.ycsb.db; + diff --git a/mongodb/src/main/resources/log4j.properties b/mongodb/src/main/resources/log4j.properties new file mode 100644 index 0000000..266877c --- /dev/null +++ b/mongodb/src/main/resources/log4j.properties @@ -0,0 +1,25 @@ +# 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. + +#define the console appender +log4j.appender.consoleAppender = org.apache.log4j.ConsoleAppender + +# now define the layout for the appender +log4j.appender.consoleAppender.layout = org.apache.log4j.PatternLayout +log4j.appender.consoleAppender.layout.ConversionPattern=%-4r [%t] %-5p %c %x -%m%n + +# now map our console appender as a root logger, means all log messages will go +# to this appender +log4j.rootLogger = INFO, consoleAppender diff --git a/mongodb/src/main/resources/logback.xml b/mongodb/src/main/resources/logback.xml new file mode 100644 index 0000000..73354e0 --- /dev/null +++ b/mongodb/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/mongodb/src/test/java/site/ycsb/db/AbstractDBTestCases.java b/mongodb/src/test/java/site/ycsb/db/AbstractDBTestCases.java new file mode 100644 index 0000000..4cd2952 --- /dev/null +++ b/mongodb/src/test/java/site/ycsb/db/AbstractDBTestCases.java @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2014, 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. + */ +package site.ycsb.db; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeNoException; + +import site.ycsb.ByteArrayByteIterator; +import site.ycsb.ByteIterator; +import site.ycsb.DB; +import site.ycsb.Status; +import site.ycsb.wrappers.DatabaseField; +import site.ycsb.wrappers.Wrappers; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.Vector; + +/** + * MongoDbClientTest provides runs the basic DB test cases. + *

+ * The tests will be skipped if MongoDB is not running on port 27017 on the + * local machine. See the README.md for how to get MongoDB running. + *

+ */ +@SuppressWarnings("boxing") +public abstract class AbstractDBTestCases { + + /** The default port for MongoDB. */ + private static final int MONGODB_DEFAULT_PORT = 27017; + + /** + * Verifies the mongod process (or some process) is running on port 27017, if + * not the tests are skipped. + */ + @BeforeClass + public static void setUpBeforeClass() { + // Test if we can connect. + Socket socket = null; + try { + // Connect + socket = new Socket(InetAddress.getLocalHost(), MONGODB_DEFAULT_PORT); + assertThat("Socket is not bound.", socket.getLocalPort(), not(-1)); + } catch (IOException connectFailed) { + assumeNoException("MongoDB is not running. Skipping tests.", + connectFailed); + } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException ignore) { + // Ignore. + } + } + socket = null; + } + } + + /** + * Test method for {@link DB#insert}, {@link DB#read}, and {@link DB#delete} . + */ + @Test + public void testInsertReadDelete() { + final DB client = getDB(); + + final String table = getClass().getSimpleName(); + final String id = "delete"; + + List values = new ArrayList<>(); + values.add( + new DatabaseField("a", Wrappers.wrapIterator(new ByteArrayByteIterator(new byte[] { 1, 2, 3, 4 }))) + ); + Status result = client.insert(table, id, values); + assertThat("Insert did not return success (0).", result, is(Status.OK)); + + HashMap read = new HashMap(); + Set keys = Collections.singleton("a"); + result = client.read(table, id, keys, read); + assertThat("Read did not return success (0).", result, is(Status.OK)); + for (String key : keys) { + ByteIterator iter = read.get(key); + + assertThat("Did not read the inserted field: " + key, iter, + notNullValue()); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 1))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 2))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 3))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 4))); + assertFalse(iter.hasNext()); + } + + result = client.delete(table, id); + assertThat("Delete did not return success (0).", result, is(Status.OK)); + + read.clear(); + result = client.read(table, id, null, read); + assertThat("Read, after delete, did not return not found (1).", result, + is(Status.NOT_FOUND)); + assertThat("Found the deleted fields.", read.size(), is(0)); + + result = client.delete(table, id); + assertThat("Delete did not return not found (1).", result, is(Status.NOT_FOUND)); + } + + /** + * Test method for {@link DB#insert}, {@link DB#read}, and {@link DB#update} . + */ + @Test + public void testInsertReadUpdate() { + DB client = getDB(); + + final String table = getClass().getSimpleName(); + final String id = "update"; + + List values = new ArrayList<>(); + values.add( + new DatabaseField("a", Wrappers.wrapIterator(new ByteArrayByteIterator(new byte[] { 1, 2, 3, 4 }))) + ); + Status result = client.insert(table, id, values); + assertThat("Insert did not return success (0).", result, is(Status.OK)); + + HashMap read = new HashMap(); + Set keys = Collections.singleton("a"); + result = client.read(table, id, keys, read); + assertThat("Read did not return success (0).", result, is(Status.OK)); + for (String key : keys) { + ByteIterator iter = read.get(key); + + assertThat("Did not read the inserted field: " + key, iter, + notNullValue()); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 1))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 2))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 3))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 4))); + assertFalse(iter.hasNext()); + } + + HashMap updated = new HashMap(); + updated.put("a", new ByteArrayByteIterator(new byte[] { 5, 6, 7, 8 })); + result = client.update(table, id, updated); + assertThat("Update did not return success (0).", result, is(Status.OK)); + + read.clear(); + result = client.read(table, id, null, read); + assertThat("Read, after update, did not return success (0).", result, is(Status.OK)); + for (String key : keys) { + ByteIterator iter = read.get(key); + + assertThat("Did not read the inserted field: " + key, iter, + notNullValue()); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 5))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 6))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 7))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 8))); + assertFalse(iter.hasNext()); + } + } + + /** + * Test method for {@link DB#insert}, {@link DB#read}, and {@link DB#update} . + */ + @Test + public void testInsertReadUpdateWithUpsert() { + Properties props = new Properties(); + props.setProperty("mongodb.upsert", "true"); + DB client = getDB(props); + + final String table = getClass().getSimpleName(); + final String id = "updateWithUpsert"; + + List values = new ArrayList<>(); + values.add( + new DatabaseField("a", Wrappers.wrapIterator(new ByteArrayByteIterator(new byte[] { 1, 2, 3, 4 }))) + ); + Status result = client.insert(table, id, values); + assertThat("Insert did not return success (0).", result, is(Status.OK)); + + HashMap read = new HashMap(); + Set keys = Collections.singleton("a"); + result = client.read(table, id, keys, read); + assertThat("Read did not return success (0).", result, is(Status.OK)); + for (String key : keys) { + ByteIterator iter = read.get(key); + + assertThat("Did not read the inserted field: " + key, iter, + notNullValue()); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 1))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 2))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 3))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 4))); + assertFalse(iter.hasNext()); + } + + HashMap updated = new HashMap(); + updated.put("a", new ByteArrayByteIterator(new byte[] { 5, 6, 7, 8 })); + result = client.update(table, id, updated); + assertThat("Update did not return success (0).", result, is(Status.OK)); + + read.clear(); + result = client.read(table, id, null, read); + assertThat("Read, after update, did not return success (0).", result, is(Status.OK)); + for (String key : keys) { + ByteIterator iter = read.get(key); + + assertThat("Did not read the inserted field: " + key, iter, + notNullValue()); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 5))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 6))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 7))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) 8))); + assertFalse(iter.hasNext()); + } + } + + /** + * Test method for {@link DB#scan}. + */ + @Test + public void testScan() { + final DB client = getDB(); + + final String table = getClass().getSimpleName(); + + // Insert a bunch of documents. + for (int i = 0; i < 100; ++i) { + List values = new ArrayList<>(); + values.add( + new DatabaseField("a", Wrappers.wrapIterator( + new ByteArrayByteIterator(new byte[] { + (byte) (i & 0xFF), (byte) (i >> 8 & 0xFF), (byte) (i >> 16 & 0xFF), + (byte) (i >> 24 & 0xFF) }) + )) + ); + Status result = client.insert(table, padded(i), values); + assertThat("Insert did not return success (0).", result, is(Status.OK)); + } + + Set keys = Collections.singleton("a"); + Vector> results = + new Vector>(); + Status result = client.scan(table, "00050", 5, null, results); + assertThat("Read did not return success (0).", result, is(Status.OK)); + assertThat(results.size(), is(5)); + for (int i = 0; i < 5; ++i) { + Map read = results.get(i); + for (String key : keys) { + ByteIterator iter = read.get(key); + + assertThat("Did not read the inserted field: " + key, iter, + notNullValue()); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), is(Byte.valueOf((byte) ((i + 50) & 0xFF)))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), + is(Byte.valueOf((byte) ((i + 50) >> 8 & 0xFF)))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), + is(Byte.valueOf((byte) ((i + 50) >> 16 & 0xFF)))); + assertTrue(iter.hasNext()); + assertThat(iter.nextByte(), + is(Byte.valueOf((byte) ((i + 50) >> 24 & 0xFF)))); + assertFalse(iter.hasNext()); + } + } + } + + /** + * Gets the test DB. + * + * @return The test DB. + */ + protected DB getDB() { + return getDB(new Properties()); + } + + /** + * Gets the test DB. + * + * @param props + * Properties to pass to the client. + * @return The test DB. + */ + protected abstract DB getDB(Properties props); + + /** + * Creates a zero padded integer. + * + * @param i + * The integer to padd. + * @return The padded integer. + */ + private String padded(int i) { + String result = String.valueOf(i); + while (result.length() < 5) { + result = "0" + result; + } + return result; + } + +} \ No newline at end of file diff --git a/mongodb/src/test/java/site/ycsb/db/MongoDbClientTest.java b/mongodb/src/test/java/site/ycsb/db/MongoDbClientTest.java new file mode 100644 index 0000000..87b9f2e --- /dev/null +++ b/mongodb/src/test/java/site/ycsb/db/MongoDbClientTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2014, 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. + */ +package site.ycsb.db; + +import static org.junit.Assume.assumeNoException; + +import java.util.Properties; + +import org.junit.After; + +import site.ycsb.DB; + +/** + * MongoDbClientTest provides runs the basic workload operations. + */ +public class MongoDbClientTest extends AbstractDBTestCases { + + /** The client to use. */ + private DB myClient = null; + + protected DB instantiateClient() { + return new MongoDbClient(); + } + + /** + * Stops the test client. + */ + @After + public void tearDown() { + try { + myClient.cleanup(); + } catch (Exception error) { + // Ignore. + } finally { + myClient = null; + } + } + + /** + * {@inheritDoc} + *

+ * Overridden to return the {@link MongoDbClient}. + *

+ */ + @Override + protected DB getDB(Properties props) { + if( myClient == null ) { + myClient = instantiateClient(); + myClient.setProperties(props); + try { + myClient.init(); + } catch (Exception error) { + assumeNoException(error); + } + } + return myClient; + } +} diff --git a/mongodb/src/test/java/site/ycsb/db/OptionsSupportTest.java b/mongodb/src/test/java/site/ycsb/db/OptionsSupportTest.java new file mode 100644 index 0000000..dfab5c8 --- /dev/null +++ b/mongodb/src/test/java/site/ycsb/db/OptionsSupportTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2014, 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. + */ +package site.ycsb.db; + +import static site.ycsb.db.OptionsSupport.updateUrl; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.Properties; + +import org.junit.Test; + +/** + * OptionsSupportTest provides tests for the OptionsSupport class. + * + * @author rjm + */ +public class OptionsSupportTest { + + /** + * Test method for {@link OptionsSupport#updateUrl(String, Properties)} for + * {@code mongodb.maxconnections}. + */ + @Test + public void testUpdateUrlMaxConnections() { + assertThat( + updateUrl("mongodb://locahost:27017/", + props("mongodb.maxconnections", "1234")), + is("mongodb://locahost:27017/?maxPoolSize=1234")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.maxconnections", "1234")), + is("mongodb://locahost:27017/?foo=bar&maxPoolSize=1234")); + assertThat( + updateUrl("mongodb://locahost:27017/?maxPoolSize=1", + props("mongodb.maxconnections", "1234")), + is("mongodb://locahost:27017/?maxPoolSize=1")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", props("foo", "1234")), + is("mongodb://locahost:27017/?foo=bar")); + } + + /** + * Test method for {@link OptionsSupport#updateUrl(String, Properties)} for + * {@code mongodb.threadsAllowedToBlockForConnectionMultiplier}. + */ + @Test + public void testUpdateUrlWaitQueueMultiple() { + assertThat( + updateUrl( + "mongodb://locahost:27017/", + props("mongodb.threadsAllowedToBlockForConnectionMultiplier", + "1234")), + is("mongodb://locahost:27017/?waitQueueMultiple=1234")); + assertThat( + updateUrl( + "mongodb://locahost:27017/?foo=bar", + props("mongodb.threadsAllowedToBlockForConnectionMultiplier", + "1234")), + is("mongodb://locahost:27017/?foo=bar&waitQueueMultiple=1234")); + assertThat( + updateUrl( + "mongodb://locahost:27017/?waitQueueMultiple=1", + props("mongodb.threadsAllowedToBlockForConnectionMultiplier", + "1234")), is("mongodb://locahost:27017/?waitQueueMultiple=1")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", props("foo", "1234")), + is("mongodb://locahost:27017/?foo=bar")); + } + + /** + * Test method for {@link OptionsSupport#updateUrl(String, Properties)} for + * {@code mongodb.threadsAllowedToBlockForConnectionMultiplier}. + */ + @Test + public void testUpdateUrlWriteConcern() { + assertThat( + updateUrl("mongodb://locahost:27017/", + props("mongodb.writeConcern", "errors_ignored")), + is("mongodb://locahost:27017/?w=0")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.writeConcern", "unacknowledged")), + is("mongodb://locahost:27017/?foo=bar&w=0")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.writeConcern", "acknowledged")), + is("mongodb://locahost:27017/?foo=bar&w=1")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.writeConcern", "journaled")), + is("mongodb://locahost:27017/?foo=bar&journal=true&j=true")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.writeConcern", "replica_acknowledged")), + is("mongodb://locahost:27017/?foo=bar&w=2")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.writeConcern", "majority")), + is("mongodb://locahost:27017/?foo=bar&w=majority")); + + // w already exists. + assertThat( + updateUrl("mongodb://locahost:27017/?w=1", + props("mongodb.writeConcern", "acknowledged")), + is("mongodb://locahost:27017/?w=1")); + + // Unknown options + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", props("foo", "1234")), + is("mongodb://locahost:27017/?foo=bar")); + } + + /** + * Test method for {@link OptionsSupport#updateUrl(String, Properties)} for + * {@code mongodb.threadsAllowedToBlockForConnectionMultiplier}. + */ + @Test + public void testUpdateUrlReadPreference() { + assertThat( + updateUrl("mongodb://locahost:27017/", + props("mongodb.readPreference", "primary")), + is("mongodb://locahost:27017/?readPreference=primary")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.readPreference", "primary_preferred")), + is("mongodb://locahost:27017/?foo=bar&readPreference=primaryPreferred")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.readPreference", "secondary")), + is("mongodb://locahost:27017/?foo=bar&readPreference=secondary")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.readPreference", "secondary_preferred")), + is("mongodb://locahost:27017/?foo=bar&readPreference=secondaryPreferred")); + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", + props("mongodb.readPreference", "nearest")), + is("mongodb://locahost:27017/?foo=bar&readPreference=nearest")); + + // readPreference already exists. + assertThat( + updateUrl("mongodb://locahost:27017/?readPreference=primary", + props("mongodb.readPreference", "secondary")), + is("mongodb://locahost:27017/?readPreference=primary")); + + // Unknown options + assertThat( + updateUrl("mongodb://locahost:27017/?foo=bar", props("foo", "1234")), + is("mongodb://locahost:27017/?foo=bar")); + } + + /** + * Factory method for a {@link Properties} object. + * + * @param key + * The key for the property to set. + * @param value + * The value for the property to set. + * @return The {@link Properties} with the property added. + */ + private Properties props(String key, String value) { + Properties props = new Properties(); + + props.setProperty(key, value); + + return props; + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5fa7678 --- /dev/null +++ b/pom.xml @@ -0,0 +1,336 @@ + + + + + 4.0.0 + + site.ycsb + root + 0.18.0-SNAPSHOT + pom + + YCSB Root + + + This is the top level project that builds, packages the core and all the DB bindings for YCSB infrastructure. + + + https://ycsb.site/ + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + allanbank + Robert J. Moore + robert.j.moore@allanbank.com + + + busbey + Sean Busbey + sean.busbey@gmail.com + + + cmatser + Chrisjan Matser + cmatser@codespinnerinc.com + + + stfeng2 + Stanley Feng + stfeng@google.com + + + + + scm:git:git://github.com/brianfrankcooper/YCSB.git + master + https://github.com/brianfrankcooper/YCSB + + + + sonatype.releases.https + Release Repo at sonatype oss. + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + + sonatype.snapshots.https + snapshot Repo at sonatype oss. + https://oss.sonatype.org/content/repositories/snapshots + + + + + + com.puppycrawl.tools + checkstyle + 7.7.1 + + + org.jdom + jdom + 1.1 + + + com.google.collections + google-collections + 1.0 + + + org.slf4j + slf4j-api + 1.7.25 + + + + + + + 2.5.5 + 2.10 + + + 1.9.3 + 7.2.0 + 4.4.1 + 1.8.2 + 4.8.0 + 4.0.0 + 3.0.0 + 2.0.1 + 1.4.10 + 2.3.1 + 3.5.0 + 1.1-incubating + 5.5.1 + 5.2.5 + 1.2.0 + 1.4.0 + 4.0.0 + 1.4.12 + 2.2.3 + 2.7.6 + 7.2.2.Final + 1.11.1 + 1.1.8-mapr-1710 + + 5.1.3 + 2.0.1 + 2.1.1 + 2.2.37 + UTF-8 + 2.9.0 + 2.0.5 + 6.2.2 + 1.10.20 + 1.4.1 + 3.11.5.0 + + + 7.7.2 + 1.6.5 + 0.8.0 + 4.8.0 + 10.1.1 + 3.5.8 + + + + + core + binding-parent + distribution + + + couchbase3 + + dynamodb + + jdbc + + mongodb + + scylla + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.16 + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M1 + + + enforce-maven + + enforce + + + + + + [3.1.0,3.6.2),(3.6.2,) + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + validate + validate + + check + + + checkstyle.xml + + + + + + + + + + ycsb-release + + none + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0-M1 + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-release-artifacts + + sign + + + + + + + + + diff --git a/rest/README.md b/rest/README.md new file mode 100644 index 0000000..5ce1d4c --- /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 site.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:8080/`. + +- `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 0000000..0bf4db1 --- /dev/null +++ b/rest/pom.xml @@ -0,0 +1,135 @@ + + + + 4.0.0 + + site.ycsb + binding-parent + 0.18.0-SNAPSHOT + ../binding-parent + + + rest-binding + Rest Client Binding + jar + + + true + true + true + + 8.0.28 + 2.6 + 4.5.1 + 4.4.4 + 4.12 + 1.16.0 + + + + + site.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} + + + + + + + org.apache.rat + apache-rat-plugin + 0.12 + + + src/test/resources/error_trace.txt + src/test/resources/trace.txt + + + + + + + diff --git a/rest/src/main/java/site/ycsb/webservice/rest/RestClient.java b/rest/src/main/java/site/ycsb/webservice/rest/RestClient.java new file mode 100644 index 0000000..f17ac09 --- /dev/null +++ b/rest/src/main/java/site/ycsb/webservice/rest/RestClient.java @@ -0,0 +1,371 @@ +/** + * 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 site.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.Map; +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 site.ycsb.ByteIterator; +import site.ycsb.DB; +import site.ycsb.DBException; +import site.ycsb.Status; +import site.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, Map 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, Map 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, Map 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, Map 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/site/ycsb/webservice/rest/package-info.java b/rest/src/main/java/site/ycsb/webservice/rest/package-info.java new file mode 100644 index 0000000..72264c5 --- /dev/null +++ b/rest/src/main/java/site/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 site.ycsb.webservice.rest; + diff --git a/rest/src/test/java/site/ycsb/webservice/rest/IntegrationTest.java b/rest/src/test/java/site/ycsb/webservice/rest/IntegrationTest.java new file mode 100644 index 0000000..29f5b9b --- /dev/null +++ b/rest/src/test/java/site/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 site.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 site.ycsb.Client; +import site.ycsb.DBException; +import site.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/site/ycsb/webservice/rest/ResourceLoader.java b/rest/src/test/java/site/ycsb/webservice/rest/ResourceLoader.java new file mode 100644 index 0000000..846f41e --- /dev/null +++ b/rest/src/test/java/site/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 site.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/site/ycsb/webservice/rest/RestClientTest.java b/rest/src/test/java/site/ycsb/webservice/rest/RestClientTest.java new file mode 100644 index 0000000..d29ff59 --- /dev/null +++ b/rest/src/test/java/site/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 site.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 site.ycsb.ByteIterator; +import site.ycsb.DBException; +import site.ycsb.Status; +import site.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/site/ycsb/webservice/rest/RestTestResource.java b/rest/src/test/java/site/ycsb/webservice/rest/RestTestResource.java new file mode 100644 index 0000000..aa90636 --- /dev/null +++ b/rest/src/test/java/site/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 site.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/site/ycsb/webservice/rest/Utils.java b/rest/src/test/java/site/ycsb/webservice/rest/Utils.java new file mode 100644 index 0000000..be50a42 --- /dev/null +++ b/rest/src/test/java/site/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 site.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 0000000..73634ea --- /dev/null +++ b/rest/src/test/resources/WebContent/index.html @@ -0,0 +1,29 @@ + + + + + + + rest-binding + + + + Welcome to the rest-binding integration test cases! + + + diff --git a/rest/src/test/resources/error_trace.txt b/rest/src/test/resources/error_trace.txt new file mode 100644 index 0000000..18ff9cd --- /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 0000000..65a600d --- /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 0000000..c806905 --- /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=site.ycsb.workloads.RestWorkload +db=site.ycsb.webservice.rest.RestClient +exporter=site.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 diff --git a/scylla/README.md b/scylla/README.md new file mode 100644 index 0000000..ac5a27d --- /dev/null +++ b/scylla/README.md @@ -0,0 +1,294 @@ + + +# Scylla CQL binding + +Binding for [Scylla](https://www.scylladb.com/), using the CQL API +via the [Scylla driver](https://github.com/scylladb/java-driver/). + +Requires JDK8. + +## Creating a table for use with YCSB + +For keyspace `ycsb`, table `usertable`: + + cqlsh> create keyspace ycsb + WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor': 3 }; + cqlsh> USE ycsb; + cqlsh> create table usertable ( + y_id varchar primary key, + field0 varchar, + field1 varchar, + field2 varchar, + field3 varchar, + field4 varchar, + field5 varchar, + field6 varchar, + field7 varchar, + field8 varchar, + field9 varchar); + +**Note that `replication_factor` and consistency levels (below) will affect performance.** + +## Quick start + +Create a keyspace, and a table as mentioned above. Load data with: + + $ bin/ycsb load scylla -s -P workloads/workloada \ + -threads 84 -p recordcount=1000000000 \ + -p readproportion=0 -p updateproportion=0 \ + -p fieldcount=10 -p fieldlength=128 \ + -p insertstart=0 -p insertcount=1000000000 \ + -p cassandra.username=cassandra -p cassandra.password=cassandra \ + -p scylla.hosts=ip1,ip2,ip3,... + +Use as following: + + $ bin/ycsb run scylla -s -P workloads/workloada \ + -target 120000 -threads 840 -p recordcount=1000000000 \ + -p fieldcount=10 -p fieldlength=128 \ + -p operationcount=50000000 \ + -p scylla.username=cassandra -p scylla.password=cassandra \ + -p scylla.hosts=ip1,ip2,ip3,... + +## On choosing meaningful configuration + +### 1. Load target + +Suppose, you want to test how a database handles an OLTP load. + +In this case, to get the performance picture you want to look at the latency +distribution and utilization at the sustained throughput that is independent +of the processing speed. This kind of system called an open-loop system. +Use the `-target` flag to state desired requests arrival rate. + +For example `-target 120000` means that we expect YCSB workers to generate +120,000 requests per second (RPS, QPS or TPS) overall to the database. + +Why is this important? First, we want to look at the latency at some sustained +throughput target, not visa versa. Second, without a throughput target, +the system+loader pair will converge to the closed-loop system that has completely +different characteristics than what we wanted to measure. The load will settle +at the system equilibrium point. You will be able to find the throughput that will depend +on the number of loader threads (workers) but not the latency - only service time. +This is not something we expected. + +For more information check out these resources on the coordinated omission problem. + +See +[[1]](http://highscalability.com/blog/2015/10/5/your-load-generator-is-probably-lying-to-you-take-the-red-pi.html) +[[2]](https://medium.com/@siddontang/the-coordinated-omission-problem-in-the-benchmark-tools-5d9abef79279) +[[3]](https://bravenewgeek.com/tag/coordinated-omission/) +and [[this]](https://www.youtube.com/watch?v=lJ8ydIuPFeU) +great talk by Gil Tene. + +### 2. Latency correction + +To measure latency, it is not enough to just set a target. +The latencies must be measured with the correction as we apply +a closed-class loader to the open-class problem. This is what YCSB +calls an Intended operation. + +Intended operations have points in time when they were intended to be executed +according to the scheduler defined by the load target (--target). We must correct +measurement if we did not manage to execute an operation in time. + +The fair measurement consists of the operation latency and its correction +to the point of its intended execution. Even if you don’t want to have +a completely fair measurement, use “both”: + + -p measurement.interval=both + +Other options are “op” and “intended”. “op” is the default. + +Another flag that affects measurement quality is the type of histogram +“-p measurementtype” but for a long time, it uses “hdrhistogram” that +must be fine for most use cases. + +### 3. Latency percentiles and multiple loaders + +Latencies percentiles can't be averaged. Don't fall into this trap. +Neither averages nor p99 averages do not make any sense. + +If you run a single loader instance look for P99 - 99 percentile. +If you run multiple loaders dump result histograms with: + + -p measurement.histogram.verbose=true + +or + + -p hdrhistogram.fileoutput=true + -p hdrhistogram.output.path=file.hdr + +merge them manually and extract required percentiles out of the +joined result. + +Remember that running multiple workloads may distort original +workloads distributions they were intended to produce. + +### 4. Parallelism factor and threads + +Scylla utilizes [thread-per-core](https://www.scylladb.com/product/technology/) architecture design. +That means that a Node consists of shards that are mapped to the CPU cores 1-per-core. + +In production setup, Scylla reserves 1 core to the interrupts handling and other system stuff. +For the system with hyper threading (HT) it means 2 virtual cores. From that follows that the number +of _Shards_ per _Node_ typically is `Number Of Cores - 2` for HT machine and +`Number Of Cores - 1` for a machine without HT. + +It makes sense to select number of YCSB worker _threads_ to be multiple of the number +of shards, and the number of nodes in the cluster. For example: + + AWS Amazon i3.4xlarge has 16 vCPU (8 physical cores with HT). + + => + + scylla node shards = vCPUs - 2 = 16 - 2 = 14 + + => + + threads = K * shards per node * nodes + + for i3.4xlarge where + + - K is parallelism factor: + + K >= Target Throughput / QPS per Worker / Shards per node / Nodes / Workers per shard >= 1 + where + Target Throughput = --target + QPS per Worker = 1000 [ms/second] / Latency in ms expected at target Percentile + Shards per node = vCPU per cluster node - 2 + Nodes = a number of nodes in the cluster. + Workers per shard = Target Throughput / Shards per node / Nodes / QPS per Worker + + - Nodes is number of nodes in the cluster. + +Another concern is that for high throughput scenarios you would probably +want to keep shards incoming queues non-empty. For that your parallelism factor +must be at least 2. + +### 5. Number of connections + +If you use original Cassandra drivers you need to pick the proper number +of connections per host. Scylla drivers do not require this to be configured +and by default create a connection per shard. For example if your node has +16 vCPU and thus 14 shards Scylla drivers will pick to create 14 connections +per host. An excess of connections may result in degraded latency. + +Database client protocol is asynchronous and allows queueing requests in +a single connection. The default queue limit for local keys is 1024 and 256 +for remote ones. Current binding implementation do not require this. + +Both `scylla.coreconnections` and `scylla.maxconnections` define limits per node. +When you see `-p scylla.coreconnections=14 -p scylla.maxconnections=14` that means +14 connections per node. + +Pick the number of connections per host to be divisible by the number of _shards_. + +### 6. Other considerations + +Consistency levels do not change consistency model or its strongness. +Even with `-p scylla.writeconsistencylevel=ONE` the data will be written +according to the number of a table replication factor (RF). Usually, +by default RF = 3. By using `-p scylla.writeconsistencylevel=ONE` you +can omit waiting all replicas to write the value. It will improve your +latency picture a bit but would not affect utilization. + +Remember that you can't measure CPU utilization with Scylla by normal +Unix tools. Check out Scylla own metrics to see real reactors utilization. + +For best performance it is crucial to evenly load all available shards. + +### 7. Expected performance target + +You can expect about 12500 uOPS / core (shard), where uOPS are basic +reads and writes operations post replication. Don't forget that usually +`Core = 2 vCPU` for HT systems. + +For example if we insert a row with RF = 3 we can count at least 3 writes - +1 write per each replica. That is 1 Transaction = 3 u operations. + +Formula for evaluating performance with respect to workloads is: + + uOPS / vCPU = [ + Transactions * Writes_Ratio * Replication_Factor + + Transactions * Read_Ratio * Read_Consistency_level + ] / [ (vCPU_per_node - 2) * (nodes) ] + + where Transactions == `-target` parameter (target throughput). + +For example for _workloada_ that is 50/50 reads and writes for a cluster +of 3 nodes of i3.4xlarge (16 vCPU per node) and target of 120000 is: + + [ 120K * 0.5 * 3 + 120K * 0.5 * 2 (QUORUM) ] / [ (16 - 2) * 3 nodes ] = + = 7142 uOPS / vCPU ~ 14000 uOPS / Core. + +## Scylla configuration parameters + +- `scylla.hosts` (**required**) + - A list of Scylla nodes to connect to. + - No default. Usage: `-p scylla.hosts=ip1,ip2,ip3,...` + +* `scylla.port` + + - CQL port for communicating with Scylla cluster. + This port must be the same on all cluster nodes. + - Default is `9042`. + +- `scylla.keyspace` + + - keyspace name - must match the keyspace for the table created (see above). + See https://docs.scylladb.com/getting-started/ddl/#create-keyspace-statement for details. + - Default value is `ycsb` + +- `scylla.username` and `scylla.password` + + - Optional user name and password for authentication. + - See https://docs.scylladb.com/operating-scylla/security/enable-authorization/ for details. + +* `scylla.readconsistencylevel` +* `scylla.writeconsistencylevel` + + * Default value is `QUORUM` + - Consistency level for reads and writes, respectively. + See the [Scylla documentation](https://docs.scylladb.com/glossary/#term-consistency-level-any) for details. + +* `scylla.maxconnections` +* `scylla.coreconnections` + + * Defaults for max and core connections can be found here: + https://github.com/scylladb/java-driver/tree/latest/manual/pooling. + +* `scylla.connecttimeoutmillis` +* `scylla.readtimeoutmillis` +* `scylla.useSSL` + + * Default value is false. + - To connect with SSL set this value to true. + +* `scylla.tracing` + * Default is false + * https://docs.scylladb.com/using-scylla/tracing/ + +* `scylla.local_dc` + - Specify local datacenter for multi-dc setup. + - By default uses LOCAL_QUORUM consistency level. + - Default value is empty. + +- `scylla.lwt` + - Use LWT for operations + - Default is false. diff --git a/scylla/pom.xml b/scylla/pom.xml new file mode 100644 index 0000000..00ff548 --- /dev/null +++ b/scylla/pom.xml @@ -0,0 +1,129 @@ + + + + + + 4.0.0 + + site.ycsb + binding-parent + 0.18.0-SNAPSHOT + ../binding-parent + + + scylla-binding + Scylla DB Binding + jar + + + true + + + + + com.scylladb + scylla-driver-core + + ${scylla.cql.version} + + + + site.ycsb + core + ${project.version} + provided + + + org.cassandraunit + cassandra-unit + 3.0.0.1 + shaded + test + + + org.slf4j + slf4j-api + 1.7.25 + + + org.slf4j + slf4j-simple + 1.7.25 + runtime + + + junit + junit + 4.12 + test + + + + + + jdk8-tests + + 1.8 + + + false + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack-sigar + process-test-resources + + unpack-dependencies + + + org.hyperic + sigar-dist + **/sigar-bin/lib/* + **/sigar-bin/lib/*jar + + ${project.build.directory}/scylla-dependency + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.8 + + -Djava.library.path=${project.build.directory}/scylla-dependency/hyperic-sigar-1.6.4/sigar-bin/lib + + + + + + diff --git a/scylla/src/main/java/site/ycsb/db/scylla/FilterBuilder.java b/scylla/src/main/java/site/ycsb/db/scylla/FilterBuilder.java new file mode 100644 index 0000000..3644567 --- /dev/null +++ b/scylla/src/main/java/site/ycsb/db/scylla/FilterBuilder.java @@ -0,0 +1,201 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db.scylla; + +import java.util.List; +import java.util.function.Function; + +import org.omg.CORBA.Bounds; + +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.querybuilder.Assignment; +import com.datastax.driver.core.querybuilder.Clause; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; +import com.datastax.driver.core.querybuilder.Update; +import com.datastax.driver.core.querybuilder.Update.Assignments; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.ComparisonOperator; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +public final class FilterBuilder { + + static int bindFilter(int startIndex, BoundStatement bound, List filters) { + int currentIndex = startIndex; + for(Comparison c : filters){ + if(c.isSimpleNesting()) { + throw new IllegalArgumentException("nestings are not supported for scylla."); + } + if(c.comparesStrings()) { + bound.setString(currentIndex, c.getOperandAsString()); + } else if(c.comparesInts()) { + bound.setInt(currentIndex, c.getOperandAsInt()); + } else { + throw new IllegalStateException(); + } + currentIndex++; + } + // this is the next index + return currentIndex; + } + + static void bindInnerFilter(List filters, boolean usePlaceholder, Function f) { + for(Comparison c : filters) { + if(c.isSimpleNesting()) { + throw new IllegalArgumentException("nestings are not supported for scylla."); + } + if(c.comparesStrings()) { + f.apply( + buildFilterClause(c.getFieldname(), c.getOperator(), usePlaceholder ? QueryBuilder.bindMarker() : c.getOperandAsString()) + ); + } else if(c.comparesInts()) { + f.apply( + buildFilterClause(c.getFieldname(), c.getOperator(), usePlaceholder ? QueryBuilder.bindMarker() : c.getOperandAsInt()) + ); + } else { + throw new IllegalStateException(); + } + } + } + static int bindPreparedAssignments(BoundStatement bound, List fields, int startIndex) { + int currentIndex = startIndex; + for(DatabaseField field : fields){ + DataWrapper wrapper = field.getContent(); + if(wrapper.isTerminal()) { + if(wrapper.isInteger()) { + bound.setInt(currentIndex, field.getContent().asInteger()); + } else if(wrapper.isLong()) { + bound.setLong(currentIndex, field.getContent().asLong()); + } else if(wrapper.isString()) { + bound.setString(currentIndex, field.getContent().asString()); + } else { + // assuming this is an iterator + // which is the only remaining terminal + Object o = field.getContent().asObject(); + byte[] b = (byte[]) o; + bound.setString(currentIndex, new String(b)); + } + } else if(wrapper.isNested()) { + throw new IllegalArgumentException("setting nested elements is currently not supported"); + } else if(wrapper.isArray()) { + throw new IllegalArgumentException("setting arrays or array content is currently not supported"); + } else { + throw new IllegalStateException("neither terminal, nor array, nor nested"); + } + currentIndex++; + } + return currentIndex; + } + static void buildPreparedAssignments(List fields, Assignments assignments) { + for(DatabaseField field : fields){ + assignments.and( + buildTypedAssignment(field, true) + ); + } + } + static void buildAssignments(List fields, Assignments assignments) { + for(DatabaseField field : fields){ + assignments.and( + buildTypedAssignment(field, false) + ); + } + } + private static Assignment buildTypedAssignment(DatabaseField field, boolean usePlaceholder) { + DataWrapper wrapper = field.getContent(); + if(wrapper.isTerminal()) { + final String fieldName = field.getFieldname(); + if(wrapper.isInteger()) { + return QueryBuilder.set(fieldName, usePlaceholder ? QueryBuilder.bindMarker() : field.getContent().asInteger()); + } + if(wrapper.isLong()) { + return QueryBuilder.set(fieldName, usePlaceholder ? QueryBuilder.bindMarker() : field.getContent().asLong()); + } else if(wrapper.isString()) { + return QueryBuilder.set(fieldName, usePlaceholder ? QueryBuilder.bindMarker() : field.getContent().asString()); + } else { + // assuming this is an iterator + // which is the only remaining terminal + if(usePlaceholder) { + return QueryBuilder.set(fieldName, QueryBuilder.bindMarker()); + } else { + Object o = field.getContent().asObject(); + byte[] b = (byte[]) o; + return QueryBuilder.set(fieldName, new String(b)); + } + } + } else if(wrapper.isNested()) { + throw new IllegalArgumentException("setting nested elements is currently not supported"); + } else if(wrapper.isArray()) { + throw new IllegalArgumentException("setting arrays or array content is currently not supported"); + } else { + throw new IllegalStateException("neither terminal, nor array, nor nested"); + } + } + + static void buildPreparedFilter(List filters, Select.Where where) { + buildInnerFilter(filters, true , (Clause c) -> { + where.and(c); + return Boolean.TRUE; + }); + } + + static void buildFilter(List filters, Update.Where where) { + buildInnerFilter(filters, false, (Clause c) -> { + where.and(c); + return Boolean.TRUE; + }); + } + static void buildFilter(List filters, Select.Where where) { + buildInnerFilter(filters, false, (Clause c) -> { + where.and(c); + return Boolean.TRUE; + }); + } + + static void buildInnerFilter(List filters, boolean usePlaceholder, Function f) { + for(Comparison c : filters) { + if(c.isSimpleNesting()) { + throw new IllegalArgumentException("nestings are not supported for scylla."); + } + if(c.comparesStrings()) { + f.apply( + buildFilterClause(c.getFieldname(), c.getOperator(), usePlaceholder ? QueryBuilder.bindMarker() : c.getOperandAsString()) + ); + } else if(c.comparesInts()) { + f.apply( + buildFilterClause(c.getFieldname(), c.getOperator(), usePlaceholder ? QueryBuilder.bindMarker() : c.getOperandAsInt()) + ); + } else { + throw new IllegalStateException(); + } + } + } + + private static Clause buildFilterClause(String fieldName, ComparisonOperator op, Object operand) { + switch (op) { + case STRING_EQUAL: + return QueryBuilder.like(fieldName, operand); + case INT_LTE: + return QueryBuilder.lte(fieldName, operand); + default: + throw new IllegalArgumentException("operator not supported"); + } + } + + private FilterBuilder() { + // empty + } +} diff --git a/scylla/src/main/java/site/ycsb/db/scylla/ScyllaCQLClient.java b/scylla/src/main/java/site/ycsb/db/scylla/ScyllaCQLClient.java new file mode 100644 index 0000000..4b12a71 --- /dev/null +++ b/scylla/src/main/java/site/ycsb/db/scylla/ScyllaCQLClient.java @@ -0,0 +1,814 @@ +/* + * Copyright (c) 2020 YCSB contributors. All rights reserved. + * Copyright 2023-2024 benchANT GmbH. 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 site.ycsb.db.scylla; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.helpers.MessageFormatter; + +import com.datastax.driver.core.BatchStatement; +import com.datastax.driver.core.BatchStatement.Type; +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.ConsistencyLevel; +import com.datastax.driver.core.Host; +import com.datastax.driver.core.HostDistance; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.UDTValue; +import com.datastax.driver.core.UserType; +import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy; +import com.datastax.driver.core.policies.LoadBalancingPolicy; +import com.datastax.driver.core.policies.TokenAwarePolicy; +import com.datastax.driver.core.querybuilder.Delete; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; +import com.datastax.driver.core.querybuilder.Update; +import com.datastax.driver.core.schemabuilder.SchemaBuilder; +import com.datastax.driver.core.schemabuilder.SchemaStatement; + +import site.ycsb.ByteArrayByteIterator; +import site.ycsb.ByteIterator; +import site.ycsb.DB; +import site.ycsb.DBException; +import site.ycsb.IndexableDB; +import site.ycsb.Status; +import site.ycsb.workloads.core.CoreConstants; +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DataWrapper; +import site.ycsb.wrappers.DatabaseField; + +import static site.ycsb.db.scylla.ScyllaDbConstants.*; + +/** + * Scylla DB implementation. + */ +public class ScyllaCQLClient extends DB implements IndexableDB { + static class IndexDescriptor { + String name; + List columnNames = new ArrayList<>(); + } + private static final Logger LOGGER = LoggerFactory.getLogger(ScyllaCQLClient.class); + + private static Cluster cluster = null; + static Session session = null; + + private static final ConcurrentMap, PreparedStatement> READ_STMTS = new ConcurrentHashMap<>(); + private static final ConcurrentMap, PreparedStatement> SCAN_STMTS = new ConcurrentHashMap<>(); + private static final ConcurrentMap, PreparedStatement> UPDATE_STMTS = new ConcurrentHashMap<>(); + private static final AtomicReference READ_ALL_STMT = new AtomicReference<>(); + private static final AtomicReference SCAN_ALL_STMT = new AtomicReference<>(); + private static final AtomicReference DELETE_STMT = new AtomicReference<>(); + + private static ConsistencyLevel readConsistencyLevel; + static ConsistencyLevel writeConsistencyLevel; + + static boolean lwt = false; + + /** The batch size to use for inserts. */ + private static int batchSize; + private static boolean useTypedFields; + static String keyspace; + private BatchStatement batch = null; + + /** + * Count the number of times initialized to teardown on the last + * {@link #cleanup()}. + */ + private static final AtomicInteger INIT_COUNT = new AtomicInteger(0); + + static boolean debug = false; + static boolean trace = false; + + /** + * Initialize any state for this DB. Called once per DB instance; there is one + * DB instance per client thread. + */ + @Override + public void init() throws DBException { + + // Keep track of number of calls to init (for later cleanup) + INIT_COUNT.incrementAndGet(); + + batch = new BatchStatement(Type.UNLOGGED); + // Synchronized so that we only have a single + // cluster/session instance for all the threads. + synchronized (INIT_COUNT) { + + // Check if the cluster has already been initialized + if (cluster != null) { + return; + } + + try { + // Set insert batchsize, default 1 - to be YCSB-original equivalent + batchSize = Integer.parseInt(getProperties().getProperty("batchsize", "1")); + debug = Boolean.parseBoolean(getProperties().getProperty("debug", "false")); + trace = Boolean.parseBoolean(getProperties().getProperty(TRACING_PROPERTY, TRACING_PROPERTY_DEFAULT)); + + String host = getProperties().getProperty(HOSTS_PROPERTY); + if (host == null) { + throw new DBException(String.format("Required property \"%s\" missing for scyllaCQLClient", HOSTS_PROPERTY)); + } + String[] hosts = host.split(","); + String port = getProperties().getProperty(PORT_PROPERTY, PORT_PROPERTY_DEFAULT); + + String username = getProperties().getProperty(USERNAME_PROPERTY); + String password = getProperties().getProperty(PASSWORD_PROPERTY); + + keyspace = getProperties().getProperty(KEYSPACE_PROPERTY, KEYSPACE_PROPERTY_DEFAULT); + + readConsistencyLevel = ConsistencyLevel.valueOf( + getProperties().getProperty(READ_CONSISTENCY_LEVEL_PROPERTY, READ_CONSISTENCY_LEVEL_PROPERTY_DEFAULT)); + writeConsistencyLevel = ConsistencyLevel.valueOf( + getProperties().getProperty(WRITE_CONSISTENCY_LEVEL_PROPERTY, WRITE_CONSISTENCY_LEVEL_PROPERTY_DEFAULT)); + + boolean useSSL = Boolean.parseBoolean( + getProperties().getProperty(USE_SSL_CONNECTION, DEFAULT_USE_SSL_CONNECTION)); + + Cluster.Builder builder; + if ((username != null) && !username.isEmpty()) { + builder = Cluster.builder().withCredentials(username, password) + .addContactPoints(hosts).withPort(Integer.parseInt(port)); + if (useSSL) { + builder = builder.withSSL(); + } + } else { + builder = Cluster.builder().withPort(Integer.parseInt(port)) + .addContactPoints(hosts); + } + + final String localDC = getProperties().getProperty(TOKEN_AWARE_LOCAL_DC); + if (localDC != null && !localDC.isEmpty()) { + final LoadBalancingPolicy local = DCAwareRoundRobinPolicy.builder().withLocalDc(localDC).build(); + final TokenAwarePolicy tokenAware = new TokenAwarePolicy(local); + builder = builder.withLoadBalancingPolicy(tokenAware); + + LOGGER.info("Using local datacenter with token awareness: {}\n", localDC); + + // If was not overridden explicitly, set LOCAL_QUORUM + if (getProperties().getProperty(READ_CONSISTENCY_LEVEL_PROPERTY) == null) { + readConsistencyLevel = ConsistencyLevel.LOCAL_QUORUM; + } + + if (getProperties().getProperty(WRITE_CONSISTENCY_LEVEL_PROPERTY) == null) { + writeConsistencyLevel = ConsistencyLevel.LOCAL_QUORUM; + } + } + + cluster = builder.build(); + + String maxConnections = getProperties().getProperty( + MAX_CONNECTIONS_PROPERTY); + if (maxConnections != null) { + cluster.getConfiguration().getPoolingOptions() + .setMaxConnectionsPerHost(HostDistance.LOCAL, Integer.parseInt(maxConnections)); + } + + String coreConnections = getProperties().getProperty( + CORE_CONNECTIONS_PROPERTY); + if (coreConnections != null) { + cluster.getConfiguration().getPoolingOptions() + .setCoreConnectionsPerHost(HostDistance.LOCAL, Integer.parseInt(coreConnections)); + } + + String connectTimeoutMillis = getProperties().getProperty( + CONNECT_TIMEOUT_MILLIS_PROPERTY); + if (connectTimeoutMillis != null) { + cluster.getConfiguration().getSocketOptions() + .setConnectTimeoutMillis(Integer.parseInt(connectTimeoutMillis)); + } + + String readTimeoutMillis = getProperties().getProperty( + READ_TIMEOUT_MILLIS_PROPERTY); + if (readTimeoutMillis != null) { + cluster.getConfiguration().getSocketOptions() + .setReadTimeoutMillis(Integer.parseInt(readTimeoutMillis)); + } + + Metadata metadata = cluster.getMetadata(); + LOGGER.info("Connected to cluster: {}\n", metadata.getClusterName()); + + for (Host discoveredHost : metadata.getAllHosts()) { + LOGGER.info("Datacenter: {}; Host: {}; Rack: {}\n", + discoveredHost.getDatacenter(), discoveredHost.getEndPoint().resolve().getAddress(), + discoveredHost.getRack()); + } + boolean initDb = "true".equalsIgnoreCase(getProperties().getProperty(INIT_TABLE, "false")); + if(!initDb) { + session = cluster.connect(keyspace); + } else { + session = ScyllaDbInitHelper.createKeyspaceAndSchema(getProperties(), keyspace, cluster); + } + if (Boolean.parseBoolean(getProperties().getProperty(SCYLLA_LWT, Boolean.toString(lwt)))) { + LOGGER.info("Using LWT\n"); + lwt = true; + readConsistencyLevel = ConsistencyLevel.SERIAL; + writeConsistencyLevel = ConsistencyLevel.ANY; + } else { + LOGGER.info("Not using LWT\n"); + } + LOGGER.info("Read consistency: {}, Write consistency: {}\n", + readConsistencyLevel.name(), + writeConsistencyLevel.name()); + } catch (Exception e) { + throw new DBException(e); + } + useTypedFields = "true".equalsIgnoreCase(getProperties().getProperty(TYPED_FIELDS_PROPERTY)); + List indexes = ScyllaDbInitHelper.getIndexList(getProperties()); + setIndexes(getProperties(), indexes); + } // synchronized + } + private void setIndexes(Properties props, List indexes) { + if(indexes.size() == 0) { + return; + } + System.err.println("indexes: " + indexes.get(0).columnNames); + final String table = props.getProperty(CoreConstants.TABLENAME_PROPERTY, CoreConstants.TABLENAME_PROPERTY_DEFAULT); + for(IndexDescriptor idx : indexes) { + if(idx.columnNames.size() < 1) continue; + if(idx.columnNames.size() > 1) { + LOGGER.error("found multiple columns in index. this is not supported by scylla"); + System.exit(-2); + } + SchemaStatement ss = SchemaBuilder.createIndex(idx.name) + .ifNotExists().onTable(keyspace, table) + .andColumn(idx.columnNames.get(0)); + ResultSet rs = session.execute(ss); + boolean applied = rs.wasApplied(); + List results = rs.all(); + LOGGER.info("created index: " + idx + ": " + results.toString() + "/" + applied); + } + System.err.println("created indexes"); + } + + /** + * Cleanup any state for this DB. Called once per DB instance; there is one DB + * instance per client thread. + */ + @Override + public void cleanup() throws DBException { + synchronized (INIT_COUNT) { + final int curInitCount = INIT_COUNT.decrementAndGet(); + if (curInitCount <= 0) { + ScyllaStatementHandler.cleanup(); + READ_STMTS.clear(); + SCAN_STMTS.clear(); + UPDATE_STMTS.clear(); + READ_ALL_STMT.set(null); + SCAN_ALL_STMT.set(null); + DELETE_STMT.set(null); + if (session != null) { + session.close(); + session = null; + } + if (cluster != null) { + cluster.close(); + cluster = null; + } + } + if (curInitCount < 0) { + // This should never happen. + throw new DBException(String.format("initCount is negative: %d", curInitCount)); + } + } + } + + /** + * Read a record from the database. Each field/value pair from the result will + * be stored in a HashMap. + * + * @param table + * The name of the table + * @param key + * The record key of the record to read. + * @param fields + * The list of fields to read, or null for all of them + * @param result + * A HashMap of field/value pairs for the result + * @return Zero on success, a non-zero error code on error + */ + @Override + public Status read(String table, String key, Set fields, + Map result) { + try { + PreparedStatement stmt = (fields == null) ? READ_ALL_STMT.get() : READ_STMTS.get(fields); + + // Prepare statement on demand + if (stmt == null) { + Select.Builder selectBuilder; + + if (fields == null) { + selectBuilder = QueryBuilder.select().all(); + } else { + selectBuilder = QueryBuilder.select(); + for (String col : fields) { + ((Select.Selection) selectBuilder).column(col); + } + } + + stmt = session.prepare(selectBuilder.from(table) + .where(QueryBuilder.eq(YCSB_KEY, QueryBuilder.bindMarker())) + .limit(1)); + stmt.setConsistencyLevel(readConsistencyLevel); + if (trace) { + stmt.enableTracing(); + } + + PreparedStatement prevStmt = (fields == null) ? + READ_ALL_STMT.getAndSet(stmt) : + READ_STMTS.putIfAbsent(new HashSet<>(fields), stmt); + if (prevStmt != null) { + stmt = prevStmt; + } + } + + LOGGER.debug(stmt.getQueryString()); + LOGGER.debug("key = {}", key); + + ResultSet rs = session.execute(stmt.bind(key)); + + if (rs.isExhausted()) { + return Status.NOT_FOUND; + } + + // Should be only 1 row + Row row = rs.one(); + ColumnDefinitions cd = row.getColumnDefinitions(); + + for (ColumnDefinitions.Definition def : cd) { + ByteBuffer val = row.getBytesUnsafe(def.getName()); + if (val != null) { + result.put(def.getName(), new ByteArrayByteIterator(val.array())); + } else { + result.put(def.getName(), null); + } + } + + return Status.OK; + + } catch (Exception e) { + LOGGER.error(MessageFormatter.format("Error reading key: {}", key).getMessage(), e); + return Status.ERROR; + } + + } + + /** + * Perform a range scan for a set of records in the database. Each field/value + * pair from the result will be stored in a HashMap. + * + * scylla CQL uses "token" method for range scan which doesn't always yield + * intuitive results. + * + * @param table + * The name of the table + * @param startkey + * The record key of the first record to read. + * @param recordcount + * The number of records to read + * @param fields + * The list of fields to read, or null for all of them + * @param result + * A Vector of HashMaps, where each HashMap is a set field/value + * pairs for one record + * @return Zero on success, a non-zero error code on error + */ + @Override + public Status scan(String table, String startkey, int recordcount, + Set fields, Vector> result) { + + try { + PreparedStatement stmt = (fields == null) ? SCAN_ALL_STMT.get() : SCAN_STMTS.get(fields); + + // Prepare statement on demand + if (stmt == null) { + Select.Builder selectBuilder; + + if (fields == null) { + selectBuilder = QueryBuilder.select().all(); + } else { + selectBuilder = QueryBuilder.select(); + for (String col : fields) { + ((Select.Selection) selectBuilder).column(col); + } + } + + Select selectStmt = selectBuilder.from(table); + + // The statement builder is not setup right for tokens. + // So, we need to build it manually. + String initialStmt = selectStmt.toString(); + String scanStmt = initialStmt.substring(0, initialStmt.length() - 1) + + " WHERE " + QueryBuilder.token(YCSB_KEY) + + " >= token(" + QueryBuilder.bindMarker() + ")" + + " LIMIT " + QueryBuilder.bindMarker(); + stmt = session.prepare(scanStmt); + stmt.setConsistencyLevel(readConsistencyLevel); + if (trace) { + stmt.enableTracing(); + } + + PreparedStatement prevStmt = (fields == null) ? + SCAN_ALL_STMT.getAndSet(stmt) : + SCAN_STMTS.putIfAbsent(new HashSet<>(fields), stmt); + if (prevStmt != null) { + stmt = prevStmt; + } + } + + LOGGER.debug(stmt.getQueryString()); + LOGGER.debug("startKey = {}, recordcount = {}", startkey, recordcount); + + ResultSet rs = session.execute(stmt.bind(startkey, recordcount)); + + HashMap tuple; + while (!rs.isExhausted()) { + Row row = rs.one(); + tuple = new HashMap<>(); + + ColumnDefinitions cd = row.getColumnDefinitions(); + + for (ColumnDefinitions.Definition def : cd) { + ByteBuffer val = row.getBytesUnsafe(def.getName()); + if (val != null) { + tuple.put(def.getName(), new ByteArrayByteIterator(val.array())); + } else { + tuple.put(def.getName(), null); + } + } + + result.add(tuple); + } + + return Status.OK; + + } catch (Exception e) { + LOGGER.error( + MessageFormatter.format("Error scanning with startkey: {}", startkey).getMessage(), e); + return Status.ERROR; + } + + } + + /** + * Update a record in the database. Any field/value pairs in the specified + * values HashMap will be written into the record with the specified record + * key, overwriting any existing values with the same field name. + * + * @param table + * The name of the table + * @param key + * The record key of the record to write. + * @param values + * A HashMap of field/value pairs to update in the record + * @return Zero on success, a non-zero error code on error + */ + @Override + public Status update(String table, String key, Map values) { + + try { + Set fields = values.keySet(); + PreparedStatement stmt = UPDATE_STMTS.get(fields); + + // Prepare statement on demand + if (stmt == null) { + Update updateStmt = QueryBuilder.update(table); + + // Add fields + for (String field : fields) { + updateStmt.with(QueryBuilder.set(field, QueryBuilder.bindMarker())); + } + + // Add key + updateStmt.where(QueryBuilder.eq(YCSB_KEY, QueryBuilder.bindMarker())); + + if (lwt) { + updateStmt.where().ifExists(); + } + + stmt = session.prepare(updateStmt); + stmt.setConsistencyLevel(writeConsistencyLevel); + if (trace) { + stmt.enableTracing(); + } + + PreparedStatement prevStmt = UPDATE_STMTS.putIfAbsent(new HashSet<>(fields), stmt); + if (prevStmt != null) { + stmt = prevStmt; + } + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(stmt.getQueryString()); + LOGGER.debug("key = {}", key); + for (Map.Entry entry : values.entrySet()) { + LOGGER.debug("{} = {}", entry.getKey(), entry.getValue()); + } + } + + // Add fields + ColumnDefinitions vars = stmt.getVariables(); + BoundStatement boundStmt = stmt.bind(); + for (int i = 0; i < vars.size() - 1; i++) { + boundStmt.setString(i, values.get(vars.getName(i)).toString()); + } + + // Add key + boundStmt.setString(vars.size() - 1, key); + + session.execute(boundStmt); + + return Status.OK; + } catch (Exception e) { + LOGGER.error(MessageFormatter.format("Error updating key: {}", key).getMessage(), e); + } + + return Status.ERROR; + } + + private void addLegacyFieldsToInsertStatement(ColumnDefinitions vars, List fields, BoundStatement boundStmt) { + for(DatabaseField f : fields) { + boundStmt.setString(f.getFieldname(), f.getContent().asIterator().toString()); + } + } + private static Set arrayWrapperToSet(List wList) { + // explicitly untyped as we do not know the types yet + Set retVal = new HashSet<>(); + for(DataWrapper w : wList) { + if(w.isTerminal()) { + if(w.isLong()) { + retVal.add(w.asLong()); + } else if(w.isInteger()) { + retVal.add(w.asInteger()); + } else if(w.isString()) { + retVal.add(w.asString()); + } + } else if (w.isArray()) { + throw new UnsupportedOperationException("arrays of arrays are not supported (yet)"); + } else if(w.isNested()) { + throw new UnsupportedOperationException("arrays of non-primitive objects are not supported (yet)"); + } + } + return retVal; + } + private static UDTValue nestingToUdt(String fieldName, List nesting) { + UserType myUserType = cluster.getMetadata().getKeyspace(keyspace).getUserType(fieldName); + UDTValue udtValue = myUserType.newValue(); + for(DatabaseField f : nesting) { + String name = f.getFieldname(); + DataWrapper w = f.getContent(); + if(w.isTerminal()) { + if(w.isLong()) { + udtValue.setLong(name, w.asLong()); + } else if(w.isInteger()) { + udtValue.setInt(name, w.asInteger()); + } else if(w.isString()) { + udtValue.setString(name, w.asString()); + } + } else if (w.isArray()) { + throw new UnsupportedOperationException("UDTs with arrays are not supported (yet)"); + } else if(w.isNested()) { + throw new UnsupportedOperationException("nested objects with nested are not supported (yet)"); + } + } + return udtValue; + } + private static void addTypedFieldsToInsertStatement(List fields, BoundStatement boundStmt) { + for(DatabaseField f : fields) { + String name = f.getFieldname(); + DataWrapper w = f.getContent(); + if(w.isTerminal()) { + if(w.isLong()) { + boundStmt.setLong(name, w.asLong()); + } else if(w.isInteger()) { + boundStmt.setInt(name, w.asInteger()); + } else if(w.isString()) { + boundStmt.setString(name, w.asString()); + } else { + // assuming this is an iterator + // which is the only remaining terminal + Object o = w.asObject(); + byte[] b = (byte[]) o; + boundStmt.setString(name, new String(b)); + } + } else if(w.isArray()) { + // here, we blindly assume that all elements + // of the array are of the same type and fit + // ScyllaDBs Set type + List wList = w.arrayAsList(); + boundStmt.setSet(name, arrayWrapperToSet(wList)); + } else if(w.isNested()) { + // here, we blindly assume that a user defined + // data type has been added. + List nesting = w.asNested(); + boundStmt.setUDTValue(name, nestingToUdt(name, nesting)); + } + } + } + /** + * Insert a record in the database. Any field/value pairs in the specified + * values HashMap will be written into the record with the specified record + * key. + * + * @param table + * The name of the table + * @param key + * The record key of the record to insert. + * @param values + * A HashMap of field/value pairs to insert in the record + * @return Zero on success, a non-zero error code on error + */ + @Override + public Status insert(String table, String key, List values) { + + Set fields = new HashSet<>(); + values.forEach(v -> fields.add(v.getFieldname())); + PreparedStatement stmt = ScyllaStatementHandler.getOrBuildPreparedInsertStatement(fields, table); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(stmt.getQueryString()); + LOGGER.debug("key = {}", key); + for (DatabaseField field : values) { + LOGGER.debug("{} = {}", field.getFieldname(), field.getContent()); + } + } + // Add key + BoundStatement boundStmt = stmt.bind().setString(0, key); + // Add fields + ColumnDefinitions vars = stmt.getVariables(); + if(useTypedFields) { + addTypedFieldsToInsertStatement(values, boundStmt); + } else { + addLegacyFieldsToInsertStatement(vars, values, boundStmt); + } + Statement toSend; + if(batchSize < 2) { + toSend = boundStmt; + } else { + batch.add(boundStmt); + if(batch.size() < batchSize) { + return Status.BATCHED_OK; + } + toSend = batch; + batch = new BatchStatement(Type.UNLOGGED); + toSend.setConsistencyLevel(writeConsistencyLevel); + } + if (trace) { + toSend.enableTracing(); + } + try { + session.execute(toSend); + return Status.OK; + } catch(Exception ex) { + LOGGER.error(MessageFormatter.format("Error inserting key: {}", key).getMessage(), ex); + } + return Status.ERROR; + } + + /** + * Delete a record from the database. + * + * @param table + * The name of the table + * @param key + * The record key of the record to delete. + * @return Zero on success, a non-zero error code on error + */ + @Override + public Status delete(String table, String key) { + if(debug) { + System.err.println("deleting key " + key); + } + try { + PreparedStatement stmt = DELETE_STMT.get(); + + // Prepare statement on demand + if (stmt == null) { + Delete s = QueryBuilder.delete().from(table); + s.where(QueryBuilder.eq(YCSB_KEY, QueryBuilder.bindMarker())); + + if (lwt) { + s.ifExists(); + } + + stmt = session.prepare(s); + stmt.setConsistencyLevel(writeConsistencyLevel); + if (trace) { + stmt.enableTracing(); + } + + PreparedStatement prevStmt = DELETE_STMT.getAndSet(stmt); + if (prevStmt != null) { + stmt = prevStmt; + } + } + if(LOGGER.isDebugEnabled()) { + LOGGER.debug(stmt.getQueryString()); + LOGGER.debug("key = {}", key); + } + + ResultSet rs = session.execute(stmt.bind(key)); + if(rs.wasApplied()) { + return Status.OK; + } + return Status.NOT_FOUND; + } catch (Exception e) { + LOGGER.error(MessageFormatter.format("Error deleting key: {}", key).getMessage(), e); + } + + return Status.ERROR; + } + + @Override + public Status findOne(String table, List filters, Set fields, + Map result) { + if(debug) { + System.err.println("finding one."); + } + if(fields != null) throw new IllegalArgumentException("reading specific fields is not supported yet."); + if(false == useTypedFields) throw new IllegalStateException("find one does not work without typed fields"); + PreparedStatement s = ScyllaStatementHandler.getOrBuildPreparedFindOneStatement(table, filters, fields); + if(debug) { + System.err.println(s.toString()); + s.enableTracing(); + } + BoundStatement bound = s.bind(); + // we do not need the next index + ScyllaStatementHandler.bindPreparedFineOneStatement(bound, table, filters, fields); + ResultSet rs = session.execute( bound ); + List results = rs.all(); + if(results.size() == 0) { + return Status.NOT_FOUND; + } + if(results.size() > 1) { + return Status.UNEXPECTED_STATE; + } + Row row = results.get(0); + if(debug) { + System.err.println(row.toString()); + } + for (ColumnDefinitions.Definition def : rs.getColumnDefinitions()) { + ByteBuffer val = row.getBytesUnsafe(def.getName()); + if (val != null) { + result.put(def.getName(), new ByteArrayByteIterator(val.array())); + } else { + result.put(def.getName(), null); + } + } + return Status.OK; + } + @Override + public Status updateOne(String table, List filters, List fields) { + if(false == useTypedFields) throw new IllegalStateException("find one does not work without typed fields"); + Map result = new HashMap<>(); + Status stat = findOne(table, filters, null, result); + if(stat != Status.OK) { + return stat; + } + ByteIterator iterator = result.get(YCSB_KEY); + String myPrimaryKey = iterator.toString(); + PreparedStatement stmt = ScyllaStatementHandler.getOrBuildPreparedUpdateOneStatement(table, fields); + BoundStatement bound = stmt.bind(); + ScyllaStatementHandler.bindPreparedUpdateOneStatment(myPrimaryKey, 0, fields, bound); + System.err.println(bound.toString()); + ResultSet rs = session.execute( bound ); + List results = rs.all(); + if(results.size() > 0) { + return Status.UNEXPECTED_STATE; + } + return Status.OK; + } +} diff --git a/scylla/src/main/java/site/ycsb/db/scylla/ScyllaDbConstants.java b/scylla/src/main/java/site/ycsb/db/scylla/ScyllaDbConstants.java new file mode 100644 index 0000000..f9ba85e --- /dev/null +++ b/scylla/src/main/java/site/ycsb/db/scylla/ScyllaDbConstants.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db.scylla; + +import com.datastax.driver.core.ConsistencyLevel; + +public final class ScyllaDbConstants { + + public static final String INIT_TABLE = "scylla.inittable"; + public static final String INDEX_LIST_PROPERTY = "scylla.indexlist"; + public static final String YCSB_KEY = "y_id"; + public static final String KEYSPACE_PROPERTY = "scylla.keyspace"; + public static final String KEYSPACE_PROPERTY_DEFAULT = "ycsb"; + public static final String USERNAME_PROPERTY = "scylla.username"; + public static final String PASSWORD_PROPERTY = "scylla.password"; + + public static final String HOSTS_PROPERTY = "scylla.hosts"; + public static final String PORT_PROPERTY = "scylla.port"; + public static final String PORT_PROPERTY_DEFAULT = "9042"; + + public static final String READ_CONSISTENCY_LEVEL_PROPERTY = "scylla.readconsistencylevel"; + public static final String READ_CONSISTENCY_LEVEL_PROPERTY_DEFAULT = ConsistencyLevel.QUORUM.name(); + public static final String WRITE_CONSISTENCY_LEVEL_PROPERTY = "scylla.writeconsistencylevel"; + public static final String WRITE_CONSISTENCY_LEVEL_PROPERTY_DEFAULT = ConsistencyLevel.QUORUM.name(); + + public static final String MAX_CONNECTIONS_PROPERTY = "scylla.maxconnections"; + public static final String CORE_CONNECTIONS_PROPERTY = "scylla.coreconnections"; + public static final String CONNECT_TIMEOUT_MILLIS_PROPERTY = "scylla.connecttimeoutmillis"; + public static final String READ_TIMEOUT_MILLIS_PROPERTY = "scylla.readtimeoutmillis"; + + public static final String REPLICATION_STRATEGY_CLASS_PROPERTY = "scylla.replicationclass"; + public static final String REPLICATION_STRATEGY_CLASS_DEFAULT = "org.apache.cassandra.locator.SimpleStrategy"; + + public static final String REPLICATION_DEGREE_PROPERTY = "scylla.replicationdegree"; + public static final String REPLICATION_DEGREE_DEFAULT = "1"; + + public static final String SCYLLA_LWT = "scylla.lwt"; + + public static final String TOKEN_AWARE_LOCAL_DC = "scylla.local_dc"; + + public static final String TRACING_PROPERTY = "scylla.tracing"; + public static final String TRACING_PROPERTY_DEFAULT = "false"; + + public static final String USE_SSL_CONNECTION = "scylla.useSSL"; + public static final String DEFAULT_USE_SSL_CONNECTION = "false"; + + private ScyllaDbConstants() { + // empty + } +} diff --git a/scylla/src/main/java/site/ycsb/db/scylla/ScyllaDbInitHelper.java b/scylla/src/main/java/site/ycsb/db/scylla/ScyllaDbInitHelper.java new file mode 100644 index 0000000..ec0a7c7 --- /dev/null +++ b/scylla/src/main/java/site/ycsb/db/scylla/ScyllaDbInitHelper.java @@ -0,0 +1,150 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db.scylla; + +import static site.ycsb.db.scylla.ScyllaDbConstants.REPLICATION_DEGREE_DEFAULT; +import static site.ycsb.db.scylla.ScyllaDbConstants.REPLICATION_DEGREE_PROPERTY; +import static site.ycsb.db.scylla.ScyllaDbConstants.REPLICATION_STRATEGY_CLASS_DEFAULT; +import static site.ycsb.db.scylla.ScyllaDbConstants.REPLICATION_STRATEGY_CLASS_PROPERTY; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.exceptions.InvalidTypeException; +import com.datastax.driver.core.schemabuilder.Create; +import com.datastax.driver.core.schemabuilder.SchemaBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; + +import site.ycsb.db.scylla.ScyllaCQLClient.IndexDescriptor; +import site.ycsb.workloads.core.CoreConstants; +import site.ycsb.workloads.schema.SchemaHolder; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumn; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnKind; +import site.ycsb.workloads.schema.SchemaHolder.SchemaColumnType; + +public final class ScyllaDbInitHelper { + + private static final SchemaHolder schema = SchemaHolder.INSTANCE; + + static Session createKeyspaceAndSchema(Properties props, String keyspace, Cluster cluster) { + String tableName = props.getProperty(CoreConstants.TABLENAME_PROPERTY, CoreConstants.TABLENAME_PROPERTY_DEFAULT); + Session session = cluster.connect(); + Map replication = new HashMap<>(); + replication.put("class", props.getProperty(REPLICATION_STRATEGY_CLASS_PROPERTY, REPLICATION_STRATEGY_CLASS_DEFAULT)); + replication.put("replication_factor", props.getProperty(REPLICATION_DEGREE_PROPERTY, REPLICATION_DEGREE_DEFAULT)); + Statement stmt = SchemaBuilder.createKeyspace(keyspace).ifNotExists().with() + .durableWrites(true).replication(replication).enableTracing(); + // REPLICATION = {'class': 'org.apache.cassandra.locator.SimpleStrategy', 'replication_factor': 3} + final String s = stmt.toString(); + System.err.println(s); + session.execute(stmt); + + + createTable(keyspace, tableName, session); + session.close(); + return cluster.connect(keyspace); + } + + private static void createTable(String keyspace, String tableName, Session session) { + Create create = SchemaBuilder.createTable(keyspace, tableName); + create.addPartitionKey(ScyllaDbConstants.YCSB_KEY, DataType.text()); + for(SchemaColumn column : schema.getOrderedListOfColumns()) { + if(column.getColumnKind() == SchemaColumnKind.SCALAR) { + if(column.getColumnType() == SchemaColumnType.TEXT) + create.addColumn(column.getColumnName(), DataType.text()); + else if( column.getColumnType() == SchemaColumnType.INT) + create.addColumn(column.getColumnName(), DataType.cint()); + else if( column.getColumnType() == SchemaColumnType.LONG) + create.addColumn(column.getColumnName(), DataType.bigint()); + } else if(column.getColumnKind() == SchemaColumnKind.ARRAY) { + throw new InvalidTypeException ("array column types currently not supported"); + } else if(column.getColumnKind() == SchemaColumnKind.NESTED) { + throw new InvalidTypeException ("nested column types currently not supported"); + } else { + throw new InvalidTypeException("other column types currently not supported"); + } + } + ResultSet rs = session.execute(create); + } + + static List getIndexList(Properties props) { + String indexeslist = props.getProperty(ScyllaDbConstants.INDEX_LIST_PROPERTY); + if(indexeslist == null) { + return Collections.emptyList(); + } + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = null; + try { + root = mapper.readTree(indexeslist); + } catch(IOException ex) { + throw new RuntimeException(ex); + } + if(!root.isArray()) { + throw new IllegalArgumentException("index specification must be a JSON array"); + } + ArrayNode array = (ArrayNode) root; + if(array.size() == 0) { + return Collections.emptyList(); + } + List retVal = new ArrayList<>(); + for(int i = 0; i < array.size(); i++) { + JsonNode el = array.get(i); + if(!el.isObject()) { + throw new IllegalArgumentException("index elements must be a JSON object"); + } + + IndexDescriptor desc = new IndexDescriptor(); + ObjectNode object = (ObjectNode) el; + JsonNode name = object.get("name"); + if(name == null || !name.isTextual()) { + throw new IllegalArgumentException("index elements must be a JSON object with 'name' of type string"); + } + desc.name = ((TextNode) name).asText(); + + JsonNode sorts = object.get("columns"); + if(sorts == null || !sorts.isArray()) { + throw new IllegalArgumentException("index elements must be a JSON object with 'columns' set as an array of strings"); + } + ArrayNode columnsArray = (ArrayNode) sorts; + for(int j = 0; j < columnsArray.size(); j++) { + JsonNode sortEl = columnsArray.get(j); + if(sortEl == null || !sortEl.isTextual()) { + throw new IllegalArgumentException("index elements must be a JSON object with 'sortAttributes' set as an array of strings"); + } + desc.columnNames.add(((TextNode) sortEl).asText()); + } + retVal.add(desc); + } + return retVal; + } + + private ScyllaDbInitHelper() { + // empty + } +} diff --git a/scylla/src/main/java/site/ycsb/db/scylla/ScyllaStatementHandler.java b/scylla/src/main/java/site/ycsb/db/scylla/ScyllaStatementHandler.java new file mode 100644 index 0000000..9a5ab51 --- /dev/null +++ b/scylla/src/main/java/site/ycsb/db/scylla/ScyllaStatementHandler.java @@ -0,0 +1,160 @@ +/* + * Copyright 2023-2024 benchANT GmbH. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ +package site.ycsb.db.scylla; + +import static site.ycsb.db.scylla.ScyllaDbConstants.YCSB_KEY; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.querybuilder.Insert; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; +import com.datastax.driver.core.querybuilder.Update; + +import site.ycsb.wrappers.Comparison; +import site.ycsb.wrappers.DatabaseField; + +public final class ScyllaStatementHandler { + + private static final ConcurrentMap, PreparedStatement> INSERT_STMTS = new ConcurrentHashMap<>(); + private static final ConcurrentMap, PreparedStatement> FIND_ONE_STMTS = new ConcurrentHashMap<>(); + private static final ConcurrentMap, PreparedStatement> UPDATE_ONE_STMTS = new ConcurrentHashMap<>(); + + static int bindPreparedFineOneStatement(BoundStatement stmt, String table, List filters, Set fields) { + if(fields != null) throw new IllegalArgumentException("reading specific fields is not supported yet."); + return FilterBuilder.bindFilter(0, stmt, filters); + } + static PreparedStatement getOrBuildPreparedFindOneStatement(String table, List filters, Set fields) { + if(fields != null) throw new IllegalArgumentException("reading specific fields is not supported yet."); + // fields is null, so it does not have to be considered; will need to be changed at some later stage + final List normalizedFilters = new ArrayList<>(); + for(Comparison c : filters) { + normalizedFilters.add(c.normalized()); + } + PreparedStatement stmt = FIND_ONE_STMTS.get(normalizedFilters); + if(stmt != null) return stmt; + // does not exist, create one + Select s = QueryBuilder.select().all() + .from(ScyllaCQLClient.keyspace, table) + .limit(1).allowFiltering(); + FilterBuilder.buildPreparedFilter(normalizedFilters, s.where()); + synchronized(FIND_ONE_STMTS) { + // someone may have added the element here, try again + stmt = FIND_ONE_STMTS.get(normalizedFilters); + if(stmt != null) return stmt; + stmt = ScyllaCQLClient.session.prepare(s); + stmt.setConsistencyLevel(ScyllaCQLClient.writeConsistencyLevel); + stmt.setIdempotent(true); + if (ScyllaCQLClient.trace) { + stmt.enableTracing(); + } + PreparedStatement prevStmt = FIND_ONE_STMTS.putIfAbsent(normalizedFilters, stmt); + if (prevStmt != null) { + stmt = prevStmt; + } + } + return stmt; + } + static void bindPreparedUpdateOneStatment(String primKey, int startIndex, List fields, BoundStatement bound) { + int nextIndex = FilterBuilder.bindPreparedAssignments(bound, fields, startIndex); + bound.setString(nextIndex, primKey); + } + static PreparedStatement getOrBuildPreparedUpdateOneStatement(String table, List fields) { + final Set normalizedFields = new HashSet<>(); + for(DatabaseField f : fields) { + if(!f.getContent().isTerminal()) { + throw new IllegalArgumentException("nested elements are not supported for scylla"); + } + normalizedFields.add(f.getFieldname()); + } + PreparedStatement stmt = UPDATE_ONE_STMTS.get(normalizedFields); + if(stmt != null) return stmt; + // does not exist, create one + Update u = QueryBuilder.update(ScyllaCQLClient.keyspace, table); + FilterBuilder.buildPreparedAssignments(fields, u.with()); + u.where(QueryBuilder.eq(YCSB_KEY, QueryBuilder.bindMarker())); + if(ScyllaCQLClient.debug) { + u.enableTracing(); + } + synchronized(UPDATE_ONE_STMTS) { + // someone may have added the element here, try again + stmt = UPDATE_ONE_STMTS.get(normalizedFields); + if(stmt != null) return stmt; + stmt = ScyllaCQLClient.session.prepare(u); + stmt.setConsistencyLevel(ScyllaCQLClient.writeConsistencyLevel); + stmt.setIdempotent(false); + if (ScyllaCQLClient.trace) { + stmt.enableTracing(); + } + PreparedStatement prevStmt = UPDATE_ONE_STMTS.putIfAbsent(normalizedFields, stmt); + if (prevStmt != null) { + stmt = prevStmt; + } + } + return stmt; + } + + static PreparedStatement getOrBuildPreparedInsertStatement(Set fields, String table) { + PreparedStatement stmt = INSERT_STMTS.get(fields); + if(stmt != null) return stmt; + // does not exist, create one + Insert insertStmt = QueryBuilder.insertInto(table); + + // Add key + insertStmt.value(YCSB_KEY, QueryBuilder.bindMarker()); + + // Add fields + for (String field : fields) { + insertStmt.value(field, QueryBuilder.bindMarker()); + } + + if (ScyllaCQLClient.lwt) { + insertStmt.ifNotExists(); + } + // we cannot risk running this part concurrently, so let's lock it + // insert statements are the same anyway, so this will only lock once + synchronized(INSERT_STMTS) { + // someone may have added the element here, try again + stmt = INSERT_STMTS.get(fields); + if(stmt != null) return stmt; + stmt = ScyllaCQLClient.session.prepare(insertStmt); + stmt.setConsistencyLevel(ScyllaCQLClient.writeConsistencyLevel); + if (ScyllaCQLClient.trace) { + stmt.enableTracing(); + } + PreparedStatement prevStmt = INSERT_STMTS.putIfAbsent(new HashSet<>(fields), stmt); + if (prevStmt != null) { + stmt = prevStmt; + } + } + return stmt; + } + + static void cleanup() { + INSERT_STMTS.clear(); + FIND_ONE_STMTS.clear(); + } + private ScyllaStatementHandler() { + // never called + } +} diff --git a/scylla/src/main/java/site/ycsb/db/scylla/package-info.java b/scylla/src/main/java/site/ycsb/db/scylla/package-info.java new file mode 100644 index 0000000..8cc9d1e --- /dev/null +++ b/scylla/src/main/java/site/ycsb/db/scylla/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 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. + */ + +/** + * The YCSB binding for scylla + * via CQL. + */ +package site.ycsb.db.scylla; + diff --git a/scylla/src/test/java/site/ycsb/db/scylla/ScyllaCQLClientTest.java b/scylla/src/test/java/site/ycsb/db/scylla/ScyllaCQLClientTest.java new file mode 100644 index 0000000..20fcb17 --- /dev/null +++ b/scylla/src/test/java/site/ycsb/db/scylla/ScyllaCQLClientTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2020 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 site.ycsb.db.scylla; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import com.google.common.collect.Sets; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.querybuilder.Insert; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; +import site.ycsb.ByteIterator; +import site.ycsb.Status; +import site.ycsb.StringByteIterator; +import site.ycsb.measurements.Measurements; +import site.ycsb.workloads.CoreWorkload; +import site.ycsb.wrappers.DatabaseField; +import site.ycsb.wrappers.Wrappers; + +import static site.ycsb.db.scylla.ScyllaDbConstants.*; + +import org.cassandraunit.CassandraCQLUnit; +import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Integration tests for the Scylla client + */ +public class ScyllaCQLClientTest { + // Change the default Scylla timeout from 10s to 120s for slow CI machines + private final static long timeout = 120000L; + + private final static String TABLE = "usertable"; + private final static String HOST = "localhost"; + private final static int PORT = 9142; + private final static String DEFAULT_ROW_KEY = "user1"; + + private ScyllaCQLClient client; + private Session session; + + @ClassRule + public static CassandraCQLUnit unit = new CassandraCQLUnit( + new ClassPathCQLDataSet("ycsb.cql", "ycsb"), null, timeout); + + @Before + public void setUp() throws Exception { + session = unit.getSession(); + + Properties p = new Properties(); + p.setProperty("scylla.hosts", HOST); + p.setProperty("scylla.port", Integer.toString(PORT)); + p.setProperty("scylla.table", TABLE); + + Measurements.setProperties(p); + + final CoreWorkload workload = new CoreWorkload(); + workload.init(p); + + client = new ScyllaCQLClient(); + client.setProperties(p); + client.init(); + } + + @After + public void tearDownClient() throws Exception { + if (client != null) { + client.cleanup(); + } + client = null; + } + + @After + public void clearTable() { + // Clear the table so that each test starts fresh. + final Statement truncate = QueryBuilder.truncate(TABLE); + if (unit != null) { + unit.getSession().execute(truncate); + } + } + + @Test + public void testReadMissingRow() { + final HashMap result = new HashMap<>(); + final Status status = client.read(TABLE, "Missing row", null, result); + assertThat(result.size(), is(0)); + assertThat(status, is(Status.NOT_FOUND)); + } + + private void insertRow() { + Insert insertStmt = QueryBuilder.insertInto(TABLE); + insertStmt.value(YCSB_KEY, DEFAULT_ROW_KEY); + + insertStmt.value("field0", "value1"); + insertStmt.value("field1", "value2"); + session.execute(insertStmt); + } + + @Test + public void testRead() { + insertRow(); + + final HashMap result = new HashMap<>(); + final Status status = client.read(TABLE, DEFAULT_ROW_KEY, null, result); + assertThat(status, is(Status.OK)); + assertThat(result.entrySet(), hasSize(11)); + assertThat(result, hasEntry("field2", null)); + + final HashMap strResult = new HashMap<>(); + for (final Map.Entry e : result.entrySet()) { + if (e.getValue() != null) { + strResult.put(e.getKey(), e.getValue().toString()); + } + } + assertThat(strResult, hasEntry(YCSB_KEY, DEFAULT_ROW_KEY)); + assertThat(strResult, hasEntry("field0", "value1")); + assertThat(strResult, hasEntry("field1", "value2")); + } + + @Test + public void testReadSingleColumn() { + insertRow(); + final HashMap result = new HashMap<>(); + final Set fields = Sets.newHashSet("field1"); + final Status status = client.read(TABLE, DEFAULT_ROW_KEY, fields, result); + assertThat(status, is(Status.OK)); + assertThat(result.entrySet(), hasSize(1)); + final Map strResult = StringByteIterator.getStringMap(result); + assertThat(strResult, hasEntry("field1", "value2")); + } + + @Test + public void testInsert() { + final String key = "key"; + final List input = new ArrayList<>(); + input.add(new DatabaseField("field0", Wrappers.wrapIterator(new StringByteIterator("value1")))); + input.add(new DatabaseField("field1", Wrappers.wrapIterator(new StringByteIterator("value2")))); + + final Status status = client.insert(TABLE, key, input); + assertThat(status, is(Status.OK)); + + // Verify result + final Select selectStmt = + QueryBuilder.select("field0", "field1") + .from(TABLE) + .where(QueryBuilder.eq(YCSB_KEY, key)) + .limit(1); + + final ResultSet rs = session.execute(selectStmt); + final Row row = rs.one(); + assertThat(row, notNullValue()); + assertThat(rs.isExhausted(), is(true)); + assertThat(row.getString("field0"), is("value1")); + assertThat(row.getString("field1"), is("value2")); + } + + @Test + public void testUpdate() { + insertRow(); + final Map input = new HashMap<>(); + input.put("field0", "new-value1"); + input.put("field1", "new-value2"); + + final Status status = client.update(TABLE, + DEFAULT_ROW_KEY, + StringByteIterator.getByteIteratorMap(input)); + assertThat(status, is(Status.OK)); + + // Verify result + final Select selectStmt = + QueryBuilder.select("field0", "field1") + .from(TABLE) + .where(QueryBuilder.eq(YCSB_KEY, DEFAULT_ROW_KEY)) + .limit(1); + + final ResultSet rs = session.execute(selectStmt); + final Row row = rs.one(); + assertThat(row, notNullValue()); + assertThat(rs.isExhausted(), is(true)); + assertThat(row.getString("field0"), is("new-value1")); + assertThat(row.getString("field1"), is("new-value2")); + } + + @Test + public void testDelete() { + insertRow(); + + final Status status = client.delete(TABLE, DEFAULT_ROW_KEY); + assertThat(status, is(Status.OK)); + + // Verify result + final Select selectStmt = + QueryBuilder.select("field0", "field1") + .from(TABLE) + .where(QueryBuilder.eq(YCSB_KEY, DEFAULT_ROW_KEY)) + .limit(1); + + final ResultSet rs = session.execute(selectStmt); + final Row row = rs.one(); + assertThat(row, nullValue()); + } + + @Test + public void testPreparedStatements() { + final int LOOP_COUNT = 3; + for (int i = 0; i < LOOP_COUNT; i++) { + testInsert(); + testUpdate(); + testRead(); + testReadSingleColumn(); + testReadMissingRow(); + testDelete(); + } + } +} diff --git a/scylla/src/test/resources/ycsb.cql b/scylla/src/test/resources/ycsb.cql new file mode 100644 index 0000000..9166983 --- /dev/null +++ b/scylla/src/test/resources/ycsb.cql @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2020 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. + */ + +CREATE TABLE usertable ( + y_id varchar primary key, + field0 varchar, + field1 varchar, + field2 varchar, + field3 varchar, + field4 varchar, + field5 varchar, + field6 varchar, + field7 varchar, + field8 varchar, + field9 varchar); diff --git a/workloads/indexes/airport/default b/workloads/indexes/airport/default new file mode 100644 index 0000000..ba47245 --- /dev/null +++ b/workloads/indexes/airport/default @@ -0,0 +1,5 @@ +scylla.indexlist=[ { "name": "stops_idx", "columns": [ "stops" ] }, { "name": "dest_airport_idx", "columns": [ "dst_airport" ] },{ "name": "alias_idx", "columns": [ "airline_alias" ] }, {"name": "airplane_idx", "columns": ["airplane"] }, { "name" : "src_airport_idx", "columns": [ "src_airport" ] } ] +jdbc.indexlist=[ { "name": "alias_idx", "order": "ASC", "concurrent": true, "method": "btree", "columns": [ "airline_alias" ] }, { "name" : "src_airport_idx", "order": "ASC", "concurrent": true, "method": "btree", "columns": [ "src_airport" ] }, {"name": "airplane_idx", "order": "ASC", "concurrent": true, "method": "btree", "columns": ["airplane"] }, { "name": "mixed_idx", "order": "ASC", "concurrent": true, "method": "btree", "columns": [ "src_airport", "dst_airport", "stops" ] }, { "name": "codeshares_idx", "order": "ASC", "concurrent": true, "method": "btree", "columns": [ "codeshares_0", "codeshares_1", "codeshares_2" ] } ] +mongodb.indexlist=[ { src_airport: 1 } , { airplane: 1 }, { codeshares: 1 }, { "airline.alias" : 1 }, { src_airport: 1, dest_airport: 1, stops: 1 } ] +couchbase.indexlist=[{ "isPrimary": true }, { "name": "srcAirport-index", "fields": ["src_airport"] }, { "name" : "airplane-index", "fields": [ "airplane" ] }, { "name" : "codeshares-index", "fields": [ "codeshares" ] }, { "name" : "airlinealias-index", "fields": [ "airline.alias" ] }, { "name" : "composite-index", "fields": [ "src_airport", "dst_airport", "stops" ] }] +dynamodb.indexlist=[ { "name": "alias_idx", "hashAttribute": "airline_alias", "sortAttributes": ["hash_key"] }, { "name": "dst_airport_idx", "hashAttribute": "dst_airport", "sortAttributes": ["hash_key"] }, { "name": "airplane_idx", "hashAttribute": "airplane", "sortAttributes": ["hash_key"] }, { "name": "stops_idx", "hashAttribute": "stops", "sortAttributes": ["hash_key"] }, { "name": "src_airport_idx", "hashAttribute": "src_airport", "sortAttributes": ["hash_key"] } ] \ No newline at end of file diff --git a/workloads/tsworkload_template b/workloads/tsworkload_template new file mode 100644 index 0000000..70ce774 --- /dev/null +++ b/workloads/tsworkload_template @@ -0,0 +1,283 @@ +# Copyright (c) 2017 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. + +# Yahoo! Cloud System Benchmark +# Time Series Workload Template: Default Values +# +# File contains all properties that can be set to define a +# YCSB session. All properties are set to their default +# value if one exists. If not, the property is commented +# out. When a property has a finite number of settings, +# the default is enabled and the alternates are shown in +# comments below it. +# +# Use of each property is explained through comments in Client.java, +# CoreWorkload.java, TimeSeriesWorkload.java or on the YCSB wiki page: +# https://github.com/brianfrankcooper/YCSB/wiki/TimeSeriesWorkload + +# The name of the workload class to use. Always the following. +workload=site.ycsb.workloads.TimeSeriesWorkload + +# The default is Java's Long.MAX_VALUE. +# The number of records in the table to be inserted in +# the load phase or the number of records already in the +# table before the run phase. +recordcount=1000000 + +# There is no default setting for operationcount but it is +# required to be set. +# The number of operations to use during the run phase. +operationcount=3000000 + +# The number of insertions to do, if different from recordcount. +# Used with insertstart to grow an existing table. +#insertcount= + +# ..::NOTE::.. This is different from the CoreWorkload! +# The starting timestamp of a run as a Unix Epoch numeral in the +# unit set in 'timestampunits'. This is used to determine what +# the first timestamp should be when writing or querying as well +# as how many offsets (based on 'timestampinterval'). +#insertstart= + +# The units represented by the 'insertstart' timestamp as well as +# durations such as 'timestampinterval', 'querytimespan', etc. +# For values, see https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/TimeUnit.html +# Note that only seconds through nanoseconds are supported. +timestampunits=SECONDS + +# The amount of time between each value in every time series in +# the units of 'timestampunits'. +timestampinterval=60 + +# ..::NOTE::.. This is different from the CoreWorkload! +# Represents the number of unique "metrics" or "keys" for time series. +# E.g. "sys.cpu" may be a single field or "metric" while there may be many +# time series sharing that key (perhaps a host tag with "web01" and "web02" +# as options). +fieldcount=16 + +# The number of characters in the "metric" or "key". +fieldlength=8 + +# --- TODO ---? +# The distribution used to choose the length of a field +fieldlengthdistribution=constant +#fieldlengthdistribution=uniform +#fieldlengthdistribution=zipfian + +# The number of unique tag combinations for each time series. E.g +# if this value is 4, each record will have a key and 4 tag combinations +# such as A=A, B=A, C=A, D=A. +tagcount=4 + +# The cardinality (number of unique values) of each tag value for +# every "metric" or field as a comma separated list. Each value must +# be a number from 1 to Java's Integer.MAX_VALUE and there must be +# 'tagcount' values. If there are more or fewer values than +#'tagcount' then either it is ignored or 1 is substituted respectively. +tagcardinality=1,2,4,8 + +# The length of each tag key in characters. +tagkeylength=8 + +# The length of each tag value in characters. +tagvaluelength=8 + +# The character separating tag keys from tag values when reads, deletes +# or scans are executed against a database. The default is the equals sign +# so a field passed in a read to a DB may look like 'AA=AB'. +tagpairdelimiter== + +# The delimiter between keys and tags when a delete is passed to the DB. +# E.g. if there was a key and a field, the request key would look like: +# 'AA:AA=AB' +deletedelimiter=: + +# Whether or not to randomize the timestamp order when performing inserts +# and updates against a DB. By default all writes perform with the +# timestamps moving linearly forward in time once all time series for a +# given key have been written. +randomwritetimestamporder=false + +# Whether or not to randomly shuffle the time series order when writing. +# This will shuffle the keys, tag keys and tag values. +# ************************************************************************ +# WARNING - When this is enabled, reads and scans will likely return many +# empty results as invalid tag combinations will be chosen. Likewise +# this setting is INCOMPATIBLE with data integrity checks. +# ************************************************************************ +randomtimeseriesorder=false + +# The type of numerical data generated for each data point. The values are +# 64 bit signed integers, double precision floating points or a random mix. +# For data integrity, this setting is ignored and values are switched to +# 64 bit signed ints. +#valuetype=integers +valuetype=floats +#valuetype=mixed + +# A value from 0 to 0.999999 representing how sparse each time series +# should be. The higher this value, the greater the time interval between +# values in a single series. For example, if sparsity is 0 and there are +# 10 time series with a 'timestampinterval' of 60 seconds with a total +# time range of 10 intervals, you would see 100 values written, one per +# timestamp interval per time series. If the sparsity is 0.50 then there +# would be only about 50 values written so some time series would have +# missing values at each interval. +sparsity=0.00 + +# The percentage of time series that are "lagging" behind the current +# timestamp of the writer. This is used to mimic a common behavior where +# most sources (agents, sensors, etc) are writing data in sync (same timestamp) +# but a subset are running behind due to buffering, latency issues, etc. +delayedSeries=0.10 + +# The maximum amount of delay for delayed series in interval counts. The +# actual delay is chosen based on a modulo of the series index. +delayedIntervals=5 + +# The fixed or maximum amount of time added to the start time of a +# read or scan operation to generate a query over a range of time +# instead of a single timestamp. Units are shared with 'timestampunits'. +# For example if the value is set to 3600 seconds (1 hour) then +# each read would pick a random start timestamp based on the +#'insertstart' value and number of intervals, then add 3600 seconds +# to create the end time of the query. If this value is 0 then reads +# will only provide a single timestamp. +# WARNING: Cannot be used with 'dataintegrity'. +querytimespan=0 + +# Whether or not reads should choose a random time span (aligned to +# the 'timestampinterval' value) for each read or scan request starting +# at 0 and reaching 'querytimespan' as the max. +queryrandomtimespan=false + +# A delimiter character used to separate the start and end timestamps +# of a read query when 'querytimespan' is enabled. +querytimespandelimiter=, + +# A unique key given to read, scan and delete operations when the +# operation should perform a group-by (multi-series aggregation) on one +# or more tags. If 'groupbyfunction' is set, this key will be given with +# the configured function. +groupbykey=YCSBGB + +# A function name (e.g. 'sum', 'max' or 'avg') passed during reads, +# scans and deletes to cause the database to perform a group-by +# operation on one or more tags. If this value is empty or null +# (default), group-by operations are not performed +#groupbyfunction= + +# A comma separated list of 0s or 1s to denote which of the tag keys +# should be grouped during group-by operations. The number of values +# must match the number of tags in 'tagcount'. +#groupbykeys=0,0,1,1 + +# A unique key given to read and scan operations when the operation +# should downsample the results of a query into lower resolution +# data. If 'downsamplingfunction' is set, this key will be given with +# the configured function. +downsamplingkey=YCSBDS + +# A function name (e.g. 'sum', 'max' or 'avg') passed during reads and +# scans to cause the database to perform a downsampling operation +# returning lower resolution data. If this value is empty or null +# (default), downsampling is not performed. +#downsamplingfunction= + +# A time interval for which to downsample the raw data into. Shares +# the same units as 'timestampinterval'. This value must be greater +# than 'timestampinterval'. E.g. if the timestamp interval for raw +# data is 60 seconds, the downsampling interval could be 3600 seconds +# to roll up the data into 1 hour buckets. +#downsamplinginterval= + +# What proportion of operations are reads +readproportion=0.10 + +# What proportion of operations are updates +updateproportion=0.00 + +# What proportion of operations are inserts +insertproportion=0.90 + +# The distribution of requests across the keyspace +requestdistribution=zipfian +#requestdistribution=uniform +#requestdistribution=latest + +# The name of the database table to run queries against +table=usertable + +# Whether or not data should be validated during writes and reads. If +# set then the data type is always a 64 bit signed integer and is the +# hash code of the key, timestamp and tags. +dataintegrity=false + +# How the latency measurements are presented +measurementtype=histogram +#measurementtype=timeseries +#measurementtype=raw +# When measurementtype is set to raw, measurements will be output +# as RAW datapoints in the following csv format: +# "operation, timestamp of the measurement, latency in us" +# +# Raw datapoints are collected in-memory while the test is running. Each +# data point consumes about 50 bytes (including java object overhead). +# For a typical run of 1 million to 10 million operations, this should +# fit into memory most of the time. If you plan to do 100s of millions of +# operations per run, consider provisioning a machine with larger RAM when using +# the RAW measurement type, or split the run into multiple runs. +# +# Optionally, you can specify an output file to save raw datapoints. +# Otherwise, raw datapoints will be written to stdout. +# The output file will be appended to if it already exists, otherwise +# a new output file will be created. +#measurement.raw.output_file = /tmp/your_output_file_for_this_run + +# JVM Reporting. +# +# Measure JVM information over time including GC counts, max and min memory +# used, max and min thread counts, max and min system load and others. This +# setting must be enabled in conjunction with the "-s" flag to run the status +# thread. Every "status.interval", the status thread will capture JVM +# statistics and record the results. At the end of the run, max and mins will +# be recorded. +# measurement.trackjvm = false + +# The range of latencies to track in the histogram (milliseconds) +histogram.buckets=1000 + +# Granularity for time series (in milliseconds) +timeseries.granularity=1000 + +# Latency reporting. +# +# YCSB records latency of failed operations separately from successful ones. +# Latency of all OK operations will be reported under their operation name, +# such as [READ], [UPDATE], etc. +# +# For failed operations: +# By default we don't track latency numbers of specific error status. +# We just report latency of all failed operation under one measurement name +# such as [READ-FAILED]. But optionally, user can configure to have either: +# 1. Record and report latency for each and every error status code by +# setting reportLatencyForEachError to true, or +# 2. Record and report latency for a select set of error status codes by +# providing a CSV list of Status codes via the "latencytrackederrors" +# property. +# reportlatencyforeacherror=false +# latencytrackederrors="" diff --git a/workloads/tsworkloada b/workloads/tsworkloada new file mode 100644 index 0000000..903d1d9 --- /dev/null +++ b/workloads/tsworkloada @@ -0,0 +1,46 @@ +# Copyright (c) 2017 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. + +# Yahoo! Cloud System Benchmark +# Workload A: Small cardinality consistent data for 2 days +# Application example: Typical monitoring of a single compute or small +# sensor station where 90% of the load is write and only 10% is read +# (it's usually much less). All writes are inserts. No sparsity so +# every series will have a value at every timestamp. +# +# Read/insert ratio: 10/90 +# Cardinality: 16 per key (field), 64 fields for a total of 1,024 +# time series. +workload=site.ycsb.workloads.TimeSeriesWorkload + +recordcount=1474560 +operationcount=2949120 + +fieldlength=8 +fieldcount=64 +tagcount=4 +tagcardinality=1,2,4,2 + +sparsity=0.0 +delayedSeries=0.0 +delayedIntervals=0 + +timestampunits=SECONDS +timestampinterval=60 +querytimespan=3600 + +readproportion=0.10 +updateproportion=0.00 +insertproportion=0.90 diff --git a/workloads/workload_airports b/workloads/workload_airports new file mode 100644 index 0000000..bb6e5fe --- /dev/null +++ b/workloads/workload_airports @@ -0,0 +1,105 @@ +# Copyright (c) 2010 Yahoo! Inc. All rights reserved. +# Copyright (c) 2023 - 2024 benchANT GmbH. 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. + +workload=site.ycsb.workloads.airport.AirportWorkload + +reportlatencyforeacherror=true +transactioninsertkeygenerator=simple +# debug=true +requestdistribution=uniform +# scanproportion=0 +# readproportion=0.0 +# updateproportion=0.20 +# insertproportion=0.05 +# deleteproportion=0.05 +# findoneproportion=0.70 + +# requestdistribution=zipfian +# nesteddata=false +typedfields=true +# threadcount= +# batchsize= + +# mongodb.url= +# mongodb.indexlist= + +# couchbase.bucket= +# couchbase.scope= +# couchbase.collection= +# couchbase.durability= +# couchbase.persistTo= +# couchbase.replicateTo= +# couchbase.enableMutationToken= +# couchbase.adhoc= +# couchbase.maxParallelism=0 +# couchbase.kvTimeout= +# couchbase.upsert= +# couchbase.host= +# couchbase.username= +# couchbase.password= +# couchbase.sslMode= +# couchbase.sslNoVerify= +# couchbase.certificateFile= +# couchbase.indexlist= + +## ScyllaDB +# scylla.keyspace= +# scylla.username= +# scylla.password= +# scylla.hosts= +# scylla.port= +# scylla.replicationclass +# scylla.replicationdegree +# scylla.readconsistencylevel= +# scylla.writeconsistencylevel= +# scylla.maxconnections +# scylla.coreconnections +# scylla.connecttimeoutmillis +# scylla.readtimeoutmillis +# scylla.lwt=false +# scylla.tracing= +# scylla.inittable= +# scylla.useSSL= +# scylla.indexlist= + +## J D B C +# db.driver= +# db.url= +# db.user= +# db.passwd= +# db.batchsize= +# jdbc.fetchsize +# jdbc.autocommit=true +# jdbc.batchupdateapi +# fieldcount +# jdbc.indexlist= +# jdbc.inittable=true + +## D Y N A M O D B +# dynamodb.debug= +# dynamodb.indexreadcap= +# dynamodb.indexwritecap= +# dynamodb.endpoint= +# dynamodb.awsCredentialsFile="" +# dynamodb.primaryKey= +# dynamodb.primaryKeyType= +# dynamodb.consistentReads="false" +# dynamodb.connectMax +# dynamodb.region= +# dynamodb.hashKeyName="" +# dynamodb.hashKeyValue="" +# dynamodb.settableproperties=true +# dynamodb.indexlist= \ No newline at end of file diff --git a/workloads/workload_template b/workloads/workload_template new file mode 100644 index 0000000..6429661 --- /dev/null +++ b/workloads/workload_template @@ -0,0 +1,207 @@ +# 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. + +# Yahoo! Cloud System Benchmark +# Workload Template: Default Values +# +# File contains all properties that can be set to define a +# YCSB session. All properties are set to their default +# value if one exists. If not, the property is commented +# out. When a property has a finite number of settings, +# the default is enabled and the alternates are shown in +# comments below it. +# +# Use of most explained through comments in Client.java or +# CoreWorkload.java or on the YCSB wiki page: +# https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties + +# The name of the workload class to use +workload=site.ycsb.workloads.CoreWorkload + +# There is no default setting for recordcount but it is +# required to be set. +# The number of records in the table to be inserted in +# the load phase or the number of records already in the +# table before the run phase. +recordcount=1000000 + +# There is no default setting for operationcount but it is +# required to be set. +# The number of operations to use during the run phase. +operationcount=3000000 + +# The number of insertions to do, if different from recordcount. +# Used with insertstart to grow an existing table. +#insertcount= + +# The offset of the first insertion +insertstart=0 + +# The number of fields in a record +fieldcount=10 + +# The size of each field (in bytes) +fieldlength=100 + +# Should read all fields +readallfields=true + +# Should write all fields on update +writeallfields=false + +# The distribution used to choose the length of a field +fieldlengthdistribution=constant +#fieldlengthdistribution=uniform +#fieldlengthdistribution=zipfian + +# What proportion of operations are reads +readproportion=0.95 + +# What proportion of operations are updates +updateproportion=0.05 + +# What proportion of operations are inserts +insertproportion=0 + +# What proportion of operations read then modify a record +readmodifywriteproportion=0 + +# What proportion of operations are scans +scanproportion=0 + +# On a single scan, the maximum number of records to access +maxscanlength=1000 + +# The distribution used to choose the number of records to access on a scan +scanlengthdistribution=uniform +#scanlengthdistribution=zipfian + +# Should records be inserted in order or pseudo-randomly +insertorder=hashed +#insertorder=ordered + +# The distribution of requests across the keyspace +requestdistribution=zipfian +#requestdistribution=uniform +#requestdistribution=latest + +# Percentage of data items that constitute the hot set +hotspotdatafraction=0.2 + +# Percentage of operations that access the hot set +hotspotopnfraction=0.8 + +# Maximum execution time in seconds +#maxexecutiontime= + +# The name of the database table to run queries against +table=usertable + +# The column family of fields (required by some databases) +#columnfamily= + +# How the latency measurements are presented +measurementtype=histogram +#measurementtype=timeseries +#measurementtype=raw +# When measurementtype is set to raw, measurements will be output +# as RAW datapoints in the following csv format: +# "operation, timestamp of the measurement, latency in us" +# +# Raw datapoints are collected in-memory while the test is running. Each +# data point consumes about 50 bytes (including java object overhead). +# For a typical run of 1 million to 10 million operations, this should +# fit into memory most of the time. If you plan to do 100s of millions of +# operations per run, consider provisioning a machine with larger RAM when using +# the RAW measurement type, or split the run into multiple runs. +# +# Optionally, you can specify an output file to save raw datapoints. +# Otherwise, raw datapoints will be written to stdout. +# The output file will be appended to if it already exists, otherwise +# a new output file will be created. +#measurement.raw.output_file = /tmp/your_output_file_for_this_run + +# Whether or not to emit individual histogram buckets when measuring +# using histograms. +# measurement.histogram.verbose = false + +# JVM Reporting. +# +# Measure JVM information over time including GC counts, max and min memory +# used, max and min thread counts, max and min system load and others. This +# setting must be enabled in conjunction with the "-s" flag to run the status +# thread. Every "status.interval", the status thread will capture JVM +# statistics and record the results. At the end of the run, max and mins will +# be recorded. +# measurement.trackjvm = false + +# The range of latencies to track in the histogram (milliseconds) +histogram.buckets=1000 + +# Granularity for time series (in milliseconds) +timeseries.granularity=1000 + +# Latency reporting. +# +# YCSB records latency of failed operations separately from successful ones. +# Latency of all OK operations will be reported under their operation name, +# such as [READ], [UPDATE], etc. +# +# For failed operations: +# By default we don't track latency numbers of specific error status. +# We just report latency of all failed operation under one measurement name +# such as [READ-FAILED]. But optionally, user can configure to have either: +# 1. Record and report latency for each and every error status code by +# setting reportLatencyForEachError to true, or +# 2. Record and report latency for a select set of error status codes by +# providing a CSV list of Status codes via the "latencytrackederrors" +# property. +# reportlatencyforeacherror=false +# latencytrackederrors="" + +# Insertion error retry for the core workload. +# +# By default, the YCSB core workload does not retry any operations. +# However, during the load process, if any insertion fails, the entire +# load process is terminated. +# If a user desires to have more robust behavior during this phase, they can +# enable retry for insertion by setting the following property to a positive +# number. +# core_workload_insertion_retry_limit = 0 +# +# the following number controls the interval between retries (in seconds): +# core_workload_insertion_retry_interval = 3 + +# Distributed Tracing via Apache HTrace (http://htrace.incubator.apache.org/) +# +# Defaults to blank / no tracing +# Below sends to a local file, sampling at 0.1% +# +# htrace.sampler.classes=ProbabilitySampler +# htrace.sampler.fraction=0.001 +# htrace.span.receiver.classes=org.apache.htrace.core.LocalFileSpanReceiver +# htrace.local.file.span.receiver.path=/some/path/to/local/file +# +# To capture all spans, use the AlwaysSampler +# +# htrace.sampler.classes=AlwaysSampler +# +# To send spans to an HTraced receiver, use the below and ensure +# your classpath contains the htrace-htraced jar (i.e. when invoking the ycsb +# command add -cp /path/to/htrace-htraced.jar) +# +# htrace.span.receiver.classes=org.apache.htrace.impl.HTracedSpanReceiver +# htrace.htraced.receiver.address=example.com:9075 +# htrace.htraced.error.log.period.ms=10000 diff --git a/workloads/workloada b/workloads/workloada new file mode 100644 index 0000000..ed46bb7 --- /dev/null +++ b/workloads/workloada @@ -0,0 +1,37 @@ +# Copyright (c) 2010 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 + +recordcount=1000 +operationcount=1000 +workload=site.ycsb.workloads.CoreWorkload + +readallfields=true + +readproportion=0.5 +updateproportion=0.5 +scanproportion=0 +insertproportion=0 + +requestdistribution=zipfian + diff --git a/workloads/workloadb b/workloads/workloadb new file mode 100644 index 0000000..47f2d02 --- /dev/null +++ b/workloads/workloadb @@ -0,0 +1,36 @@ +# Copyright (c) 2010 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 B: Read mostly workload +# Application example: photo tagging; add a tag is an update, but most operations are to read tags +# +# Read/update ratio: 95/5 +# Default data size: 1 KB records (10 fields, 100 bytes each, plus key) +# Request distribution: zipfian + +recordcount=1000 +operationcount=1000 +workload=site.ycsb.workloads.CoreWorkload + +readallfields=true + +readproportion=0.95 +updateproportion=0.05 +scanproportion=0 +insertproportion=0 + +requestdistribution=zipfian + diff --git a/workloads/workloadc b/workloads/workloadc new file mode 100644 index 0000000..03d1efb --- /dev/null +++ b/workloads/workloadc @@ -0,0 +1,38 @@ +# Copyright (c) 2010 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 C: Read only +# Application example: user profile cache, where profiles are constructed elsewhere (e.g., Hadoop) +# +# Read/update ratio: 100/0 +# Default data size: 1 KB records (10 fields, 100 bytes each, plus key) +# Request distribution: zipfian + +recordcount=1000 +operationcount=1000 +workload=site.ycsb.workloads.CoreWorkload + +readallfields=true + +readproportion=1 +updateproportion=0 +scanproportion=0 +insertproportion=0 + +requestdistribution=zipfian + + + diff --git a/workloads/workloadd b/workloads/workloadd new file mode 100644 index 0000000..de0415e --- /dev/null +++ b/workloads/workloadd @@ -0,0 +1,41 @@ +# Copyright (c) 2010 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 D: Read latest workload +# Application example: user status updates; people want to read the latest +# +# Read/update/insert ratio: 95/0/5 +# Default data size: 1 KB records (10 fields, 100 bytes each, plus key) +# Request distribution: latest + +# The insert order for this is hashed, not ordered. The "latest" items may be +# scattered around the keyspace if they are keyed by userid.timestamp. A workload +# which orders items purely by time, and demands the latest, is very different than +# workload here (which we believe is more typical of how people build systems.) + +recordcount=1000 +operationcount=1000 +workload=site.ycsb.workloads.CoreWorkload + +readallfields=true + +readproportion=0.95 +updateproportion=0 +scanproportion=0 +insertproportion=0.05 + +requestdistribution=latest + diff --git a/workloads/workloade b/workloads/workloade new file mode 100644 index 0000000..b904aa1 --- /dev/null +++ b/workloads/workloade @@ -0,0 +1,46 @@ +# Copyright (c) 2010 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 E: Short ranges +# Application example: threaded conversations, where each scan is for the posts in a given thread (assumed to be clustered by thread id) +# +# Scan/insert ratio: 95/5 +# Default data size: 1 KB records (10 fields, 100 bytes each, plus key) +# Request distribution: zipfian + +# The insert order is hashed, not ordered. Although the scans are ordered, it does not necessarily +# follow that the data is inserted in order. For example, posts for thread 342 may not be inserted contiguously, but +# instead interspersed with posts from lots of other threads. The way the YCSB client works is that it will pick a start +# key, and then request a number of records; this works fine even for hashed insertion. + +recordcount=1000 +operationcount=1000 +workload=site.ycsb.workloads.CoreWorkload + +readallfields=true + +readproportion=0 +updateproportion=0 +scanproportion=0.95 +insertproportion=0.05 + +requestdistribution=zipfian + +maxscanlength=100 + +scanlengthdistribution=uniform + + diff --git a/workloads/workloadf b/workloads/workloadf new file mode 100644 index 0000000..6fe0e7a --- /dev/null +++ b/workloads/workloadf @@ -0,0 +1,37 @@ +# Copyright (c) 2010 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 F: Read-modify-write workload +# Application example: user database, where user records are read and modified by the user or to record user activity. +# +# Read/read-modify-write ratio: 50/50 +# Default data size: 1 KB records (10 fields, 100 bytes each, plus key) +# Request distribution: zipfian + +recordcount=1000 +operationcount=1000 +workload=site.ycsb.workloads.CoreWorkload + +readallfields=true + +readproportion=0.5 +updateproportion=0 +scanproportion=0 +insertproportion=0 +readmodifywriteproportion=0.5 + +requestdistribution=zipfian +