Repository: pinterest/jbender Branch: master Commit: d0aa989be2d0 Files: 43 Total size: 142.3 KB Directory structure: gitextract_ovnm_52_/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── doc/ │ ├── http/ │ │ ├── TUTORIAL.md │ │ └── jbender-http-tutorial/ │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── LoadTest.java │ └── thrift/ │ ├── TUTORIAL.md │ └── jbender-thrift-tutorial/ │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── src/ │ └── main/ │ ├── java/ │ │ └── echo/ │ │ ├── jbender/ │ │ │ └── Main.java │ │ └── server/ │ │ └── Main.java │ └── thrift/ │ └── echo.thrift ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src/ ├── jmh/ │ └── java/ │ └── com/ │ └── pinterest/ │ └── jbender/ │ ├── JBenderBenchmark.java │ └── JBenderHttpBenchmark.java ├── main/ │ └── java/ │ └── com/ │ └── pinterest/ │ └── jbender/ │ ├── JBender.java │ ├── events/ │ │ ├── TimingEvent.java │ │ └── recording/ │ │ ├── HdrHistogramRecorder.java │ │ ├── LoggingRecorder.java │ │ └── Recorder.java │ ├── executors/ │ │ ├── NoopRequestExecutor.java │ │ ├── RequestExecutor.java │ │ ├── SleepyRequestExecutor.java │ │ ├── Validator.java │ │ └── http/ │ │ └── FiberApacheHttpClientRequestExecutor.java │ ├── intervals/ │ │ ├── ConstantIntervalGenerator.java │ │ ├── ExponentialIntervalGenerator.java │ │ └── IntervalGenerator.java │ └── util/ │ ├── ConnectionPool.java │ ├── ListReceivePort.java │ └── WaitGroup.java └── test/ └── java/ └── com/ └── pinterest/ └── jbender/ └── JBenderTest.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.class *.jar !gradle/wrapper/gradle-wrapper.jar *.war *.ear hs_err_pid* .gradle build/ gradle-app.setting .idea jbender.iml ================================================ FILE: .travis.yml ================================================ language: java jdk: - oraclejdk8 before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.md ================================================ JBender ======= [![Build Status](https://travis-ci.org/pinterest/jbender.svg)](https://travis-ci.org/pinterest/jbender) JBender makes it easy to build load testers for services using protocols like HTTP and Thrift (and many others). JBender provides a library of flexible, easy-to-use primitives that can be combined (with plain Java code) to build high performance load testers customized to any use case, and that can evolve with your service over time. JBender provides two different approaches to load testing. The first, `JBender.loadTestThroughput` gives the tester control over the throughput (queries per second), but not over the concurrency (number of active connections). This approach is well suited for services that are open to the Internet, like web services, and the backend services to which they speak. The primary benefit of this approach is that the load tester will maintain the requested throughput, even if the service is struggling (or failing) to support it. As a result, a much more clear picture of how the target service responds to load is provided. The second approach, `JBender.loadTestConcurrency`, gives the tester control over the concurrency (number of active connections), but not the throughput (queries per second). This approach is well suited to applications that need to test a large number of concurrent, inactive connections, like chat servers. This approach is not suitable for testing request latency, as the load tester will slow down to match the server (because it cannot start more connections than requested). That JBender is a library makes it flexible and easy to extend, but means it takes longer to create an initial load tester. As a result, we've focused on creating easy-to-follow tutorials. ## Quasar The JBender library makes heavy use of [Quasar](http://www.paralleluniverse.co/quasar/), a library from [Parallel Universe](http://www.paralleluniverse.co/) which adds lightweight threads (called "fibers") to the JVM. The tutorials, linked below, will help you get up and running with Quasar and JBender, and provide some background on how Quasar works (hint: it's very straightforward to use Quasar!). Here are some links for more information: * [Quasar's Documentation](http://docs.paralleluniverse.co/quasar/). * [Quasar's Github Page](https://github.com/puniverse/quasar). * [The Parallel Universe Blog](http://blog.paralleluniverse.co/). ## Getting Started The easiest way to get started with JBender is to use one of the tutorials: * [Thrift](https://github.com/pinterest/jbender/blob/master/doc/thrift/TUTORIAL.md) * [HTTP](https://github.com/pinterest/jbender/blob/master/doc/http/TUTORIAL.md) ## Performance The Linux TCP stack for a default server installation is usually not tuned to high throughput servers or load testers. After some experimentation, we have settled on adding these lines to `/etc/sysctl.conf`, after which you can run `sysctl -p` to load them (although it is recommended to restart your host at this point to make sure these take effect). ``` # /etc/sysctl.conf # Increase system file descriptor limit fs.file-max = 100000 # Increase ephermeral IP ports net.ipv4.ip_local_port_range = 1024 65000 # Increase Linux autotuning TCP buffer limits # Set max to 16MB for 1GE and 32M (33554432) or 54M (56623104) for 10GE # Don't set tcp_mem itself! Let the kernel scale it based on RAM. net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.core.rmem_default = 16777216 net.core.wmem_default = 16777216 net.core.optmem_max = 40960 net.ipv4.tcp_rmem = 4096 87380 16777216 net.ipv4.tcp_wmem = 4096 65536 16777216 # Make room for more TIME_WAIT sockets due to more clients, # and allow them to be reused if we run out of sockets # Also increase the max packet backlog net.core.netdev_max_backlog = 100000 net.ipv4.tcp_max_syn_backlog = 100000 net.ipv4.tcp_max_tw_buckets = 2000000 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 10 # Disable TCP slow start on idle connections net.ipv4.tcp_slow_start_after_idle = 0 # From https://people.redhat.com/alikins/system_tuning.html net.ipv4.tcp_sack = 0 net.ipv4.tcp_timestamps = 1 ``` This is a slightly modified version of advice taken from this source: http://www.nateware.com/linux-network-tuning-for-2013.html#.VBjahC5dVyE In addition, it helps to increase the open file limit with something like: ```ulimit -n 100000``` ## What Is Missing JBender does not provide any support for sending load from more than one machine. If you need to send more load than a single machine can handle, or you need the requests to come from multiple physical hosts (or different networks, or whatever), you currently have to write your own tools. In addition, the histogram implementation used by JBender is inefficient to send over the network, unlike q-digest or t-digest, which we hope to implement in the future. JBender does not provide any visualization tools, and has a relatively simple set of measurements, including a customizable histogram of latencies, an error rate and some other summary statistics. JBender does provide a complete log of everything that happens during a load test, so you can use existing tools to graph any aspect of that data, but nothing in JBender makes that easier right now. JBender only provides helper functions for HTTP and Thrift currently, because that is all we use internally at Pinterest. The load testers we have written internally with JBender have a lot of common command line arguments, but we haven't finalized a set to share as part of the library. ## Comparison to Other Load Testers #### Bender JBender is a port of Bender to the JVM platform with [Quasar](http://docs.paralleluniverse.co/quasar/) lightweight threads (_fibers_) and channels. #### JMeter JMeter provides a GUI to configure and run load tests, and can also be configured via XML (really, really not recommended by hand!) and run from the command line. JMeter's is not a good approach to load testing services (see the JBender docs and the Iago philosophy for more details on why that is). It isn't easy to extend JMeter to handle new protocols, so it doesn't have support for Thrift or Protobuf. It is relatively easy to extend other parts of JMeter by writing Java code, however, and the GUI makes it easy to plug all the pieces together. #### Iago Iago is Twitter's load testing library and it is the inspiration for JBender's `loadTestThroughput` function. Iago is a Scala library written on top of Netty and the Twitter Finagle libraries. As a result, Iago is powerful, but difficult to understand, extend and configure. It was frustration with making Iago work that led to the creation of JBender. #### The Grinder The Grinder has the same load testing approach as JMeter, but allows scripting via Jython, which makes it more flexible and extensible. The Grinder uses threads, which limits the concurrency at which it can work, and makes it hard to implement things like JBender's `loadTestThroughput` function. The Grinder does have support for conveniently running distributed load tests. ## Copyright Copyright 2015 Pinterest.com 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. ## Attribution JBender includes open source from the following sources: * Apache Thrift Libraries. Copyright 2014 Apache Software Foundation. Licensed under the Apache License v2.0 (http://www.apache.org/licenses/). * Quasar Libraries. Copyright 2014 Parallel Universe. Licensed under the GNU Lesser General Public License (http://www.gnu.org/licenses/lgpl.html). ================================================ FILE: build.gradle ================================================ plugins { id 'me.champeau.gradle.jmh' version '0.2.0' } apply plugin: 'java' apply plugin: 'maven' apply plugin: 'me.champeau.gradle.jmh' sourceCompatibility = 1.8 targetCompatibility = 1.8 group = 'com.pinterest' version = '1.0.1-SNAPSHOT' ext.jmhVer = '1.13' ext.hdrHistVer = '2.1.9' ext.quasarVer = '0.7.5' ext.comsatVer = '0.7.0' ext.httpCoreVer = '4.4.5' ext.slf4jVer = '1.7.21' ext.junitVer = '4.12' repositories { // mavenLocal() mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } configurations { quasar } configurations.all { resolutionStrategy { failOnVersionConflict() force "org.hdrhistogram:HdrHistogram:$hdrHistVer" force "org.apache.httpcomponents:httpcore:$httpCoreVer" } } dependencies { testCompile group: 'junit', name: 'junit', version: "$junitVer" compile group: 'co.paralleluniverse', name: 'quasar-core', version: "$quasarVer", classifier: 'jdk8' compile group: 'co.paralleluniverse', name: 'comsat-httpclient', version: "$comsatVer" compile group: 'org.hdrhistogram', name: 'HdrHistogram', version: "$hdrHistVer" compile group: 'org.slf4j', name: 'slf4j-api', version: "$slf4jVer" // compile group: 'org.slf4j', name: 'slf4j-simple', version: "$slf4jVer" // For the IDE // compile "org.openjdk.jmh:jmh-core:$jmhVer" // compile "org.openjdk.jmh:jmh-generator-annprocess:$jmhVer" quasar group: 'co.paralleluniverse', name: 'quasar-core', version: "$quasarVer", classifier: 'jdk8' } jmh { jmhVersion = "$jmhVer" include = '.*' jvmArgs = "-server -XX:+TieredCompilation -XX:+AggressiveOpts -javaagent:${configurations.quasar.iterator().next()} -Dco.paralleluniverse.fibers.detectRunawayFibers=false" benchmarkMode = 'avgt' // 'thrpt' timeUnit = 'ms' } tasks.withType(Test) { allJvmArgs = [] useJUnit() jvmArgs "-javaagent:${configurations.quasar.iterator().next()}" } ================================================ FILE: doc/http/TUTORIAL.md ================================================ JBender HTTP Tutorial ===================== This tutorial walks through the steps to create an HTTP load tester with JBender on a pre-built HTTP server. ### Getting Started JBender uses [Gradle](http://gradle.org) as a build tool and we're going to use it for our sample load tester as well, so make sure you have it installed. The easiest way to get it on Mac OS X is probably to install [HomeBrew](http://brew.sh/) and then `brew install gradle`, while on Linux there's [LinuxBrew](https://github.com/Homebrew/linuxbrew). Your specific Linux distribution could offer native Gradle packages but they tend to lag behind the most recent version, so it's probably better to brew anyway. We load test a ready-made server available as the [Comsat Gradle template](https://github.com/puniverse/comsat-gradle-template), so you can just `git clone` it and then run it in a separate terminal window with `gradle wrapper` followed by `./gradlew -Penv=dropwizard run` from the project directory. Writing a JBender HTTP load test involves using JBender's `FiberApacheHttpClientRequestExecutor`, which is based on [Comsat HTTP client](http://docs.paralleluniverse.co/comsat/#http-clients). ### Creating the load test Gradle project In your usual sources work root create a `jbender-http-tutorial` directory and view the following `build.gradle` file in it (you won't need to add this, it is already there): ``` groovy plugins { id "us.kirchmeier.capsule" version "1.0-rc1" } apply plugin: 'java' // Target JDK8 sourceCompatibility = 1.8 targetCompatibility = 1.8 group = 'jbendertut' version = '0.1-SNAPSHOT' // UTF8 encoding for sources [compileJava, compileTestJava]*.options*.encoding = "UTF-8" repositories { // Enable this if you want to use locally-built artifacts, e.g. if you have installed jbender locally mavenLocal() // This allows using published Quasar snapshots maven { url "https://oss.sonatype.org/content/repositories/snapshots" } mavenCentral() } configurations { quasar } dependencies { // Quasar API compile group: "co.paralleluniverse", name: "quasar-core", version: "0.6.3-SNAPSHOT", classifier: "jdk8" // Comsat HTTP Client compile group: "co.paralleluniverse", name: "comsat-httpclient", version: "0.3.0" // HDR histogram compile group: 'org.hdrhistogram', name: 'HdrHistogram', version: "2.1.4" // JBender API compile group: "com.pinterest", name: "jbender", version: "1.0" // Logging compile group: "org.slf4j", name: "slf4j-api", version: "1.7.12" compile group: "org.slf4j", name: "slf4j-simple", version: "1.7.12" // Useful to point to the Quasar agent later in JVM flags (and Capsule-building task) quasar group: "co.paralleluniverse", name: "quasar-core", version: "0.6.3-SNAPSHOT", classifier: "jdk8" } // Task building an handy self-contained load test capsule task capsule(type: FatCapsule) { applicationClass "LoadTest" capsuleManifest { javaAgents = [configurations.quasar.iterator().next().getName()] // Add "=vdc" to the Quasar agent to trace instrumentation jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts"] // Aggressive optimizations } } // Gradle JavaExec load test task task runLoadTest(type: JavaExec) { main = "LoadTest" classpath = sourceSets.main.runtimeClasspath // Aggressive optimizations and Quasar agent jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts", "-javaagent:${configurations.quasar.iterator().next()}"] // Add "=vdc" to the Quasar agent to trace instrumentation // Enable this to troubleshoot instrumentation issues // systemProperties = ["co.paralleluniverse.fibers.verifyInstrumentation" : "true"] } ``` This uses [Capsule](https://github.com/puniverse/capsule) to package the load tester, and provides a convenience task named `runLoadTest` that makes it easy to run the load test with gradle (in development). This is an example of what a production build file would look like for a load tester, and we highly recommend the use of Capsule! ## Load Testing Let's now write a simple load tester with JBender. The next few sections walk through the various parts of the load tester. If you are in a hurry skip to the section "Final Load Tester Program" and just follow the instructions from there. The sample code you copied in the first step already has all this code, so these sections just describe what that code does, and why. JBender has a very simple loop in which it does the following: 1. Generate an interval (in nanoseconds) and sleep for that amount of time (you have control over the length of these intervals). 2. Fetch the next request from a channel of requests (created by you). 3. Spawn a lightweight thread (fiber) to send the request and wait for the response (and then generate timing information). 4. Repeat until there are no more requests to send. ### Intervals The first thing we need is a function to generate intervals (in nanoseconds) between executing requests. The JBender library comes with some predefined intervals: a uniform distribution (always wait the same amount of time between each request) and an exponential distribution. In this case we will use the exponential distribution, which means our server will experience load as generated by a [Poisson process](http://en.wikipedia.org/wiki/Poisson_process), which is fairly typical of server workloads on the Internet (with the usual caveats that every service is a special snowflake, etc, etc). We get the interval function with this code: ``` java final IntervalGenerator intervalGenerator = new ConstantIntervalGenerator(qps); ``` Where `qps` is our desired throughput measured in queries per second. It is also the reciprocal of the mean value of the exponential distribution used to generate the request arrival times (see the wikipedia article above). In practice this means you will see an average QPS that fluctuates around the target QPS (with less fluctuation as you increase the time interval over which you are averaging). ### Request Generator The second thing we need is a channel of requests to send to the HTTP server. When an interval has been generated and JBender is ready to send the request, it pulls the next request from this channel and spawns a Quasar _fiber_ (lightweight thread) to send the request to the server. This code creates and starts a simple synthetic Apache HTTP Client's `HttpGet` request generator to the "Hello World" server endpoint: ``` java new Fiber("message-producer", () -> { // Bench handling 10k reqs for (int i = 0; i < 10000; ++i) { requestCh.send(new HttpGet("http://localhost:8080/hello-world")); } requestCh.close(); return null; }).start(); ``` Note that this loop will send 10k requests and then close the channel. Closing the channel notifies the load tester that there will be no more requests, so it can finish waiting for pending requests and then shut itself down. If you fail to close the channel the load tester will wait for a next request forever. ### Request Executor The next thing we need is a request executor, which takes the requests generated above and sends them to the service. We will just use JBender's pre-built one and add a response validator: ``` final FiberApacheHttpClientRequestExecutor requestExecutor = new FiberApacheHttpClientRequestExecutor<>((res) -> { if (res == null) { throw new AssertionError("Response is null"); } final int status = res.getStatusLine().getStatusCode(); if (status != 200) { throw new AssertionError("Status " + status + " is not 200"); } }, 1000000); ``` This validates that the response has actually been produced ans has a HTTP 200 status code. ### Recorder The last thing we need is a channel that will output `TimingEvent` objects as the load tester runs. This will let us listen to the load testers progress and record stats. We want this channel to be buffered so that we can run somewhat independently of the load test without slowing it down: ``` java final Channel> eventCh = Channels.newChannel(10000); ``` The `TimingEvent` object contains fields that include the interval time between requests, the duration of the request (how long it took to get a response), whether it was an error or a success, how much "overage" time it is experiencing and a few other things (including a field that can be filled in by the request executor). JBender has a few simple "recorders" that make it easy to do basic things like logging events and generating histograms: * `LoggingRecorder` creates a recorder that takes a `Logger` and outputs each event. * `NewHistogramRecorder` records request latencies on a [`org.HdrHistogram.Histogram`](https://github.com/HdrHistogram/HdrHistogram). You can combine recorders using the `Recorder.record` function, so you can both log events and manage a histogram using code like this: ``` final Logger LOG = LoggerFactory.getLogger(LoadTest.class); final Histogram histogram = new Histogram(3600000000L, 3); record("recorder", eventCh, new HdrHistogramRecorder(histogram), new LoggingRecorder(LOG)); ``` The histogram takes two arguments: the maximum expected value and the number of precision digits and will adjust automatically to record latencies both efficiently and with high-definition buckets. It is relatively easy to build recorders, or to just process the events from the channel yourself: see the JBender documentation for more details on what events can be sent, and what data they contain. ### Final Load Tester Program Create a directory for the load tester: ``` bash mkdir -p src/main/java ``` Then create a file named `LoadTest.java` in that directory and add these lines to it: ``` java import java.util.concurrent.ExecutionException; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.HdrHistogram.Histogram; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.nio.reactor.IOReactorException; import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import com.pinterest.jbender.JBender; import com.pinterest.jbender.events.Event; import com.pinterest.jbender.events.recording.HdrHistogramRecorder; import com.pinterest.jbender.events.recording.LoggingRecorder; import com.pinterest.jbender.executors.RequestExecutor; import com.pinterest.jbender.executors.http.FiberApacheHttpClientRequestExecutor; import com.pinterest.jbender.intervals.ConstantIntervalGenerator; import com.pinterest.jbender.intervals.IntervalGenerator; import static com.pinterest.jbender.events.recording.Recorder.record; /** * Sample HTTP benchmark against {@url https://github.com/puniverse/comsat-gradle-template} */ public class LoadTest { public static void main(final String[] args) throws SuspendExecution, InterruptedException, ExecutionException, IOReactorException, IOException { final IntervalGenerator intervalGenerator = new ConstantIntervalGenerator(10000000); try (final FiberApacheHttpClientRequestExecutor requestExecutor = new FiberApacheHttpClientRequestExecutor<>((res) -> { if (res == null) { throw new AssertionError("Response is null"); } final int status = res.getStatusLine().getStatusCode(); if (status != 200) { throw new AssertionError("Status is " + status); } }, 1000000)) { final Channel requestCh = Channels.newChannel(1000); final Channel> eventCh = Channels.newChannel(1000); // Requests generator new Fiber("req-gen", () -> { // Bench handling 1k reqs for (int i = 0; i < 1000; ++i) { requestCh.send(new HttpGet("http://localhost:8080/hello-world")); } requestCh.close(); }).start(); final Histogram histogram = new Histogram(3600000000L, 3); // Event recording, both HistHDR and logging record("recorder", eventCh, new HdrHistogramRecorder(histogram), new LoggingRecorder(LOG)); // Main new Fiber("jbender", () -> { JBender.loadTestThroughput(intervalGenerator, requestCh, requestExecutor, eventCh); }).start().join(); histogram.outputPercentileDistribution(System.out, 1000.0); } } private static final Logger LOG = LoggerFactory.getLogger(LoadTest.class); } ``` ### Run Server and Load Tester With the `comsat-gradle-template` server running in one terminal window, run the load tester in another one from the `jbender-http-tutorial` project directory with `gradle wrapper` (only the first time) and then `./gradlew runLoadTest`. The output of the load test will be the percentile distribution from the histogram. ================================================ FILE: doc/http/jbender-http-tutorial/build.gradle ================================================ // Capsule plugin plugins { id "us.kirchmeier.capsule" version "1.0-rc1" } apply plugin: 'java' // Target JDK8 sourceCompatibility = 1.8 targetCompatibility = 1.8 group = 'jbendertut' version = '0.1-SNAPSHOT' // UTF8 encoding for sources [compileJava, compileTestJava]*.options*.encoding = "UTF-8" repositories { // Enable this if you want to use locally-built artifacts mavenLocal() // This allows using published Quasar snapshots maven { url "https://oss.sonatype.org/content/repositories/snapshots" } mavenCentral() } configurations { quasar } dependencies { // Quasar API compile group: "co.paralleluniverse", name: "quasar-core", version: "0.6.3-SNAPSHOT", classifier: "jdk8" // Comsat HTTP Client compile group: "co.paralleluniverse", name: "comsat-httpclient", version: "0.3.0" // HDR histogram compile group: 'org.hdrhistogram', name: 'HdrHistogram', version: "2.1.4" // JBender API compile group: "com.pinterest", name: "jbender", version: "1.0" // Logging compile group: "org.slf4j", name: "slf4j-api", version: "1.7.12" compile group: "org.slf4j", name: "slf4j-simple", version: "1.7.12" // Useful to point to the Quasar agent later in JVM flags (and Capsule-building task) quasar group: "co.paralleluniverse", name: "quasar-core", version: "0.6.3-SNAPSHOT", classifier: "jdk8" } // Task building an handy self-contained load test capsule task capsule(type: FatCapsule) { applicationClass "LoadTest" capsuleManifest { // Aggressive optimizations and Quasar agent javaAgents = [configurations.quasar.iterator().next().getName()] // Add "=vdc" to the Quasar agent to trace instrumentation jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts"] } } // Gradle JavaExec load test task task runLoadTest(type: JavaExec) { main = "LoadTest" classpath = sourceSets.main.runtimeClasspath // Aggressive optimizations and Quasar agent jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts", "-javaagent:${configurations.quasar.iterator().next()}"] // Add "=vdc" to the Quasar agent to trace instrumentation // Enable this to troubleshoot instrumentation issues // systemProperties = ["co.paralleluniverse.fibers.verifyInstrumentation" : "true"] } ================================================ FILE: doc/http/jbender-http-tutorial/gradle/wrapper/gradle-wrapper.properties ================================================ #Tue May 12 13:10:51 PDT 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-bin.zip ================================================ FILE: doc/http/jbender-http-tutorial/gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: doc/http/jbender-http-tutorial/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: doc/http/jbender-http-tutorial/src/main/java/LoadTest.java ================================================ import java.util.concurrent.ExecutionException; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.HdrHistogram.Histogram; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.nio.reactor.IOReactorException; import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import com.pinterest.jbender.JBender; import com.pinterest.jbender.events.TimingEvent; import com.pinterest.jbender.events.recording.HdrHistogramRecorder; import com.pinterest.jbender.events.recording.LoggingRecorder; import com.pinterest.jbender.executors.RequestExecutor; import com.pinterest.jbender.executors.http.FiberApacheHttpClientRequestExecutor; import com.pinterest.jbender.intervals.ConstantIntervalGenerator; import com.pinterest.jbender.intervals.IntervalGenerator; import static com.pinterest.jbender.events.recording.Recorder.record; /** * Sample HTTP benchmark against {@url https://github.com/puniverse/comsat-gradle-template} */ public class LoadTest { public static void main(final String[] args) throws SuspendExecution, InterruptedException, ExecutionException, IOReactorException, IOException { final IntervalGenerator intervalGenerator = new ConstantIntervalGenerator(10000000); try (final FiberApacheHttpClientRequestExecutor requestExecutor = new FiberApacheHttpClientRequestExecutor<>((res) -> { if (res == null) { throw new AssertionError("Response is null"); } final int status = res.getStatusLine().getStatusCode(); if (status != 200) { throw new AssertionError("Status is " + status); } }, 1000000)) { final Channel requestCh = Channels.newChannel(1000); final Channel> eventCh = Channels.newChannel(1000); // Requests generator new Fiber("req-gen", () -> { // Bench handling 1k reqs for (int i = 0; i < 1000; ++i) { requestCh.send(new HttpGet("http://localhost:8080/hello-world")); } requestCh.close(); }).start(); final Histogram histogram = new Histogram(3600000000L, 3); // Event recording, both HistHDR and logging record(eventCh, new HdrHistogramRecorder(histogram, 1000000), new LoggingRecorder(LOG)); // Main new Fiber("jbender", () -> { JBender.loadTestThroughput(intervalGenerator, 0, requestCh, requestExecutor, eventCh); }).start().join(); histogram.outputPercentileDistribution(System.out, 1000.0); } } private static final Logger LOG = LoggerFactory.getLogger(LoadTest.class); } ================================================ FILE: doc/thrift/TUTORIAL.md ================================================ JBender Thrift Tutorial ======================= This tutorial walks through the steps to create a simple "Echo" Thrift service with a load tester. The complete project can be found at [jbender-echo](https://github.com/cgordon/jbender-echo) TODO: open it up or include it here. ## Getting Started You will need a copy of Thrift installed on your machine, which will allow you to run the `thrift` command. You can follow the "Getting Started" instructions on the [Apache Thrift](https://thrift.apache.org/) page to download and install it. The easiest way to get it on Mac OS X is probably to install [HomeBrew](http://brew.sh/) and then `brew install gradle` though. JBender uses [Gradle](http://gradle.org) as a build tool and we're going to use it for our sample load tester as well, so make sure you have it installed. The easiest way to get it on Mac OS X is still `brew install gradle`, while on Linux there's [LinuxBrew](https://github.com/Homebrew/linuxbrew). Your specific Linux distribution could offer native Gradle packages but they tend to lag behind the most recent version, so it's probably better to brew anyway. ### Creating the load test Gradle project In your usual sources work root create a `jbender-thrift-tutorial` directory and the following `build.gradle` file in it: ``` groovy // Gradle Thrift plugin buildscript { repositories { mavenCentral() } dependencies { classpath "co.tomlee.gradle.plugins:gradle-thrift-plugin:0.0.4" } } // Capsule plugin plugins { id "us.kirchmeier.capsule" version "1.0-rc1" } apply plugin: 'java' apply plugin: 'thrift' // Target JDK8 sourceCompatibility = 1.8 targetCompatibility = 1.8 group = 'jbendertut' version = '0.1-SNAPSHOT' // UTF8 encoding for sources [compileJava, compileTestJava]*.options*.encoding = "UTF-8" repositories { // Enable this if you want to use locally-built artifacts mavenLocal() mavenCentral() } configurations { quasar } dependencies { // Thrift API compile group: "org.apache.thrift", name: "libthrift", version: "0.9.1" // Quasar-Thrift server compile group: "com.pinterest", name: "quasar-thrift", version: "0.1-SNAPSHOT" // Quasar API compile group: "co.paralleluniverse", name: "quasar-core", version: "0.7.3", classifier: "jdk8" // JBender API compile group: "com.pinterest", name: "jbender", version: "1.0" // Logging compile group: "org.slf4j", name: "slf4j-api", version: "1.7.12" compile group: "org.slf4j", name: "slf4j-simple", version: "1.7.12" // Useful to point to the Quasar agent later in JVM flags (and Capsule-building task) quasar group: "co.paralleluniverse", name: "quasar-core", version: "0.7.3", classifier: "jdk8" } // Thrift generators generateThriftSource { generators { java {} } } // Automatically find Quasar suspendables in Thrift-generated code classes { doFirst { ant.taskdef(name: 'scanSuspendables', classname: 'co.paralleluniverse.fibers.instrument.SuspendablesScanner', classpath: "build/classes/main:build/resources/main:${configurations.runtime.asPath}") ant.scanSuspendables( auto: true, suspendablesFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendables", supersFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendable-supers", append: true) { fileset(dir: sourceSets.main.output.classesDir) } } } // Task building an handy self-contained server capsule task serverCapsule(type: FatCapsule) { applicationClass "com.pinterest.echo.jbender.server.Main" capsuleManifest { javaAgents = [configurations.quasar.iterator().next().getName()] // Aggressive optimizations jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts"] } } // Task building an handy self-contained load test capsule task capsule(type: FatCapsule) { applicationClass "com.pinterest.echo.jbender.Main" capsuleManifest { javaAgents = [configurations.quasar.iterator().next().getName()] // Aggressive optimizations jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts"] } } // Gradle JavaExec load test task task runLoadTest(type: JavaExec) { main = "com.pinterest.echo.jbender.Main" classpath = sourceSets.main.runtimeClasspath // Aggressive optimizations and Quasar agent jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts", "-javaagent:${configurations.quasar.iterator().next()}"] // Add "=vdc" to the Quasar agent to trace instrumentation // Enable this to troubleshoot instrumentation issues // systemProperties = ["co.paralleluniverse.fibers.verifyInstrumentation" : "true"] } // Gradle JavaExec server task task runServer(type: JavaExec) { main = "com.pinterest.echo.jbender.server.Main" classpath = sourceSets.main.runtimeClasspath // Aggressive optimizations and Quasar agent jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts", "-javaagent:${configurations.quasar.iterator().next()}"] // Add "=vdc" to the Quasar agent to trace instrumentation // Enable this to troubleshoot instrumentation issues // systemProperties = ["co.paralleluniverse.fibers.verifyInstrumentation" : "true"] } ``` ## Writing the Thrift Server and Client This section will walk through the creation of a Thrift client and server, which we will use to test JBender in the following section. ### Thrift Service Definition and Code Generation Now create a file named `src/main/thrift/echo.thrift` and add these lines to it using your text editor: ``` thrift namespace java com.pinterest.echo.thrift struct EchoRequest { 1: optional string message; } struct EchoResponse { 2: optional string message; } service EchoService { EchoResponse echo(1: EchoRequest request); } ``` This defines a Thrift service with one API endpoint named `echo` that takes a `EchoRequest` and returns a `EchoResponse`. ### Thrift Service Implementation Now we will create a simple service definition that just echoes the request string to the response. First, create a new directory: ``` mkdir -p src/main/java/echo/server ``` Then create a file named `Main.java` in that directory and add these lines to it: ``` java package echo.server; import co.paralleluniverse.fibers.Suspendable; import com.pinterest.quasar.thrift.TFiberServer; import com.pinterest.quasar.thrift.TFiberServerSocket; import echo.thrift.EchoRequest; import echo.thrift.EchoResponse; import echo.thrift.EchoService; import org.apache.thrift.TException; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.transport.TFastFramedTransport; import java.net.InetSocketAddress; public class Main { static final class EchoServiceImpl implements EchoService.Iface { @Override @Suspendable public EchoResponse echo(EchoRequest request) throws TException { return new EchoResponse().setMessage(request.getMessage()); } } @Suspendable public static void main(String[] args) throws Exception { EchoService.Processor processor = new EchoService.Processor(new EchoServiceImpl()); TFiberServerSocket trans = new TFiberServerSocket(new InetSocketAddress(9999)); TFiberServer.Args targs = new TFiberServer.Args(trans, processor) .protocolFactory(new TBinaryProtocol.Factory()) .transportFactory(new TFastFramedTransport.Factory()); TFiberServer server = new TFiberServer(targs); server.serve(); server.join(); } } ``` ## Load Testing Let's now write a simple load tester with JBender. The next few sections walk through the various parts of the load tester. If you are in a hurry skip to the section "Final Load Tester Program" and just follow the instructions from there. ### Intervals The first thing we need is a function to generate intervals (in nanoseconds) between executing requests. The JBender library comes with some predefined intervals: a uniform distribution (always wait the same amount of time between each request) and an exponential distribution. In this case we will use the exponential distribution, which means our server will experience load as generated by a [Poisson process](http://en.wikipedia.org/wiki/Poisson_process), which is fairly typical of server workloads on the Internet (with the usual caveats that every service is a special snowflake, etc, etc). We get the interval function with this code: ``` java final IntervalGenerator intervalGenerator = new ConstantIntervalGenerator(qps); ``` Where `qps` is our desired throughput measured in queries per second. It is also the reciprocal of the mean value of the exponential distribution used to generate the request arrival times (see the wikipedia article above). In practice this means you will see an average QPS that fluctuates around the target QPS (with less fluctuation as you increase the time interval over which you are averaging). ### Request Generator The second thing we need is a channel of requests to send to the HTTP server. When an interval has been generated and JBender is ready to send the request, it pulls the next request from this channel and spawns a Quasar _fiber_ (lightweight thread) to send the request to the server. This code creates and starts a simple synthetic Apache HTTP Client's `HttpGet` request generator to the "Hello World" server endpoint: ``` java new Fiber("message-producer", () -> { // Bench handling 10k reqs for (int i = 0; i < 10000; ++i) { requestCh.send(new HttpGet("http://localhost:8080/hello-world")); } requestCh.close(); return null; }).start(); ``` ### Request Executor The next thing we need is a request executor, which takes the requests generated above and sends them to the service. We will just use JBender's pre-built one and add a response validator: ``` final RequestExecutor requestExecutor = new FiberApacheHttpClientRequestExecutor<>((res) -> { if (res == null) { throw new AssertionError("Response is null"); } final int status = res.getStatusLine().getStatusCode(); if (status != 200) { throw new AssertionError("Status " + status + " is not 200"); } }, 1000000); ``` This validates that the response has actually been produced ans has a HTTP 200 status code. ### Recording Results The last thing we need is a channel that will output events as the load tester runs. This will let us listen to the load testers progress and record stats. We want this channel to be buffered so that we can run somewhat independently of the load test without slowing it down: ``` java final Channel> eventCh = Channels.newChannel(10000); ``` The `JBender.loadTestThroughput` function will send there events for things like how long it waits between requests, how much overage it is currently experiencing, and when requests start and end, how long they took and whether or not they had errors. That raw event stream makes it possible to analyze the results of a load test. JBender has a couple simple "recorders" that provide basic functionality for result analysis: * `LoggingRecorder` creates a recorder that takes a `Logger` and outputs each event. * `NewHistogramRecorder` records request latencies on a [`org.HdrHistogram.Histogram`](https://github.com/HdrHistogram/HdrHistogram). You can combine recorders using the `Recorder.record` function, so you can both log events and manage a histogram using code like this: ``` final Logger LOG = LoggerFactory.getLogger(LoadTest.class); final Histogram histogram = new Histogram(3600000000L, 3); record("recorder", eventCh, new HdrHistogramRecorder(histogram), new LoggingRecorder(LOG)); ``` The histogram takes two arguments: the maximum expected value and the number of precision digits and will adjust automatically to record latencies both efficiently and with high-definition buckets. It is relatively easy to build recorders, or to just process the events from the channel yourself: see the JBender documentation for more details on what events can be sent, and what data they contain. ### Final Load Tester Program Then create a file named `src/main/java/echo/jbender/Main.java`: ``` java package echo.jbender; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import co.paralleluniverse.fibers.Fiber; import echo.thrift.EchoService; import echo.thrift.EchoRequest; import echo.thrift.EchoResponse; import com.pinterest.jbender.JBender; import com.pinterest.jbender.events.TimingEvent; import com.pinterest.jbender.events.recording.HdrHistogramRecorder; import com.pinterest.jbender.events.recording.LoggingRecorder; import com.pinterest.jbender.executors.RequestExecutor; import com.pinterest.jbender.intervals.ConstantIntervalGenerator; import com.pinterest.jbender.intervals.IntervalGenerator; import com.pinterest.quasar.thrift.TFiberSocket; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TFastFramedTransport; import org.HdrHistogram.Histogram; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; import static com.pinterest.jbender.events.recording.Recorder.record; public class Main { static final class EchoRequestExecutor implements RequestExecutor { @Override public EchoResponse execute(long l, EchoRequest echoRequest) throws SuspendExecution, InterruptedException { try { TProtocol proto = new TBinaryProtocol(new TFastFramedTransport(TFiberSocket.open(new InetSocketAddress("localhost", 9999)))); EchoService.Client client = new EchoService.Client(proto); return client.echo(echoRequest); } catch (Exception ex) { LOG.error("failed to echo", ex); throw new RuntimeException(ex); } } } public static void main(String[] args) throws SuspendExecution, InterruptedException { final IntervalGenerator intervalGen = new ConstantIntervalGenerator(10000000); final RequestExecutor requestExector = new EchoRequestExecutor(); final Channel requestCh = Channels.newChannel(-1); final Channel> eventCh = Channels.newChannel(-1); // Requests generator new Fiber("req-gen", () -> { for (int i=0; i < 1000; ++i) { final EchoRequest req = new EchoRequest(); req.setMessage("foo"); requestCh.send(req); } requestCh.close(); }).start(); final Histogram histogram = new Histogram(3600000000L, 3); // Event recording, both HistHDR and logging record(eventCh, new HdrHistogramRecorder(histogram, 1000000), new LoggingRecorder(LOG)); JBender.loadTestThroughput(intervalGen, 0, requestCh, requestExector, eventCh); histogram.outputPercentileDistribution(System.out, 1000.0); } private static final Logger LOG = LoggerFactory.getLogger(Main.class); } ``` ### Run Server and Load Tester The first time you use these instructions, run `gradle wrapper` to create the gradle wrapper. With `./gradlew runServer` running in one terminal window, run the load tester in another one with `./gradlew runLoadTest`. The output of the load test will be the percentile distribution from the histogram. ================================================ FILE: doc/thrift/jbender-thrift-tutorial/build.gradle ================================================ // Gradle Thrift plugin buildscript { repositories { mavenCentral() } dependencies { classpath "co.tomlee.gradle.plugins:gradle-thrift-plugin:0.0.4" } } // Capsule plugin plugins { id "us.kirchmeier.capsule" version "1.0-rc1" } apply plugin: 'java' apply plugin: 'thrift' // Target JDK8 sourceCompatibility = 1.8 targetCompatibility = 1.8 group = 'jbendertut' version = '0.1-SNAPSHOT' // UTF8 encoding for sources [compileJava, compileTestJava]*.options*.encoding = "UTF-8" repositories { // Enable this if you want to use locally-built artifacts mavenLocal() mavenCentral() } configurations { quasar } dependencies { // Thrift API compile group: "org.apache.thrift", name: "libthrift", version: "0.9.1" // Quasar-Thrift server compile group: "com.pinterest", name: "quasar-thrift", version: "0.3" // Quasar API compile group: "co.paralleluniverse", name: "quasar-core", version: "0.7.3", classifier: "jdk8" // JBender API compile group: "com.pinterest", name: "jbender", version: "1.0" // Logging compile group: "org.slf4j", name: "slf4j-api", version: "1.7.12" compile group: "org.slf4j", name: "slf4j-simple", version: "1.7.12" // Useful to point to the Quasar agent later in JVM flags (and Capsule-building task) quasar group: "co.paralleluniverse", name: "quasar-core", version: "0.7.3", classifier: "jdk8" } // Thrift generators generateThriftSource { generators { java {} } } // Automatically find Quasar suspendables in Thrift-generated code classes { doFirst { ant.taskdef(name: 'scanSuspendables', classname: 'co.paralleluniverse.fibers.instrument.SuspendablesScanner', classpath: "build/classes/main:build/resources/main:${configurations.runtime.asPath}") ant.scanSuspendables( auto: true, suspendablesFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendables", supersFile: "$sourceSets.main.output.resourcesDir/META-INF/suspendable-supers", append: true) { fileset(dir: sourceSets.main.output.classesDir) } } } // Task building an handy self-contained server capsule task serverCapsule(type: FatCapsule) { applicationClass "com.pinterest.echo.jbender.server.Main" capsuleManifest { javaAgents = [configurations.quasar.iterator().next().getName()] // Aggressive optimizations jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts"] } } // Task building an handy self-contained load test capsule task capsule(type: FatCapsule) { applicationClass "com.pinterest.echo.jbender.Main" capsuleManifest { javaAgents = [configurations.quasar.iterator().next().getName()] // Aggressive optimizations jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts"] } } // Gradle JavaExec load test task task runLoadTest(type: JavaExec) { main = "echo.jbender.Main" classpath = sourceSets.main.runtimeClasspath // Aggressive optimizations and Quasar agent jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts", "-javaagent:${configurations.quasar.iterator().next()}"] // Add "=vdc" to the Quasar agent to trace instrumentation // Enable this to troubleshoot instrumentation issues // systemProperties = ["co.paralleluniverse.fibers.verifyInstrumentation" : "true"] } // Gradle JavaExec server task task runServer(type: JavaExec) { main = "echo.server.Main" classpath = sourceSets.main.runtimeClasspath // Aggressive optimizations and Quasar agent jvmArgs = ["-server", "-XX:+TieredCompilation", "-XX:+AggressiveOpts", "-javaagent:${configurations.quasar.iterator().next()}"] // Add "=vdc" to the Quasar agent to trace instrumentation // Enable this to troubleshoot instrumentation issues // systemProperties = ["co.paralleluniverse.fibers.verifyInstrumentation" : "true"] } ================================================ FILE: doc/thrift/jbender-thrift-tutorial/gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Nov 02 11:45:44 PST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.6-bin.zip ================================================ FILE: doc/thrift/jbender-thrift-tutorial/gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: doc/thrift/jbender-thrift-tutorial/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: doc/thrift/jbender-thrift-tutorial/src/main/java/echo/jbender/Main.java ================================================ package echo.jbender; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import co.paralleluniverse.fibers.Fiber; import echo.thrift.EchoService; import echo.thrift.EchoRequest; import echo.thrift.EchoResponse; import com.pinterest.jbender.JBender; import com.pinterest.jbender.events.TimingEvent; import com.pinterest.jbender.events.recording.HdrHistogramRecorder; import com.pinterest.jbender.events.recording.LoggingRecorder; import com.pinterest.jbender.executors.RequestExecutor; import com.pinterest.jbender.intervals.ConstantIntervalGenerator; import com.pinterest.jbender.intervals.IntervalGenerator; import com.pinterest.quasar.thrift.TFiberSocket; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.TFastFramedTransport; import org.HdrHistogram.Histogram; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; import static com.pinterest.jbender.events.recording.Recorder.record; public class Main { static final class EchoRequestExecutor implements RequestExecutor { @Override public EchoResponse execute(long l, EchoRequest echoRequest) throws SuspendExecution, InterruptedException { try { TProtocol proto = new TBinaryProtocol(new TFastFramedTransport(TFiberSocket.open(new InetSocketAddress("localhost", 9999)))); EchoService.Client client = new EchoService.Client(proto); return client.echo(echoRequest); } catch (Exception ex) { LOG.error("failed to echo", ex); throw new RuntimeException(ex); } } } public static void main(String[] args) throws SuspendExecution, InterruptedException { final IntervalGenerator intervalGen = new ConstantIntervalGenerator(10000000); final RequestExecutor requestExector = new EchoRequestExecutor(); final Channel requestCh = Channels.newChannel(-1); final Channel> eventCh = Channels.newChannel(-1); // Requests generator new Fiber("req-gen", () -> { for (int i=0; i < 1000; ++i) { final EchoRequest req = new EchoRequest(); req.setMessage("foo"); requestCh.send(req); } requestCh.close(); }).start(); final Histogram histogram = new Histogram(3600000000L, 3); // Event recording, both HistHDR and logging record(eventCh, new HdrHistogramRecorder(histogram, 1000000), new LoggingRecorder(LOG)); JBender.loadTestThroughput(intervalGen, 0, requestCh, requestExector, eventCh); histogram.outputPercentileDistribution(System.out, 1000.0); } private static final Logger LOG = LoggerFactory.getLogger(Main.class); } ================================================ FILE: doc/thrift/jbender-thrift-tutorial/src/main/java/echo/server/Main.java ================================================ package echo.server; import co.paralleluniverse.fibers.Suspendable; import com.pinterest.quasar.thrift.TFiberServer; import com.pinterest.quasar.thrift.TFiberServerSocket; import echo.thrift.EchoRequest; import echo.thrift.EchoResponse; import echo.thrift.EchoService; import org.apache.thrift.TException; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.transport.TFastFramedTransport; import java.net.InetSocketAddress; public class Main { static final class EchoServiceImpl implements EchoService.Iface { @Override @Suspendable public EchoResponse echo(EchoRequest request) throws TException { return new EchoResponse().setMessage(request.getMessage()); } } @Suspendable public static void main(String[] args) throws Exception { EchoService.Processor processor = new EchoService.Processor(new EchoServiceImpl()); TFiberServerSocket trans = new TFiberServerSocket(new InetSocketAddress(9999)); TFiberServer.Args targs = new TFiberServer.Args(trans, processor) .protocolFactory(new TBinaryProtocol.Factory()) .transportFactory(new TFastFramedTransport.Factory()); TFiberServer server = new TFiberServer(targs); server.serve(); server.join(); } } ================================================ FILE: doc/thrift/jbender-thrift-tutorial/src/main/thrift/echo.thrift ================================================ namespace java echo.thrift struct EchoRequest { 1: optional string message; } struct EchoResponse { 2: optional string message; } service EchoService { EchoResponse echo(1: EchoRequest request); } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue Aug 02 17:39:27 PDT 2016 distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ rootProject.name = 'jbender' ================================================ FILE: src/jmh/java/com/pinterest/jbender/JBenderBenchmark.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender; import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import com.pinterest.jbender.events.TimingEvent; import com.pinterest.jbender.events.recording.HdrHistogramRecorder; import com.pinterest.jbender.executors.NoopRequestExecutor; import com.pinterest.jbender.executors.RequestExecutor; import com.pinterest.jbender.intervals.ConstantIntervalGenerator; import com.pinterest.jbender.intervals.IntervalGenerator; import org.HdrHistogram.Histogram; import org.openjdk.jmh.annotations.Benchmark; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.ExecutionException; import static com.pinterest.jbender.events.recording.Recorder.record; public class JBenderBenchmark { // @Benchmark public Histogram loadtestThroughput() throws SuspendExecution, InterruptedException, ExecutionException { final IntervalGenerator intervalGenerator = new ConstantIntervalGenerator(1000); final RequestExecutor requestExecutor = new NoopRequestExecutor<>(); final Channel requestCh = Channels.newChannel(10000); final Channel> eventCh = Channels.newChannel(10000); // Requests generator new Fiber("req-gen", () -> { // Bench handling 10k reqs for (int i = 0; i < 10000; ++i) { requestCh.send("message"); } requestCh.close(); }).start(); final Histogram histogram = new Histogram(3600000000L, 3); // Event recording, both HistHDR and logging record(eventCh, new HdrHistogramRecorder(histogram, 1000000)); // Main new Fiber("jbender", () -> { JBender.loadTestThroughput(intervalGenerator, 0, requestCh, requestExecutor, eventCh); eventCh.close(); }).start().join(); // Avoid code elimination return histogram; } } ================================================ FILE: src/jmh/java/com/pinterest/jbender/JBenderHttpBenchmark.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender; import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import com.pinterest.jbender.events.TimingEvent; import com.pinterest.jbender.events.recording.HdrHistogramRecorder; import com.pinterest.jbender.executors.http.FiberApacheHttpClientRequestExecutor; import com.pinterest.jbender.intervals.ConstantIntervalGenerator; import com.pinterest.jbender.intervals.IntervalGenerator; import org.HdrHistogram.Histogram; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.openjdk.jmh.annotations.Benchmark; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.ExecutionException; import static com.pinterest.jbender.events.recording.Recorder.record; /** * Sample HTTP benchmark against {@url https://github.com/puniverse/comsat-gradle-template} */ public class JBenderHttpBenchmark { @Benchmark public Histogram loadtestHttpThroughput() throws SuspendExecution, InterruptedException, ExecutionException, IOException { final IntervalGenerator intervalGenerator = new ConstantIntervalGenerator(10000000); try (final FiberApacheHttpClientRequestExecutor requestExecutor = new FiberApacheHttpClientRequestExecutor<>((res) -> { if (res == null) { throw new AssertionError("Response is null"); } final int status = res.getStatusLine().getStatusCode(); if (status != 200) { throw new AssertionError("Status is " + status); } }, 1000000)) { final Channel requestCh = Channels.newChannel(1000); final Channel> eventCh = Channels.newChannel(1000); // Requests generator new Fiber("req-gen", () -> { // Bench handling 1k reqs for (int i = 0; i < 1000; ++i) { requestCh.send(new HttpGet("http://localhost:8080/hello-world")); } requestCh.close(); }).start(); final Histogram histogram = new Histogram(3600000000L, 3); // Event recording, both HistHDR and logging record(eventCh, new HdrHistogramRecorder(histogram, 1000000)); // Main new Fiber("jbender", () -> { JBender.loadTestThroughput(intervalGenerator, 0, requestCh, requestExecutor, eventCh); eventCh.close(); }).start().join(); // Avoid code elimination return histogram; } } } ================================================ FILE: src/main/java/com/pinterest/jbender/JBender.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender; import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.FiberScheduler; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.*; import co.paralleluniverse.strands.channels.ReceivePort; import co.paralleluniverse.strands.channels.SendPort; import co.paralleluniverse.strands.concurrent.Semaphore; import com.pinterest.jbender.events.TimingEvent; import com.pinterest.jbender.executors.RequestExecutor; import com.pinterest.jbender.intervals.IntervalGenerator; import com.pinterest.jbender.util.WaitGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; /** * JBender has static methods for running load tests by throughput or concurrency. */ public final class JBender { private static final Logger LOG = LoggerFactory.getLogger(JBender.class); private JBender() {} /** * Run a load test with the given throughput, using as many fibers as necessary. * * This method can be run in any strand; thread-fiber synchronization is more expensive than * fiber-fiber synchronization though, so if requests are being performed by fibers its best * to call this method inside a fiber. * * @param intervalGen provides the interval between subsequent requests (in nanoseconds). This * controls the throughput of the load test. * @param warmupRequests the number of requests to use as "warmup" for the load tester and the * service. These requests will not have TimingEvents generated in the * eventChannel, but will be sent to the remote service at the requested * rate. * @param requests provides requests for the load test, must be closed by the caller to stop the * load test (the load test will continue for as long as this channel is open, * even if there are no requests arriving). * @param executor executes the requests provided by the requests channel, returning a response * object. * @param eventChannel a TimingEvent is sent on this channel for every request executed during * the load test (whether the request succeeds or not). * @param the request type. * @param the response type. * @throws SuspendExecution * @throws InterruptedException */ public static void loadTestThroughput(final IntervalGenerator intervalGen, final int warmupRequests, final ReceivePort requests, final RequestExecutor executor, final SendPort> eventChannel) throws InterruptedException, SuspendExecution { loadTestThroughput(intervalGen, warmupRequests, requests, executor, eventChannel, null, null); } /** * Run a load test with a given throughput, using as many fibers as necessary. * * This method can be run in any strand; thread-fiber synchronization is more expensive than * fiber-fiber synchronization though, so if requests are being performed by fibers its best * to call this method inside a fiber. * * @param intervalGen provides the interval between subsequent requests (in nanoseconds). This * controls the throughput of the load test. * @param warmupRequests the number of requests to use as "warmup" for the load tester and the * service. These requests will not have TimingEvents generated in the * eventChannel, but will be sent to the remote service at the requested * rate. * @param requests provides requests for the load test, must be closed by the caller to stop the * load test (the load test will continue for as long as this channel is open, * even if there are no requests arriving). * @param executor executes the requests provided by the requests channel, returning a response * object. * @param eventChannel a TimingEvent is sent on this channel for every request executed during * the load test (whether the request succeeds or not). * @param fiberScheduler an optional scheduler for fibers that will perform the requests (the * default one will be used if {@code null}). * @param the request type. * @param the response type. * @throws SuspendExecution * @throws InterruptedException */ public static void loadTestThroughput(final IntervalGenerator intervalGen, final int warmupRequests, final ReceivePort requests, final RequestExecutor executor, final SendPort> eventChannel, final FiberScheduler fiberScheduler) throws InterruptedException, SuspendExecution { loadTestThroughput(intervalGen, warmupRequests, requests, executor, eventChannel, fiberScheduler, null); } /** * Run a load test with a given throughput, using as many fibers as necessary. * * This method can be run in any strand; thread-fiber synchronization is more expensive than * fiber-fiber synchronization though, so if requests are being performed by fibers its best * to call this method inside a fiber. * * @param intervalGen provides the interval between subsequent requests (in nanoseconds). This * controls the throughput of the load test. * @param warmupRequests the number of requests to use as "warmup" for the load tester and the * service. These requests will not have TimingEvents generated in the * eventChannel, but will be sent to the remote service at the requested * rate. * @param requests provides requests for the load test, must be closed by the caller to stop the * load test (the load test will continue for as long as this channel is open, * even if there are no requests arriving). * @param executor executes the requests provided by the requests channel, returning a response * object. * @param eventChannel a TimingEvent is sent on this channel for every request executed during * the load test (whether the request succeeds or not). * @param strandFactory an optional factory for strands that will perform the requests (the * default one will be used if {@code null}). * @param the request type. * @param the response type. * @throws SuspendExecution * @throws InterruptedException */ public static void loadTestThroughput(final IntervalGenerator intervalGen, final int warmupRequests, final ReceivePort requests, final RequestExecutor executor, final SendPort> eventChannel, final StrandFactory strandFactory) throws InterruptedException, SuspendExecution { loadTestThroughput(intervalGen, warmupRequests, requests, executor, eventChannel, null, strandFactory); } /** * Run a load test with a given number of fibers, making as many requests as possible. * * This method can be run in any strand; thread-fiber synchronization is more expensive than * fiber-fiber synchronization though, so if requests are being performed by fibers its best * to call this method inside a fiber. * * @param concurrency the number of Fibers to run. Each Fiber will execute requests serially with * as little overhead as possible. * @param warmupRequests the number of requests to use when warming up the load tester and the * remote service. These requests will not not have TimingEvents generated * in the eventChannel, but will be sent to the remote service. * @param requests provides requests for the load test and must be closed by the caller to stop * the load test (the load test will continue for as long as this channel is * open, even if there are no requests arriving). * @param executor executes the requets provided by the requests channel, returning a response * object. * @param eventChannel a TimingEvent is sent on this channel for every request executed during * the load test (whether the request succeeds or not). * @param the request type. * @param the response type. * @throws SuspendExecution * @throws InterruptedException */ public static void loadTestConcurrency(final int concurrency, final int warmupRequests, final ReceivePort requests, final RequestExecutor executor, final SendPort> eventChannel) throws SuspendExecution, InterruptedException { loadTestConcurrency(concurrency, warmupRequests, requests, executor, eventChannel, null, null); } /** * Run a load test with a given number of fibers, making as many requests as possible. * * This method can be run in any strand; thread-fiber synchronization is more expensive than * fiber-fiber synchronization though, so if requests are being performed by fibers its best * to call this method inside a fiber. * * @param concurrency the number of Fibers to run. Each Fiber will execute requests serially with * as little overhead as possible. * @param warmupRequests the number of requests to use when warming up the load tester and the * remote service. These requests will not not have TimingEvents generated * in the eventChannel, but will be sent to the remote service. * @param requests provides requests for the load test and must be closed by the caller to stop * the load test (the load test will continue for as long as this channel is * open, even if there are no requests arriving). * @param executor executes the requets provided by the requests channel, returning a response * object. * @param eventChannel a TimingEvent is sent on this channel for every request executed during * the load test (whether the request succeeds or not). * @param fiberScheduler an optional scheduler for fibers that will perform the requests (the * default one will be used if {@code null}). * @param the request type. * @param the response type. * @throws SuspendExecution * @throws InterruptedException */ public static void loadTestConcurrency(final int concurrency, final int warmupRequests, final ReceivePort requests, final RequestExecutor executor, final SendPort> eventChannel, final FiberScheduler fiberScheduler) throws SuspendExecution, InterruptedException { loadTestConcurrency(concurrency, warmupRequests, requests, executor, eventChannel, fiberScheduler, null); } /** * Run a load test with a given number of fibers, making as many requests as possible. * * This method can be run in any strand; thread-fiber synchronization is more expensive than * fiber-fiber synchronization though, so if requests are being performed by fibers its best * to call this method inside a fiber. * * @param concurrency the number of Fibers to run. Each Fiber will execute requests serially with * as little overhead as possible. * @param warmupRequests the number of requests to use when warming up the load tester and the * remote service. These requests will not not have TimingEvents generated * in the eventChannel, but will be sent to the remote service. * @param requests provides requests for the load test and must be closed by the caller to stop * the load test (the load test will continue for as long as this channel is * open, even if there are no requests arriving). * @param executor executes the requets provided by the requests channel, returning a response * object. * @param eventChannel a TimingEvent is sent on this channel for every request executed during * the load test (whether the request succeeds or not). * @param strandFactory an optional factory for strands that will perform the requests (the * default one will be used if {@code null}). * @param the request type. * @param the response type. * @throws SuspendExecution * @throws InterruptedException */ public static void loadTestConcurrency(final int concurrency, final int warmupRequests, final ReceivePort requests, final RequestExecutor executor, final SendPort> eventChannel, final StrandFactory strandFactory) throws SuspendExecution, InterruptedException { loadTestConcurrency(concurrency, warmupRequests, requests, executor, eventChannel, null, strandFactory); } private static class RequestExecOutcome { final long execTime; final Res response; final Exception exception; public RequestExecOutcome(final long execTime, final Res response, final Exception exception) { this.execTime = execTime; this.response = response; this.exception = exception; } } private static void loadTestThroughput(final IntervalGenerator intervalGen, int warmupRequests, final ReceivePort requests, final RequestExecutor executor, final SendPort> eventChannel, final FiberScheduler fiberScheduler, final StrandFactory strandFactory) throws SuspendExecution, InterruptedException { final long startNanos = System.nanoTime(); try { long overageNanos = 0; long overageStart = System.nanoTime(); final WaitGroup waitGroup = new WaitGroup(); while (true) { final long receiveNanosStart = System.nanoTime(); final Req request = requests.receive(); LOG.trace("Receive request time: {}", System.nanoTime() - receiveNanosStart); if (request == null) { break; } // Wait before dispatching request as much as generated, minus the remaining dispatching overhead // to be compensated for (up to having 0 waiting time of course, not negative) long waitNanos = intervalGen.nextInterval(System.nanoTime() - startNanos); final long adjust = Math.min(waitNanos, overageNanos); waitNanos -= adjust; overageNanos -= adjust; // Sleep in the accepting fiber long sleepNanosStart = System.nanoTime(); Strand.sleep(waitNanos, TimeUnit.NANOSECONDS); LOG.trace("Sleep time: {}", System.nanoTime() - sleepNanosStart); // Increment wait group count for new request handler waitGroup.add(); final long curWaitNanos = waitNanos; final long curWarmupRequests = warmupRequests; final long curOverageNanos = overageNanos; final SuspendableCallable sc = () -> { try { final RequestExecOutcome outcome = executeRequest(request, executor); if (curWarmupRequests <= 0) { report(curWaitNanos, curOverageNanos, outcome, eventChannel); } } finally { // Complete, decrementing wait group count waitGroup.done(); } return null; }; if (fiberScheduler != null) { new Fiber<>(fiberScheduler, sc).start(); } else if (strandFactory != null) { strandFactory.newStrand(sc).start(); } else { new Fiber<>(sc).start(); } final long nowNanos = System.nanoTime(); overageNanos += nowNanos - overageStart - waitNanos; overageStart = nowNanos; warmupRequests = Math.max(warmupRequests - 1, 0); } // Wait for all outstanding requests waitGroup.await(); } finally { eventChannel.close(); } } private static void loadTestConcurrency(final int concurrency, int warmupRequests, final ReceivePort requests, final RequestExecutor executor, final SendPort> eventChannel, final FiberScheduler fiberScheduler, final StrandFactory strandFactory) throws SuspendExecution, InterruptedException { try { final WaitGroup waitGroup = new WaitGroup(); final Semaphore running = new Semaphore(concurrency); while (true) { final Req request = requests.receive(); if (request == null) { break; } running.acquire(); waitGroup.add(); final long curWarmupRequests = warmupRequests; final SuspendableCallable sc = () -> { try { final RequestExecOutcome outcome = executeRequest(request, executor); if (curWarmupRequests <= 0) { report(0, 0, outcome, eventChannel); } } finally { running.release(); waitGroup.done(); } return null; }; if (fiberScheduler != null) { new Fiber<>(fiberScheduler, sc).start(); } else if (strandFactory != null) { strandFactory.newStrand(sc).start(); } else { new Fiber<>(sc).start(); } warmupRequests = Math.max(warmupRequests - 1, 0); } waitGroup.await(); } finally { eventChannel.close(); } } private static RequestExecOutcome executeRequest(final Req request, final RequestExecutor executor) throws SuspendExecution, InterruptedException { Res response = null; Exception exc = null; final long startNanos = System.nanoTime(); try { response = executor.execute(startNanos, request); } catch (final Exception ex) { LOG.error("Exception while executing request {}", request, ex); exc = ex; } return new RequestExecOutcome<>(System.nanoTime() - startNanos, response, exc); } private static void report(final long curWaitNanos, long curOverageNanos, final RequestExecOutcome outcome, final SendPort> eventChannel) throws SuspendExecution, InterruptedException { if (outcome.exception == null) { eventChannel.send( new TimingEvent<>(curWaitNanos, outcome.execTime, curOverageNanos, outcome.response)); } else { eventChannel.send( new TimingEvent<>(curWaitNanos, outcome.execTime, curOverageNanos, outcome.exception)); } } } ================================================ FILE: src/main/java/com/pinterest/jbender/events/TimingEvent.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.events; import com.google.common.base.MoreObjects; /** * TimingEvents collect timing information and result data for a single request from the load * tester. * * @param the response type from the service being tested. */ public class TimingEvent { public final long waitNanos; public final long durationNanos; public final long overageNanos; public final boolean isSuccess; public final Exception exception; public final T response; private TimingEvent(final long waitNanos, final long durationNanos, long overageNanos, final T response, final Exception exc) { this.response = response; this.exception = exc; this.waitNanos = waitNanos; this.durationNanos = durationNanos; this.overageNanos = overageNanos; this.isSuccess = exc == null; } public TimingEvent( final long waitNanos, final long durationNanos, long overageNanos, final T response) { this(waitNanos, durationNanos, overageNanos, response, null); } public TimingEvent( final long waitNanos, final long durationNanos, long overageNanos, final Exception exc) { this(waitNanos, durationNanos, overageNanos, null, exc); } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("waitNanos", waitNanos) .add("durationNanos", durationNanos) .add("overageNanos", overageNanos) .add("isSuccess", isSuccess) .add("exception", exception) .add("response", response) .toString(); } } ================================================ FILE: src/main/java/com/pinterest/jbender/events/recording/HdrHistogramRecorder.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.events.recording; import com.pinterest.jbender.events.TimingEvent; import org.HdrHistogram.Histogram; /** * Records the duration of each TimingEvent in an HdrHistogram and keeps track of the total number * of errors and the start and end time of the load test. */ public class HdrHistogramRecorder implements Recorder { private final long scale; private boolean started; // The histogram used to record durations public final Histogram histogram; // The number of errors seen by this recorder so far. public long errorCount; // The start time of the first TimingEvent seen by this recorder. This is an estimate of the // start time of the load test, and not exact. public long startNanos; // The end time of the latest TimingEvent seen by this recorder. public long endNanos; /** * Constructor. * * @param h the HdrHistogram object into which values are written. * @param scale the value by which to divide the durationNanos field of each TimingEvent before * recording it in the histogram. For example, to record milliseconds you would set * the scale to 1,000,000, for microseconds you would use 1,000, and so on. */ public HdrHistogramRecorder(final Histogram h, long scale) { this.histogram = h; this.scale = scale; this.errorCount = 0; this.startNanos = System.nanoTime(); this.endNanos = System.nanoTime(); this.started = false; } @Override public void record(final TimingEvent e) { if (!started) { started = true; startNanos = System.nanoTime() - e.durationNanos; } if (!e.isSuccess) { errorCount++; } histogram.recordValue(e.durationNanos / scale); endNanos = System.nanoTime(); } } ================================================ FILE: src/main/java/com/pinterest/jbender/events/recording/LoggingRecorder.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.events.recording; import com.pinterest.jbender.events.TimingEvent; import org.slf4j.Logger; /** * A very simple Recorder that logs each TimingEvent at the INFO level, using the * TimingEvent#toString method. */ public class LoggingRecorder implements Recorder { private final Logger log; public LoggingRecorder(final Logger l) { this.log = l; } @Override public void record(final TimingEvent e) { log.info(e.toString()); } } ================================================ FILE: src/main/java/com/pinterest/jbender/events/recording/Recorder.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.events.recording; import co.paralleluniverse.fibers.DefaultFiberScheduler; import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.FiberScheduler; import co.paralleluniverse.strands.channels.ReceivePort; import com.pinterest.jbender.events.TimingEvent; /** * Event recorder interface and main recording facility. */ @FunctionalInterface public interface Recorder { void record(TimingEvent e); /** * Record events in a separate Fiber using one or more Recorders. * * This method returns immediately after starting the Fiber to record events. * * @param rp a channel on which to receive TimingEvents, which are passed to the given recorders * in the order specified. * @param rs a list of one or more recorders to process each TimingEvent. * @param the type of the Response object in each TimingEvent. */ @SafeVarargs static Fiber record(final ReceivePort> rp, final Recorder... rs) { return record("jbender-recorder", null, rp, rs); } /** * Record events in a separate Fiber using one or more Recorders. * * This method returns immediately after starting the Fiber for recording events. * * @param fiberName the name of the fiber that records events. * @param fe an optional scheduler for the spawned recording fiber, null to use the default. * @param rp a channel on which to receive TimingEvents, which are passed to the underlying * Recorders. * @param rs zero or more Recorders, each of which receives every event (after the delay period) * in the order they are passed to this method. * @param the type of the Response object in each TimingEvent. */ @SafeVarargs static Fiber record(final String fiberName, final FiberScheduler fe, final ReceivePort> rp, final Recorder... rs) { return new Fiber(fiberName, fe != null ? fe : DefaultFiberScheduler.getInstance(), () -> { while (true) { final TimingEvent event = rp.receive(); if (event == null) { break; } for (final Recorder r : rs) { r.record(event); } } return null; }).start(); } } ================================================ FILE: src/main/java/com/pinterest/jbender/executors/NoopRequestExecutor.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.executors; import co.paralleluniverse.fibers.SuspendExecution; public class NoopRequestExecutor implements RequestExecutor { @Override public Void execute(final long nanoTime, final Q request) throws SuspendExecution, InterruptedException { // NOP return null; } } ================================================ FILE: src/main/java/com/pinterest/jbender/executors/RequestExecutor.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.executors; import co.paralleluniverse.fibers.SuspendExecution; /** * Request executor interface. * * @param The request class. * @param The response class. */ public interface RequestExecutor { /** * The suspendable request execution logic. * * @param nanoTime The request execution start time. * @param request The request to be executed. * * @return The response value. */ S execute(long nanoTime, Q request) throws SuspendExecution, InterruptedException; } ================================================ FILE: src/main/java/com/pinterest/jbender/executors/SleepyRequestExecutor.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.executors; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.Strand; public class SleepyRequestExecutor implements RequestExecutor { private final int sleepMillis; private final int sleepNanos; public SleepyRequestExecutor(int sleepMillis, int sleepNanos) { this.sleepMillis = sleepMillis; this.sleepNanos = sleepNanos; } @Override public Void execute(final long nanoTime, final Q request) throws SuspendExecution, InterruptedException { Strand.sleep(sleepMillis, sleepNanos); return null; } } ================================================ FILE: src/main/java/com/pinterest/jbender/executors/Validator.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.executors; /** * Validator of {@code T} values. * * @param The value class. */ @FunctionalInterface public interface Validator { /** * Will throw an unchecked exception if validation failes */ void validate(final T value); } ================================================ FILE: src/main/java/com/pinterest/jbender/executors/http/FiberApacheHttpClientRequestExecutor.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.executors.http; import co.paralleluniverse.common.util.Exceptions; import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.fibers.httpclient.FiberHttpClient; import co.paralleluniverse.strands.SuspendableCallable; import com.pinterest.jbender.executors.RequestExecutor; import com.pinterest.jbender.executors.Validator; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager; import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor; import org.apache.http.impl.nio.reactor.IOReactorConfig; import org.apache.http.nio.reactor.IOReactorException; import java.io.IOException; import java.util.concurrent.ExecutionException; /** * Executor base class offering a Comsat-based implementation of an HTTP request executor. */ public class FiberApacheHttpClientRequestExecutor implements RequestExecutor, AutoCloseable { // Inspired by https://github.com/puniverse/photon/blob/master/src/main/java/co/paralleluniverse/photon/Photon.java private final Validator validator; private final FiberHttpClient client; public FiberApacheHttpClientRequestExecutor(final Validator resValidator, final int maxConnections, final int timeout, final int parallelism) throws IOReactorException { final DefaultConnectingIOReactor ioreactor = new DefaultConnectingIOReactor(IOReactorConfig.custom(). setConnectTimeout(timeout). setIoThreadCount(parallelism). setSoTimeout(timeout). build()); final PoolingNHttpClientConnectionManager mngr = new PoolingNHttpClientConnectionManager(ioreactor); mngr.setDefaultMaxPerRoute(maxConnections); mngr.setMaxTotal(maxConnections); final CloseableHttpAsyncClient ahc = HttpAsyncClientBuilder.create(). setConnectionManager(mngr). setDefaultRequestConfig(RequestConfig.custom().setLocalAddress(null).build()).build(); client = new FiberHttpClient(ahc); validator = resValidator; } public FiberApacheHttpClientRequestExecutor(final Validator resValidator, final int maxConnections, final int timeout) throws IOReactorException { this(resValidator, maxConnections, timeout, Runtime.getRuntime().availableProcessors()); } public FiberApacheHttpClientRequestExecutor(final Validator resValidator, final int maxConnections) throws IOReactorException { this(resValidator, maxConnections, 0); } public FiberApacheHttpClientRequestExecutor(final int maxConnections) throws IOReactorException { this(null, maxConnections, 0); } // TODO Figure out meaningful and sensible default for maxConnections and add no-args constructor @Override public CloseableHttpResponse execute(final long nanoTime, final HttpRequestBase request) throws SuspendExecution, InterruptedException { // TODO See if timeout can be configured per-request final CloseableHttpResponse ret; try { ret = new Fiber<>((SuspendableCallable) () -> { try { return client.execute(request); } catch (final IOException e) { throw Exceptions.rethrowUnwrap(e); } }).start().get(); } catch (final ExecutionException e) { throw Exceptions.rethrowUnwrap(e); } if (validator != null) { validator.validate(ret); } return ret; } @Override public void close() throws IOException { client.close(); } } ================================================ FILE: src/main/java/com/pinterest/jbender/intervals/ConstantIntervalGenerator.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.intervals; public class ConstantIntervalGenerator implements IntervalGenerator { private final long interval; public ConstantIntervalGenerator(long interval) { this.interval = interval; } @Override public long nextInterval(long nanoTimeSinceStart) { return interval; } } ================================================ FILE: src/main/java/com/pinterest/jbender/intervals/ExponentialIntervalGenerator.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.intervals; import java.util.Random; /** * Poisson distribution request interval generator. */ public class ExponentialIntervalGenerator implements IntervalGenerator { private final double nanosPerQuery; private final Random rand; public ExponentialIntervalGenerator(int queriesPerSecond) { nanosPerQuery = 1000000000.0 / queriesPerSecond; this.rand = new Random(); } @Override public long nextInterval(long nanoTimeSinceStart) { return (long) (-Math.log(rand.nextDouble()) * this.nanosPerQuery); } } ================================================ FILE: src/main/java/com/pinterest/jbender/intervals/IntervalGenerator.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.intervals; /** * Request interval duration generator interface. */ @FunctionalInterface public interface IntervalGenerator { long nextInterval(long nanoTimeSinceStart); } ================================================ FILE: src/main/java/com/pinterest/jbender/util/ConnectionPool.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.util; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.IOException; /** * An inconvenient ConnectionPool for sockets that are closeable. * * Use will typically look like this: * * * ConnectionPool pool = new ConnectionPool(...); * T t = null; * try { * t = pool.acquire(); * ... * pool.release(t); * } catch (Exception ex) { * try { * pool.releaseAfterError(t); * } catch (IOException ioex) { * LOG.warn("Failed to close connection", ioex); * } * } * * * @param */ public class ConnectionPool { @FunctionalInterface public interface SuspendableSupplierWithIO { T get() throws IOException, SuspendExecution; } private final SuspendableSupplierWithIO supplier; private final Channel pool; public ConnectionPool(SuspendableSupplierWithIO supplier, int maxPoolSize) { this.supplier = supplier; pool = Channels.newChannel(maxPoolSize, Channels.OverflowPolicy.BLOCK, false, false); } public T acquire() throws IOException, SuspendExecution { T t = pool.tryReceive(); if (t == null) { t = supplier.get(); } return t; } public void release(T t) throws IOException { if (t == null) { return; } boolean released = pool.trySend(t); if (!released) { t.close(); } } public void releaseAfterError(T t) throws IOException { if (t == null) { return; } t.close(); } } ================================================ FILE: src/main/java/com/pinterest/jbender/util/ListReceivePort.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.util; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.Timeout; import co.paralleluniverse.strands.channels.ReceivePort; import java.util.List; import java.util.concurrent.TimeUnit; public class ListReceivePort implements ReceivePort { private final List list; private final int total; private int cur; public ListReceivePort(List list) { this.list = list; total = list.size(); cur = 0; } public ListReceivePort(List list, int total) { this.list = list; this.total = total; cur = 0; } @Override public T receive() throws SuspendExecution, InterruptedException { if (cur >= total) { return null; } return list.get(cur++ % list.size()); } @Override public T receive(long timeout, TimeUnit unit) throws SuspendExecution, InterruptedException { return receive(); } @Override public T receive(Timeout timeout) throws SuspendExecution, InterruptedException { return receive(); } @Override public T tryReceive() { if (cur >= total) { return null; } return list.get(cur++ % list.size()); } @Override public void close() {} @Override public boolean isClosed() { return false; } } ================================================ FILE: src/main/java/com/pinterest/jbender/util/WaitGroup.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender.util; import java.util.concurrent.atomic.AtomicLong; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.Strand; /** * Simplified phaser supporting up to {@code Long.MAX_VALUE} fibers. */ public class WaitGroup { private volatile Strand waiter; private AtomicLong running; public WaitGroup() { running = new AtomicLong(); waiter = null; } public void add() { running.incrementAndGet(); } public void done() { long count = running.decrementAndGet(); if (count == 0 && waiter != null) { waiter.unpark(); } } public void await() throws SuspendExecution { waiter = Strand.currentStrand(); while (running.get() > 0) { Strand.park(); } } } ================================================ FILE: src/test/java/com/pinterest/jbender/JBenderTest.java ================================================ /* Copyright 2014 Pinterest.com 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. */ package com.pinterest.jbender; import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import com.google.common.collect.Sets; import com.pinterest.jbender.events.TimingEvent; import com.pinterest.jbender.executors.RequestExecutor; import com.pinterest.jbender.intervals.ConstantIntervalGenerator; import com.pinterest.jbender.intervals.IntervalGenerator; import org.junit.Test; import java.util.Set; import static org.junit.Assert.assertEquals; public class JBenderTest { private static final class FakeRequestExecutor implements RequestExecutor { @Override public Integer execute(long nanoTime, Integer request) throws SuspendExecution, InterruptedException { return request; } } private void assertEvents(Channel> eventCh, int start, int end) throws SuspendExecution, InterruptedException { Set actual = Sets.newHashSetWithExpectedSize(end - start); Set expected = Sets.newHashSetWithExpectedSize(end - start); for (int i = start; i < end; ++i) { expected.add(i); } int cur = start; while (true) { TimingEvent t = eventCh.receive(); if (t == null) { break; } Integer i = t.response; actual.add(i); cur++; } assertEquals(cur, end); assertEquals(actual, expected); } private void requests(Channel requestCh, int count) { new Fiber(() -> { for (int i = 0; i < count; ++i) { requestCh.send(i); } requestCh.close(); }).start(); } @Test public void testLoadTestThroughputNoRequests() throws SuspendExecution, InterruptedException { IntervalGenerator intervalGen = new ConstantIntervalGenerator(0); Channel requestCh = Channels.newChannel(-1); Channel> eventCh = Channels.newChannel(-1); RequestExecutor executor = new FakeRequestExecutor(); requests(requestCh, 0); JBender.loadTestThroughput(intervalGen, 0, requestCh, executor, eventCh); assertEvents(eventCh, 0, 0); } @Test public void testLoadTestThroughputOneRequest() throws SuspendExecution, InterruptedException { IntervalGenerator intervalGen = new ConstantIntervalGenerator(0); Channel requestCh = Channels.newChannel(-1); Channel> eventCh = Channels.newChannel(-1); RequestExecutor executor = new FakeRequestExecutor(); requests(requestCh, 1); JBender.loadTestThroughput(intervalGen, 0, requestCh, executor, eventCh); assertEvents(eventCh, 0, 1); } @Test public void testLoadTestThroughputTenRequests() throws SuspendExecution, InterruptedException { IntervalGenerator intervalGen = new ConstantIntervalGenerator(0); Channel requestCh = Channels.newChannel(-1); Channel> eventCh = Channels.newChannel(-1); RequestExecutor executor = new FakeRequestExecutor(); requests(requestCh, 10); JBender.loadTestThroughput(intervalGen, 0, requestCh, executor, eventCh); assertEvents(eventCh, 0, 10); } @Test public void testLoadTestThroughputWarmup() throws SuspendExecution, InterruptedException { IntervalGenerator intervalGen = new ConstantIntervalGenerator(0); Channel requestCh = Channels.newChannel(-1); Channel> eventCh = Channels.newChannel(-1); RequestExecutor executor = new FakeRequestExecutor(); requests(requestCh, 10); JBender.loadTestThroughput(intervalGen, 5, requestCh, executor, eventCh); assertEvents(eventCh, 5, 10); } @Test public void testLoadTestConcurrencyNoRequests() throws SuspendExecution, InterruptedException { Channel requestCh = Channels.newChannel(-1); Channel> eventCh = Channels.newChannel(-1); RequestExecutor executor = new FakeRequestExecutor(); requests(requestCh, 0); JBender.loadTestConcurrency(1, 0, requestCh, executor, eventCh); assertEvents(eventCh, 0, 0); } @Test public void testLoadTestConcurrencyOneRequest() throws SuspendExecution, InterruptedException { Channel requestCh = Channels.newChannel(-1); Channel> eventCh = Channels.newChannel(-1); RequestExecutor executor = new FakeRequestExecutor(); requests(requestCh, 1); JBender.loadTestConcurrency(1, 0, requestCh, executor, eventCh); assertEvents(eventCh, 0, 1); } @Test public void testLoadTestConcurrencyTenRequests() throws SuspendExecution, InterruptedException { Channel requestCh = Channels.newChannel(-1); Channel> eventCh = Channels.newChannel(-1); RequestExecutor executor = new FakeRequestExecutor(); requests(requestCh, 10); JBender.loadTestConcurrency(1, 0, requestCh, executor, eventCh); assertEvents(eventCh, 0, 10); } @Test public void testLoadTestConcurrencyWarmup() throws SuspendExecution, InterruptedException { Channel requestCh = Channels.newChannel(-1); Channel> eventCh = Channels.newChannel(-1); RequestExecutor executor = new FakeRequestExecutor(); requests(requestCh, 10); JBender.loadTestConcurrency(1, 5, requestCh, executor, eventCh); assertEvents(eventCh, 5, 10); } }