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
=======
[](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<Void>("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<TimingEvent<CloseableHttpResponse>> 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<HttpGet> requestCh = Channels.newChannel(1000);
final Channel<Event<CloseableHttpResponse>> eventCh = Channels.newChannel(1000);
// Requests generator
new Fiber<Void>("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<Void>("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<HttpGet> requestCh = Channels.newChannel(1000);
final Channel<TimingEvent<CloseableHttpResponse>> eventCh = Channels.newChannel(1000);
// Requests generator
new Fiber<Void>("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<Void>("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<EchoService.Iface> processor =
new EchoService.Processor<EchoService.Iface>(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<Void>("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<HttpGet, CloseableHttpResponse> 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<Event<CloseableHttpResponse>> 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<EchoRequest, EchoResponse> {
@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<EchoRequest, EchoResponse> requestExector = new EchoRequestExecutor();
final Channel<EchoRequest> requestCh = Channels.newChannel(-1);
final Channel<TimingEvent<EchoResponse>> eventCh = Channels.newChannel(-1);
// Requests generator
new Fiber<Void>("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<EchoRequest, EchoResponse> {
@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<EchoRequest, EchoResponse> requestExector = new EchoRequestExecutor();
final Channel<EchoRequest> requestCh = Channels.newChannel(-1);
final Channel<TimingEvent<EchoResponse>> eventCh = Channels.newChannel(-1);
// Requests generator
new Fiber<Void>("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<EchoService.Iface> processor =
new EchoService.Processor<EchoService.Iface>(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<String, Void> requestExecutor = new NoopRequestExecutor<>();
final Channel<String> requestCh = Channels.newChannel(10000);
final Channel<TimingEvent<Void>> eventCh = Channels.newChannel(10000);
// Requests generator
new Fiber<Void>("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<Void>("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<HttpGet> requestCh = Channels.newChannel(1000);
final Channel<TimingEvent<CloseableHttpResponse>> eventCh = Channels.newChannel(1000);
// Requests generator
new Fiber<Void>("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<Void>("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 <Req> the request type.
* @param <Res> the response type.
* @throws SuspendExecution
* @throws InterruptedException
*/
public static <Req, Res> void loadTestThroughput(final IntervalGenerator intervalGen,
final int warmupRequests,
final ReceivePort<Req> requests,
final RequestExecutor<Req, Res> executor,
final SendPort<TimingEvent<Res>> 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 <Req> the request type.
* @param <Res> the response type.
* @throws SuspendExecution
* @throws InterruptedException
*/
public static <Req, Res> void loadTestThroughput(final IntervalGenerator intervalGen,
final int warmupRequests,
final ReceivePort<Req> requests,
final RequestExecutor<Req, Res> executor,
final SendPort<TimingEvent<Res>> 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 <Req> the request type.
* @param <Res> the response type.
* @throws SuspendExecution
* @throws InterruptedException
*/
public static <Req, Res> void loadTestThroughput(final IntervalGenerator intervalGen,
final int warmupRequests,
final ReceivePort<Req> requests,
final RequestExecutor<Req, Res> executor,
final SendPort<TimingEvent<Res>> 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 <Req> the request type.
* @param <Res> the response type.
* @throws SuspendExecution
* @throws InterruptedException
*/
public static <Req, Res> void loadTestConcurrency(final int concurrency,
final int warmupRequests,
final ReceivePort<Req> requests,
final RequestExecutor<Req, Res> executor,
final SendPort<TimingEvent<Res>> 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 <Req> the request type.
* @param <Res> the response type.
* @throws SuspendExecution
* @throws InterruptedException
*/
public static <Req, Res> void loadTestConcurrency(final int concurrency,
final int warmupRequests,
final ReceivePort<Req> requests,
final RequestExecutor<Req, Res> executor,
final SendPort<TimingEvent<Res>> 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 <Req> the request type.
* @param <Res> the response type.
* @throws SuspendExecution
* @throws InterruptedException
*/
public static <Req, Res> void loadTestConcurrency(final int concurrency,
final int warmupRequests,
final ReceivePort<Req> requests,
final RequestExecutor<Req, Res> executor,
final SendPort<TimingEvent<Res>> eventChannel,
final StrandFactory strandFactory)
throws SuspendExecution, InterruptedException
{
loadTestConcurrency(concurrency, warmupRequests, requests, executor, eventChannel, null, strandFactory);
}
private static class RequestExecOutcome<Res> {
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 <Req, Res> void loadTestThroughput(final IntervalGenerator intervalGen,
int warmupRequests,
final ReceivePort<Req> requests,
final RequestExecutor<Req, Res> executor,
final SendPort<TimingEvent<Res>> 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<Void> sc = () -> {
try {
final RequestExecOutcome<Res> 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 <Req, Res> void loadTestConcurrency(final int concurrency,
int warmupRequests,
final ReceivePort<Req> requests,
final RequestExecutor<Req, Res> executor,
final SendPort<TimingEvent<Res>> 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<Void> sc = () -> {
try {
final RequestExecOutcome<Res> 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 <Req, Res> RequestExecOutcome<Res> executeRequest(final Req request, final RequestExecutor<Req, Res> 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 <Res> void report(final long curWaitNanos,
long curOverageNanos,
final RequestExecOutcome<Res> outcome,
final SendPort<TimingEvent<Res>> 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 <T> the response type from the service being tested.
*/
public class TimingEvent<T> {
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<T> {
void record(TimingEvent<T> 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 <T> the type of the Response object in each TimingEvent.
*/
@SafeVarargs
static <T> Fiber<Void> record(final ReceivePort<TimingEvent<T>> rp, final Recorder<T>... 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 <T> the type of the Response object in each TimingEvent.
*/
@SafeVarargs
static <T> Fiber<Void> record(final String fiberName,
final FiberScheduler fe,
final ReceivePort<TimingEvent<T>> rp,
final Recorder<T>... rs)
{
return new Fiber<Void>(fiberName, fe != null ? fe : DefaultFiberScheduler.getInstance(), () -> {
while (true) {
final TimingEvent<T> event = rp.receive();
if (event == null) {
break;
}
for (final Recorder<T> 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<Q> implements RequestExecutor<Q, Void> {
@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 <Q> The request class.
* @param <S> The response class.
*/
public interface RequestExecutor<Q, S> {
/**
* 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<Q> implements RequestExecutor<Q, Void> {
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 <T> The value class.
*/
@FunctionalInterface
public interface Validator<T> {
/**
* 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<X extends HttpRequestBase> implements RequestExecutor<X, CloseableHttpResponse>, AutoCloseable {
// Inspired by https://github.com/puniverse/photon/blob/master/src/main/java/co/paralleluniverse/photon/Photon.java
private final Validator<CloseableHttpResponse> validator;
private final FiberHttpClient client;
public FiberApacheHttpClientRequestExecutor(final Validator<CloseableHttpResponse> 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<CloseableHttpResponse> resValidator, final int maxConnections, final int timeout) throws IOReactorException {
this(resValidator, maxConnections, timeout, Runtime.getRuntime().availableProcessors());
}
public FiberApacheHttpClientRequestExecutor(final Validator<CloseableHttpResponse> 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<CloseableHttpResponse>) () -> {
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:
*
* <code>
* ConnectionPool<T> 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);
* }
* }
* </code>
*
* @param <T>
*/
public class ConnectionPool<T extends Closeable> {
@FunctionalInterface
public interface SuspendableSupplierWithIO<T> {
T get() throws IOException, SuspendExecution;
}
private final SuspendableSupplierWithIO<T> supplier;
private final Channel<T> pool;
public ConnectionPool(SuspendableSupplierWithIO<T> 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<T> implements ReceivePort<T> {
private final List<T> list;
private final int total;
private int cur;
public ListReceivePort(List<T> list) {
this.list = list;
total = list.size();
cur = 0;
}
public ListReceivePort(List<T> 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<Integer, Integer> {
@Override
public Integer execute(long nanoTime, Integer request) throws SuspendExecution, InterruptedException {
return request;
}
}
private void assertEvents(Channel<TimingEvent<Integer>> eventCh, int start, int end) throws SuspendExecution, InterruptedException {
Set<Integer> actual = Sets.newHashSetWithExpectedSize(end - start);
Set<Integer> expected = Sets.newHashSetWithExpectedSize(end - start);
for (int i = start; i < end; ++i) {
expected.add(i);
}
int cur = start;
while (true) {
TimingEvent<Integer> 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<Integer> requestCh, int count) {
new Fiber<Void>(() -> {
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<Integer> requestCh = Channels.newChannel(-1);
Channel<TimingEvent<Integer>> eventCh = Channels.newChannel(-1);
RequestExecutor<Integer, Integer> 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<Integer> requestCh = Channels.newChannel(-1);
Channel<TimingEvent<Integer>> eventCh = Channels.newChannel(-1);
RequestExecutor<Integer, Integer> 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<Integer> requestCh = Channels.newChannel(-1);
Channel<TimingEvent<Integer>> eventCh = Channels.newChannel(-1);
RequestExecutor<Integer, Integer> 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<Integer> requestCh = Channels.newChannel(-1);
Channel<TimingEvent<Integer>> eventCh = Channels.newChannel(-1);
RequestExecutor<Integer, Integer> 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<Integer> requestCh = Channels.newChannel(-1);
Channel<TimingEvent<Integer>> eventCh = Channels.newChannel(-1);
RequestExecutor<Integer, Integer> 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<Integer> requestCh = Channels.newChannel(-1);
Channel<TimingEvent<Integer>> eventCh = Channels.newChannel(-1);
RequestExecutor<Integer, Integer> 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<Integer> requestCh = Channels.newChannel(-1);
Channel<TimingEvent<Integer>> eventCh = Channels.newChannel(-1);
RequestExecutor<Integer, Integer> 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<Integer> requestCh = Channels.newChannel(-1);
Channel<TimingEvent<Integer>> eventCh = Channels.newChannel(-1);
RequestExecutor<Integer, Integer> executor = new FakeRequestExecutor();
requests(requestCh, 10);
JBender.loadTestConcurrency(1, 5, requestCh, executor, eventCh);
assertEvents(eventCh, 5, 10);
}
}
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
SYMBOL INDEX (101 symbols across 22 files)
FILE: doc/http/jbender-http-tutorial/src/main/java/LoadTest.java
class LoadTest (line 26) | public class LoadTest {
method main (line 27) | public static void main(final String[] args) throws SuspendExecution, ...
FILE: doc/thrift/jbender-thrift-tutorial/src/main/java/echo/jbender/Main.java
class Main (line 29) | public class Main {
class EchoRequestExecutor (line 30) | static final class EchoRequestExecutor implements RequestExecutor<Echo...
method execute (line 31) | @Override
method main (line 44) | public static void main(String[] args) throws SuspendExecution, Interr...
FILE: doc/thrift/jbender-thrift-tutorial/src/main/java/echo/server/Main.java
class Main (line 14) | public class Main {
class EchoServiceImpl (line 15) | static final class EchoServiceImpl implements EchoService.Iface {
method echo (line 16) | @Override
method main (line 23) | @Suspendable
FILE: src/jmh/java/com/pinterest/jbender/JBenderBenchmark.java
class JBenderBenchmark (line 34) | public class JBenderBenchmark {
method loadtestThroughput (line 37) | public Histogram loadtestThroughput() throws SuspendExecution, Interru...
FILE: src/jmh/java/com/pinterest/jbender/JBenderHttpBenchmark.java
class JBenderHttpBenchmark (line 39) | public class JBenderHttpBenchmark {
method loadtestHttpThroughput (line 41) | @Benchmark
FILE: src/main/java/com/pinterest/jbender/JBender.java
class JBender (line 34) | public final class JBender {
method JBender (line 37) | private JBender() {}
method loadTestThroughput (line 64) | public static <Req, Res> void loadTestThroughput(final IntervalGenerat...
method loadTestThroughput (line 101) | public static <Req, Res> void loadTestThroughput(final IntervalGenerat...
method loadTestThroughput (line 139) | public static <Req, Res> void loadTestThroughput(final IntervalGenerat...
method loadTestConcurrency (line 174) | public static <Req, Res> void loadTestConcurrency(final int concurrency,
method loadTestConcurrency (line 210) | public static <Req, Res> void loadTestConcurrency(final int concurrency,
method loadTestConcurrency (line 247) | public static <Req, Res> void loadTestConcurrency(final int concurrency,
class RequestExecOutcome (line 258) | private static class RequestExecOutcome<Res> {
method RequestExecOutcome (line 263) | public RequestExecOutcome(final long execTime, final Res response, f...
method loadTestThroughput (line 270) | private static <Req, Res> void loadTestThroughput(final IntervalGenera...
method loadTestConcurrency (line 345) | private static <Req, Res> void loadTestConcurrency(final int concurrency,
method executeRequest (line 396) | private static <Req, Res> RequestExecOutcome<Res> executeRequest(final...
method report (line 411) | private static <Res> void report(final long curWaitNanos,
FILE: src/main/java/com/pinterest/jbender/events/TimingEvent.java
class TimingEvent (line 23) | public class TimingEvent<T> {
method TimingEvent (line 31) | private TimingEvent(final long waitNanos,
method TimingEvent (line 45) | public TimingEvent(
method TimingEvent (line 50) | public TimingEvent(
method toString (line 55) | @Override
FILE: src/main/java/com/pinterest/jbender/events/recording/HdrHistogramRecorder.java
class HdrHistogramRecorder (line 22) | public class HdrHistogramRecorder implements Recorder {
method HdrHistogramRecorder (line 47) | public HdrHistogramRecorder(final Histogram h, long scale) {
method record (line 56) | @Override
FILE: src/main/java/com/pinterest/jbender/events/recording/LoggingRecorder.java
class LoggingRecorder (line 22) | public class LoggingRecorder implements Recorder {
method LoggingRecorder (line 25) | public LoggingRecorder(final Logger l) {
method record (line 29) | @Override
FILE: src/main/java/com/pinterest/jbender/events/recording/Recorder.java
type Recorder (line 24) | @FunctionalInterface
method record (line 26) | void record(TimingEvent<T> e);
method record (line 38) | @SafeVarargs
method record (line 56) | @SafeVarargs
FILE: src/main/java/com/pinterest/jbender/executors/NoopRequestExecutor.java
class NoopRequestExecutor (line 17) | public class NoopRequestExecutor<Q> implements RequestExecutor<Q, Void> {
method execute (line 18) | @Override
FILE: src/main/java/com/pinterest/jbender/executors/RequestExecutor.java
type RequestExecutor (line 23) | public interface RequestExecutor<Q, S> {
method execute (line 32) | S execute(long nanoTime, Q request) throws SuspendExecution, Interrupt...
FILE: src/main/java/com/pinterest/jbender/executors/SleepyRequestExecutor.java
class SleepyRequestExecutor (line 18) | public class SleepyRequestExecutor<Q> implements RequestExecutor<Q, Void> {
method SleepyRequestExecutor (line 22) | public SleepyRequestExecutor(int sleepMillis, int sleepNanos) {
method execute (line 27) | @Override
FILE: src/main/java/com/pinterest/jbender/executors/Validator.java
type Validator (line 20) | @FunctionalInterface
method validate (line 25) | void validate(final T value);
FILE: src/main/java/com/pinterest/jbender/executors/http/FiberApacheHttpClientRequestExecutor.java
class FiberApacheHttpClientRequestExecutor (line 38) | public class FiberApacheHttpClientRequestExecutor<X extends HttpRequestB...
method FiberApacheHttpClientRequestExecutor (line 44) | public FiberApacheHttpClientRequestExecutor(final Validator<CloseableH...
method FiberApacheHttpClientRequestExecutor (line 63) | public FiberApacheHttpClientRequestExecutor(final Validator<CloseableH...
method FiberApacheHttpClientRequestExecutor (line 67) | public FiberApacheHttpClientRequestExecutor(final Validator<CloseableH...
method FiberApacheHttpClientRequestExecutor (line 71) | public FiberApacheHttpClientRequestExecutor(final int maxConnections) ...
method execute (line 77) | @Override
method close (line 98) | @Override
FILE: src/main/java/com/pinterest/jbender/intervals/ConstantIntervalGenerator.java
class ConstantIntervalGenerator (line 15) | public class ConstantIntervalGenerator implements IntervalGenerator {
method ConstantIntervalGenerator (line 18) | public ConstantIntervalGenerator(long interval) {
method nextInterval (line 22) | @Override
FILE: src/main/java/com/pinterest/jbender/intervals/ExponentialIntervalGenerator.java
class ExponentialIntervalGenerator (line 20) | public class ExponentialIntervalGenerator implements IntervalGenerator {
method ExponentialIntervalGenerator (line 24) | public ExponentialIntervalGenerator(int queriesPerSecond) {
method nextInterval (line 29) | @Override
FILE: src/main/java/com/pinterest/jbender/intervals/IntervalGenerator.java
type IntervalGenerator (line 18) | @FunctionalInterface
method nextInterval (line 20) | long nextInterval(long nanoTimeSinceStart);
FILE: src/main/java/com/pinterest/jbender/util/ConnectionPool.java
class ConnectionPool (line 47) | public class ConnectionPool<T extends Closeable> {
type SuspendableSupplierWithIO (line 48) | @FunctionalInterface
method get (line 50) | T get() throws IOException, SuspendExecution;
method ConnectionPool (line 56) | public ConnectionPool(SuspendableSupplierWithIO<T> supplier, int maxPo...
method acquire (line 61) | public T acquire() throws IOException, SuspendExecution {
method release (line 69) | public void release(T t) throws IOException {
method releaseAfterError (line 80) | public void releaseAfterError(T t) throws IOException {
FILE: src/main/java/com/pinterest/jbender/util/ListReceivePort.java
class ListReceivePort (line 22) | public class ListReceivePort<T> implements ReceivePort<T> {
method ListReceivePort (line 27) | public ListReceivePort(List<T> list) {
method ListReceivePort (line 33) | public ListReceivePort(List<T> list, int total) {
method receive (line 39) | @Override
method receive (line 48) | @Override
method receive (line 53) | @Override
method tryReceive (line 58) | @Override
method close (line 67) | @Override
method isClosed (line 70) | @Override
FILE: src/main/java/com/pinterest/jbender/util/WaitGroup.java
class WaitGroup (line 22) | public class WaitGroup {
method WaitGroup (line 26) | public WaitGroup() {
method add (line 31) | public void add() {
method done (line 35) | public void done() {
method await (line 42) | public void await() throws SuspendExecution {
FILE: src/test/java/com/pinterest/jbender/JBenderTest.java
class JBenderTest (line 30) | public class JBenderTest {
class FakeRequestExecutor (line 31) | private static final class FakeRequestExecutor implements RequestExecu...
method execute (line 32) | @Override
method assertEvents (line 38) | private void assertEvents(Channel<TimingEvent<Integer>> eventCh, int s...
method requests (line 63) | private void requests(Channel<Integer> requestCh, int count) {
method testLoadTestThroughputNoRequests (line 72) | @Test
method testLoadTestThroughputOneRequest (line 84) | @Test
method testLoadTestThroughputTenRequests (line 96) | @Test
method testLoadTestThroughputWarmup (line 108) | @Test
method testLoadTestConcurrencyNoRequests (line 120) | @Test
method testLoadTestConcurrencyOneRequest (line 131) | @Test
method testLoadTestConcurrencyTenRequests (line 142) | @Test
method testLoadTestConcurrencyWarmup (line 153) | @Test
Condensed preview — 43 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (155K chars).
[
{
"path": ".gitignore",
"chars": 125,
"preview": "*.class\n*.jar\n!gradle/wrapper/gradle-wrapper.jar\n*.war\n*.ear\nhs_err_pid*\n.gradle\nbuild/\ngradle-app.setting\n.idea\njbender"
},
{
"path": ".travis.yml",
"chars": 184,
"preview": "language: java\njdk:\n - oraclejdk8\nbefore_cache:\n - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock\ncache:\n direct"
},
{
"path": "LICENSE",
"chars": 11324,
"preview": "Apache License\n Version 2.0, January 2004\n http://www.apache.org/licens"
},
{
"path": "README.md",
"chars": 8042,
"preview": "JBender\n=======\n\n[](https://travis-ci.org/pinterest/jbender)"
},
{
"path": "build.gradle",
"chars": 1992,
"preview": "plugins {\n id 'me.champeau.gradle.jmh' version '0.2.0'\n}\n\napply plugin: 'java'\napply plugin: 'maven'\napply plugin: 'm"
},
{
"path": "doc/http/TUTORIAL.md",
"chars": 13062,
"preview": "JBender HTTP Tutorial\n=====================\n\nThis tutorial walks through the steps to create an HTTP load tester with JB"
},
{
"path": "doc/http/jbender-http-tutorial/build.gradle",
"chars": 2349,
"preview": "// Capsule plugin\nplugins {\n id \"us.kirchmeier.capsule\" version \"1.0-rc1\"\n}\n\napply plugin: 'java'\n\n// Target JDK8\nsou"
},
{
"path": "doc/http/jbender-http-tutorial/gradle/wrapper/gradle-wrapper.properties",
"chars": 230,
"preview": "#Tue May 12 13:10:51 PDT 2015\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
},
{
"path": "doc/http/jbender-http-tutorial/gradlew",
"chars": 5080,
"preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n## Gradle start "
},
{
"path": "doc/http/jbender-http-tutorial/gradlew.bat",
"chars": 2404,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "doc/http/jbender-http-tutorial/src/main/java/LoadTest.java",
"chars": 2888,
"preview": "import java.util.concurrent.ExecutionException;\nimport java.io.IOException;\nimport org.slf4j.Logger;\nimport org.slf4j.Lo"
},
{
"path": "doc/thrift/TUTORIAL.md",
"chars": 15701,
"preview": "JBender Thrift Tutorial\n=======================\n\nThis tutorial walks through the steps to create a simple \"Echo\" Thrift "
},
{
"path": "doc/thrift/jbender-thrift-tutorial/build.gradle",
"chars": 4016,
"preview": "// Gradle Thrift plugin\nbuildscript {\n repositories {\n mavenCentral()\n }\n\n dependencies {\n classp"
},
{
"path": "doc/thrift/jbender-thrift-tutorial/gradle/wrapper/gradle-wrapper.properties",
"chars": 230,
"preview": "#Mon Nov 02 11:45:44 PST 2015\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
},
{
"path": "doc/thrift/jbender-thrift-tutorial/gradlew",
"chars": 5080,
"preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n## Gradle start "
},
{
"path": "doc/thrift/jbender-thrift-tutorial/gradlew.bat",
"chars": 2404,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "doc/thrift/jbender-thrift-tutorial/src/main/java/echo/jbender/Main.java",
"chars": 2904,
"preview": "package echo.jbender;\n\nimport co.paralleluniverse.fibers.SuspendExecution;\nimport co.paralleluniverse.strands.channels.C"
},
{
"path": "doc/thrift/jbender-thrift-tutorial/src/main/java/echo/server/Main.java",
"chars": 1363,
"preview": "package echo.server;\n\nimport co.paralleluniverse.fibers.Suspendable;\nimport com.pinterest.quasar.thrift.TFiberServer;\nim"
},
{
"path": "doc/thrift/jbender-thrift-tutorial/src/main/thrift/echo.thrift",
"chars": 212,
"preview": "namespace java echo.thrift\n\nstruct EchoRequest {\n 1: optional string message;\n}\n\nstruct EchoResponse {\n 2: optiona"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 233,
"preview": "#Tue Aug 02 17:39:27 PDT 2016\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-2.14.1-bin.zip\ndistribut"
},
{
"path": "gradlew",
"chars": 5046,
"preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n## Gradle start "
},
{
"path": "gradlew.bat",
"chars": 2404,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "settings.gradle",
"chars": 30,
"preview": "rootProject.name = 'jbender'\n\n"
},
{
"path": "src/jmh/java/com/pinterest/jbender/JBenderBenchmark.java",
"chars": 2532,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/jmh/java/com/pinterest/jbender/JBenderHttpBenchmark.java",
"chars": 3174,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/JBender.java",
"chars": 21450,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/events/TimingEvent.java",
"chars": 2189,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/events/recording/HdrHistogramRecorder.java",
"chars": 2340,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/events/recording/LoggingRecorder.java",
"chars": 1040,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/events/recording/Recorder.java",
"chars": 2935,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/executors/NoopRequestExecutor.java",
"chars": 881,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/executors/RequestExecutor.java",
"chars": 1095,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/executors/SleepyRequestExecutor.java",
"chars": 1165,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/executors/Validator.java",
"chars": 835,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/executors/http/FiberApacheHttpClientRequestExecutor.java",
"chars": 4430,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/intervals/ConstantIntervalGenerator.java",
"chars": 882,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/intervals/ExponentialIntervalGenerator.java",
"chars": 1120,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/intervals/IntervalGenerator.java",
"chars": 764,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/util/ConnectionPool.java",
"chars": 2277,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/util/ListReceivePort.java",
"chars": 1845,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/main/java/com/pinterest/jbender/util/WaitGroup.java",
"chars": 1333,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
},
{
"path": "src/test/java/com/pinterest/jbender/JBenderTest.java",
"chars": 6147,
"preview": "/*\nCopyright 2014 Pinterest.com\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this fil"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the pinterest/jbender GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 43 files (142.3 KB), approximately 35.8k tokens, and a symbol index with 101 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.