├── .github
├── dco.yml
├── dependabot.yml
├── release-drafter.yml
└── workflows
│ ├── ci.yml
│ ├── release-drafter.yml
│ ├── release.yml
│ └── snapshot.yml
├── .gitignore
├── LICENSE
├── README.md
├── agent
├── build.gradle
└── src
│ ├── jarFileTest
│ └── java
│ │ └── reactor
│ │ └── blockhoud
│ │ ├── AbstractJarFileTest.java
│ │ └── JarFileShadingTest.java
│ └── main
│ └── java
│ └── reactor
│ └── blockhound
│ ├── AllowancesByteBuddyTransformer.java
│ ├── BlockHound.java
│ ├── BlockHoundRuntime.java
│ ├── BlockingCallsByteBuddyTransformer.java
│ ├── BlockingMethod.java
│ ├── BlockingOperationError.java
│ ├── InstrumentationUtils.java
│ ├── NativeWrappingClassFileTransformer.java
│ ├── TestThread.java
│ └── integration
│ ├── BlockHoundIntegration.java
│ ├── LoggingIntegration.java
│ ├── ReactorIntegration.java
│ ├── RxJava2Integration.java
│ └── StandardOutputIntegration.java
├── benchmarks
├── build.gradle
└── src
│ └── jmh
│ └── java
│ └── reactor
│ └── blockhound
│ └── BlockHoundBenchmark.java
├── build.gradle
├── docs
├── README.md
├── custom_integrations.md
├── customization.md
├── how_it_works.md
├── quick_start.md
├── supported_testing_frameworks.md
└── tips.md
├── example
├── build.gradle
└── src
│ └── test
│ ├── java
│ └── com
│ │ └── example
│ │ ├── BlockingDisallowTest.java
│ │ ├── BuilderTest.java
│ │ ├── CustomBlockingMethodTest.java
│ │ ├── DynamicCurrentThreadTest.java
│ │ ├── DynamicThreadsTest.java
│ │ ├── FalseNegativesTest.java
│ │ ├── IntegrationOrderingTest.java
│ │ ├── JDKTest.java
│ │ ├── ReactorTest.java
│ │ ├── RxJavaTest.java
│ │ ├── StackTraceTest.java
│ │ ├── StandardOutputTest.java
│ │ └── StaticInitTest.java
│ └── resources
│ └── META-INF
│ └── services
│ └── reactor.blockhound.integration.BlockHoundIntegration
├── gradle.properties
├── gradle
├── publishing.gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── junit-platform
├── build.gradle
└── src
│ ├── main
│ └── java
│ │ └── reactor
│ │ └── blockhound
│ │ └── junit
│ │ └── platform
│ │ └── BlockHoundTestExecutionListener.java
│ └── test
│ └── java
│ └── reactor
│ └── blockhound
│ └── junit
│ └── platform
│ ├── JUnitPlatformDynamicIntegrationTest.java
│ └── JUnitPlatformIntegrationTest.java
└── settings.gradle
/.github/dco.yml:
--------------------------------------------------------------------------------
1 | require:
2 | members: false
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gradle
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | assignees:
9 | - violetagg
10 | labels:
11 | - type/dependency-upgrade
12 | ignore:
13 | - dependency-name: io.projectreactor:reactor-core
14 | versions:
15 | - "> 3.2.5.RELEASE"
16 | - dependency-name: io.reactivex.rxjava2:rxjava
17 | versions:
18 | - "> 2.2.18, < 2.3"
19 | - dependency-name: me.champeau.gradle.jmh
20 | versions:
21 | - ">= 0.5.a, < 0.6"
22 | - dependency-name: org.junit.platform:junit-platform-launcher
23 | versions:
24 | - "> 1.0.0"
25 | - dependency-name: io.projectreactor.tools:blockhound
26 | versions:
27 | - 1.0.5.RELEASE
28 | - dependency-name: net.bytebuddy:byte-buddy-agent
29 | versions:
30 | - 1.10.20
31 | - dependency-name: net.bytebuddy:byte-buddy
32 | versions:
33 | - 1.10.20
34 | rebase-strategy: disabled
35 | - package-ecosystem: github-actions
36 | directory: "/"
37 | schedule:
38 | interval: daily
39 | open-pull-requests-limit: 10
40 | assignees:
41 | - violetagg
42 | labels:
43 | - type/dependency-upgrade
44 | rebase-strategy: disabled
45 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: $NEXT_PATCH_VERSION.RELEASE
2 | tag-template: $NEXT_PATCH_VERSION.RELEASE
3 | template: |
4 | # What's Changed
5 |
6 | $CHANGES
7 | categories:
8 | - title: 🚀 Features / Enhancements
9 | label: type/enhancement
10 | - title: 🐛 Bug Fixes
11 | label: type/bug
12 | - title: 📖 Documentation
13 | label: type/documentation
14 | - title: 🧹 Housekeeping
15 | label: type/chore
16 | - title: 📦 Dependency updates
17 | label: type/dependency-upgrade
18 |
19 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-22.04
8 |
9 | steps:
10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
11 | - name: Set up JDK
12 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
13 | with:
14 | java-version: '13'
15 | distribution: 'adopt'
16 | - name: Run tests and javadoc
17 | run: ./gradlew check javadoc
18 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | update_release_draft:
10 | runs-on: ubuntu-22.04
11 | steps:
12 | - uses: release-drafter/release-drafter@v6
13 | env:
14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
15 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release: #note: fortunately this event is not triggered for draft releases, as `released` could include drafts
5 | types: [ prereleased, released ]
6 |
7 | jobs:
8 | publishRelease:
9 | runs-on: ubuntu-22.04
10 | environment: release
11 | steps:
12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
13 | - name: Set up JDK
14 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
15 | with:
16 | java-version: '8'
17 | distribution: 'temurin'
18 | - name: Publish Release
19 | if: endsWith(github.event.release.tag_name, '.RELEASE')
20 | run: ./gradlew --no-daemon -Pversion="${{github.event.release.tag_name}}" sign publish
21 | env:
22 | GRADLE_PUBLISH_REPO_URL: https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/
23 | GRADLE_PUBLISH_MAVEN_USER: ${{secrets.MAVEN_USER}}
24 | GRADLE_PUBLISH_MAVEN_PASSWORD: ${{secrets.MAVEN_PASSWORD}}
25 | GRADLE_SIGNING_KEY: ${{secrets.SIGNING_KEY}}
26 | GRADLE_SIGNING_PASSWORD: ${{secrets.SIGNING_PASSPHRASE}}
27 | - name: Publish Milestone
28 | if: github.event.release.prerelease && (contains(github.event.release.tag_name, '.M') || contains(github.event.release.tag_name, '.RC'))
29 | run: ./gradlew --no-daemon -Pversion="${{github.event.release.tag_name}}" publish
30 | env:
31 | GRADLE_PUBLISH_REPO_URL: https://repo.spring.io/libs-milestone-local/
32 | GRADLE_PUBLISH_MAVEN_USER: ${{secrets.ARTIFACTORY_USERNAME}}
33 | GRADLE_PUBLISH_MAVEN_PASSWORD: ${{secrets.ARTIFACTORY_PASSWORD}}
34 | - name: Stage the release
35 | if: endsWith(github.event.release.tag_name, '.RELEASE')
36 | env:
37 | GRADLE_PUBLISH_MAVEN_USER: ${{secrets.MAVEN_USER}}
38 | GRADLE_PUBLISH_MAVEN_PASSWORD: ${{secrets.MAVEN_PASSWORD}}
39 | run: |
40 | GRADLE_PUBLISH_MAVEN_AUTHORIZATION=$(echo "${GRADLE_PUBLISH_MAVEN_USER}:${GRADLE_PUBLISH_MAVEN_PASSWORD}" | base64)
41 | REPOSITORY_RESPONSE=$(curl -s -X GET \
42 | -H "Authorization: Bearer ${GRADLE_PUBLISH_MAVEN_AUTHORIZATION}" \
43 | "https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?state=open")
44 | REPOSITORY_KEY=$(echo "${REPOSITORY_RESPONSE}" | grep -o '"key":"[^"]*"' | head -1 | cut -d':' -f2 | tr -d '"')
45 | curl -s -X POST \
46 | -H "Authorization: Bearer ${GRADLE_PUBLISH_MAVEN_AUTHORIZATION}" \
47 | "https://ossrh-staging-api.central.sonatype.com/manual/upload/repository/${REPOSITORY_KEY}?publishing_type=user_managed"
48 |
--------------------------------------------------------------------------------
/.github/workflows/snapshot.yml:
--------------------------------------------------------------------------------
1 | name: Snapshot
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | checkSnapshot:
10 | runs-on: ubuntu-22.04
11 | steps:
12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
13 | - name: Set up JDK
14 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
15 | with:
16 | java-version: '13' # Modern JVM is needed for Java 10+ specific tests
17 | distribution: 'adopt'
18 | - name: Run tests and javadoc
19 | run: ./gradlew check javadoc
20 | publishSnapshot:
21 | runs-on: ubuntu-22.04
22 | needs: checkSnapshot
23 | environment: snapshot
24 | env: #change this after a release
25 | BLOCKHOUND_VERSION: 1.0.14.BUILD-SNAPSHOT
26 | steps:
27 | - name: check version
28 | if: ${{ !endsWith(env.BLOCKHOUND_VERSION, '.BUILD-SNAPSHOT') }}
29 | run: |
30 | echo "::error ::$BLOCKHOUND_VERSION is not following the x.y.z.BUILD-SNAPSHOT format"
31 | exit 1
32 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
33 | - name: Set up JDK
34 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
35 | with:
36 | java-version: '8'
37 | distribution: 'temurin'
38 | - name: Publish Snapshot
39 | run: ./gradlew --no-daemon -Pversion=$BLOCKHOUND_VERSION publish
40 | env:
41 | GRADLE_PUBLISH_REPO_URL: https://repo.spring.io/libs-snapshot-local/
42 | GRADLE_PUBLISH_MAVEN_USER: ${{secrets.ARTIFACTORY_USERNAME}}
43 | GRADLE_PUBLISH_MAVEN_PASSWORD: ${{secrets.ARTIFACTORY_PASSWORD}}
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.gradle/
2 | build/
3 |
4 | .idea/
5 | *.iml
6 | *.ipr
7 | *.iws
8 |
9 | .DS_Store
10 |
11 | .vscode/
12 |
13 | cmake-build-*/
14 |
15 | out/
16 |
17 | .settings/
18 | .project
19 | .classpath
20 | bin/
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | https://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | https://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BlockHound
2 |
3 | [](https://repo1.maven.org/maven2/io/projectreactor/tools/blockhound/)
4 | [](https://repo.spring.io/milestone/io/projectreactor/tools/blockhound/)
5 | [](https://repo.spring.io/snapshot/io/projectreactor/tools/blockhound/)
6 | [](https://gitter.im/reactor/BlockHound)
7 |
8 | Java agent to detect blocking calls from non-blocking threads.
9 |
10 | ## How it works
11 | BlockHound will transparently instrument the JVM classes and intercept blocking calls (e.g. IO) if they are performed from threads marked as "non-blocking operations only" (ie. threads implementing Reactor's `NonBlocking` marker interface, like those started by `Schedulers.parallel()`). If and when this happens (but remember, this should never happen! :stuck_out_tongue_winking_eye:), an error will be thrown. Here is an example:
12 | ```java
13 | // Example.java
14 | BlockHound.install();
15 |
16 | Mono.delay(Duration.ofSeconds(1))
17 | .doOnNext(it -> {
18 | try {
19 | Thread.sleep(10);
20 | }
21 | catch (InterruptedException e) {
22 | throw new RuntimeException(e);
23 | }
24 | })
25 | .block();
26 | ```
27 |
28 | Will result in:
29 | ```
30 | reactor.blockhound.BlockingOperationError: Blocking call! java.lang.Thread.sleep
31 | at java.base/java.lang.Thread.sleep(Native Method)
32 | at com.example.Example.lambda$exampleTest$0(Example.java:16)
33 | ```
34 | Note that it points to the exact place where the blocking call got triggered. In this example it was `Example.java:16`.
35 |
36 | ## Getting it
37 |
38 | Download it from Maven Central repositories (stable releases only) or repo.spring.io:
39 |
40 | _Gradle_
41 |
42 | ```groovy
43 | repositories {
44 | mavenCentral()
45 | // maven { url 'https://repo.spring.io/milestone' }
46 | // maven { url 'https://repo.spring.io/snapshot' }
47 | }
48 |
49 | dependencies {
50 | testImplementation 'io.projectreactor.tools:blockhound:$LATEST_RELEASE'
51 | // testImplementation 'io.projectreactor.tools:blockhound:$LATEST_MILESTONE'
52 | // testImplementation 'io.projectreactor.tools:blockhound:$LATEST_SNAPSHOT'
53 | }
54 | ```
55 |
56 |
57 | with Kotlin DSL
58 |
59 | ```kotlin
60 | repositories {
61 | mavenCentral()
62 | // maven("https://repo.spring.io/milestone")
63 | // maven("https://repo.spring.io/snapshot")
64 | }
65 |
66 | dependencies {
67 | testImplementation("io.projectreactor.tools:blockhound:$LATEST_RELEASE")
68 | // testImplementation("io.projectreactor.tools:blockhound:$LATEST_MILESTONE")
69 | // testImplementation("io.projectreactor.tools:blockhound:$LATEST_SNAPSHOT")
70 | }
71 | ```
72 |
73 |
74 | _Maven_
75 |
76 | ```xml
77 |
78 |
79 | io.projectreactor.tools
80 | blockhound
81 | $LATEST_RELEASE
82 |
83 |
84 | ```
85 |
86 | Where:
87 |
88 | |||
89 | |-|-|
90 | |`$LATEST_RELEASE`|[](https://repo1.maven.org/maven2/io/projectreactor/tools/blockhound/)|
91 | |`$LATEST_MILESTONE`|[](https://repo.spring.io/milestone/io/projectreactor/tools/blockhound/)|
92 | |`$LATEST_SNAPSHOT`|[](https://repo.spring.io/snapshot/io/projectreactor/tools/blockhound/)|
93 |
94 | ## JDK13+ support
95 |
96 | for JDK 13+, it is no longer allowed redefining native methods. So for the moment, as a temporary work around, please use the
97 | `-XX:+AllowRedefinitionToAddDeleteMethods` jvm argument:
98 |
99 | _Maven_
100 |
101 | ```xml
102 |
103 | org.apache.maven.plugins
104 | maven-surefire-plugin
105 | 2.22.2
106 |
107 | -XX:+AllowRedefinitionToAddDeleteMethods
108 |
109 |
110 | ```
111 |
112 | _Gradle_
113 |
114 | ```groovy
115 | tasks.withType(Test).all {
116 | if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) {
117 | jvmArgs += [
118 | "-XX:+AllowRedefinitionToAddDeleteMethods"
119 | ]
120 | }
121 | }
122 | ```
123 |
124 |
125 | with Kotlin DSL
126 |
127 | ```kotlin
128 | tasks.withType().all {
129 | if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) {
130 | jvmArgs("-XX:+AllowRedefinitionToAddDeleteMethods")
131 | }
132 | }
133 | ```
134 |
135 |
136 | ## Built-in integrations
137 | Although BlockHound supports [the SPI mechanism to integrate with](https://github.com/reactor/BlockHound/blob/master/docs/custom_integrations.md), it comes with a few built-in integrations:
138 | 1. [Project Reactor](https://projectreactor.io)
139 | Version 3.2.x is supported out of the box.
140 | Starting with `reactor-core` version 3.3.0, there is [a built-in integration in Reactor itself](https://github.com/reactor/reactor-core/blob/v3.3.0.RELEASE/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java) that uses [the SPI](https://github.com/reactor/BlockHound/blob/master/docs/custom_integrations.md).
141 | 2. [RxJava 2](https://github.com/ReactiveX/RxJava/) is supported.
142 | RxJava 3 and further versions of RxJava will require an SPI to be implemented, either by the framework or user. See [this PR to RxJava](https://github.com/ReactiveX/RxJava/pull/6692) with an example of the SPI's implementation.
143 |
144 | # Quick Start
145 | See [the docs](./docs/README.md).
146 |
147 | -------------------------------------
148 | _Licensed under [Apache Software License 2.0](www.apache.org/licenses/LICENSE-2.0)_
149 |
150 | _Sponsored by [Pivotal](https://pivotal.io)_
151 |
--------------------------------------------------------------------------------
/agent/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.github.johnrengelman.shadow' version '7.1.2'
3 | id "java"
4 | id "maven-publish"
5 | id "signing"
6 | id "org.unbroken-dome.test-sets" version "4.0.0"
7 | id 'me.champeau.gradle.japicmp' version '0.4.6' apply false
8 | id 'de.undercouch.download' version '5.6.0' apply false
9 | }
10 |
11 | import me.champeau.gradle.japicmp.JapicmpTask
12 | apply plugin: 'me.champeau.gradle.japicmp'
13 | apply plugin: 'de.undercouch.download'
14 |
15 | description = "BlockHound Java Agent"
16 | ext.detailedDescription = "Java agent to detect blocking calls from non-blocking threads."
17 |
18 | testSets {
19 | jarFileTest
20 | }
21 |
22 | sourceCompatibility = targetCompatibility = 8
23 |
24 | shadowJar {
25 | classifier = null
26 |
27 | manifest {
28 | attributes('Can-Retransform-Classes': 'true')
29 | attributes('Can-Set-Native-Method-Prefix': 'true')
30 | attributes('Can-Redefine-Classes': 'true')
31 | attributes('Automatic-Module-Name': 'reactor.blockhound')
32 | attributes('Premain-Class': 'reactor.blockhound.BlockHound')
33 | }
34 |
35 | exclude 'META-INF/versions/**'
36 | exclude 'META-INF/LICENSE'
37 | exclude 'META-INF/NOTICE'
38 | exclude 'META-INF/licenses/'
39 | exclude 'META-INF/maven/**'
40 | exclude 'reactor/shaded/META-INF/**'
41 |
42 | // TODO discuss with ByteBuddy folks how to shade it
43 | exclude 'win32-x86*/**'
44 | }
45 |
46 | project.tasks.build.dependsOn(shadowJar)
47 |
48 | task relocateShadowJar(type: com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation) {
49 | target = tasks.shadowJar
50 | prefix = "reactor.blockhound.shaded"
51 | }
52 |
53 | tasks.shadowJar.dependsOn tasks.relocateShadowJar
54 |
55 | project.tasks.jarFileTest.configure {
56 | systemProperty("jarFile", shadowJar.outputs.files.singleFile)
57 | dependsOn(shadowJar)
58 | }
59 | tasks.check.dependsOn tasks.jarFileTest
60 |
61 | dependencies {
62 | compileOnly 'com.google.auto.service:auto-service-annotations:1.1.1'
63 | annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
64 |
65 | implementation 'net.bytebuddy:byte-buddy:1.17.5'
66 | implementation 'net.bytebuddy:byte-buddy-agent:1.17.5'
67 |
68 | compileOnly 'io.projectreactor:reactor-core:3.2.5.RELEASE'
69 | compileOnly 'io.reactivex.rxjava2:rxjava:2.2.18'
70 |
71 | jarFileTestImplementation 'org.assertj:assertj-core:3.27.3'
72 | jarFileTestImplementation 'junit:junit:4.13.2'
73 | }
74 |
75 | task sourcesJar(type: Jar) {
76 | archiveClassifier.set('sources')
77 | from sourceSets.main.allJava
78 | }
79 |
80 | task javadocJar(type: Jar) {
81 | from javadoc
82 | archiveClassifier.set('javadoc')
83 | }
84 |
85 | publishing {
86 | publications {
87 | mavenJava(MavenPublication) { publication ->
88 | artifacts.removeAll { it.classifier == null }
89 | artifact project.tasks.shadowJar
90 | artifact sourcesJar
91 | artifact javadocJar
92 |
93 | artifactId 'blockhound'
94 | }
95 | }
96 | }
97 |
98 | task downloadBaseline(type: Download) {
99 | onlyIf {
100 | if (project.gradle.startParameter.isOffline()) {
101 | println "Offline: skipping downloading of baseline and JAPICMP"
102 | return false
103 | }
104 | else if ("$compatibleVersion" == "SKIP") {
105 | println "SKIP: Instructed to skip the baseline comparison"
106 | return false
107 | }
108 | else {
109 | println "Will download and perform baseline comparison with ${compatibleVersion}"
110 | return true
111 | }
112 | }
113 |
114 | onlyIfNewer true
115 | compress true
116 | src "${repositories.mavenCentral().url}io/projectreactor/tools/blockhound/$compatibleVersion/blockhound-${compatibleVersion}.jar"
117 | dest "${buildDir}/baselineLibs/blockhound-${compatibleVersion}.jar"
118 | }
119 |
120 | def japicmpReport = tasks.register('japicmpReport') {
121 | onlyIf {
122 | japicmp.state.failure != null
123 | }
124 | doLast {
125 | def reportFile = file("${project.buildDir}/reports/japi.txt")
126 | if (reportFile.exists()) {
127 | println "\n **********************************"
128 | println " * /!\\ API compatibility failures *"
129 | println " **********************************"
130 | println "Japicmp report was filtered and interpreted to find the following incompatibilities:"
131 | reportFile.eachLine {
132 | if (it.contains("*") && (!it.contains("***") || it.contains("****")))
133 | println "source incompatible change: $it"
134 | else if (it.contains("!"))
135 | println "binary incompatible change: $it"
136 | }
137 | }
138 | else println "No incompatible change to report"
139 | }
140 | }
141 |
142 | task japicmp(type: JapicmpTask) {
143 | finalizedBy(japicmpReport)
144 | dependsOn(shadowJar)
145 | onlyIf { "$compatibleVersion" != "SKIP" }
146 |
147 | oldClasspath.from(files("${buildDir}/baselineLibs/blockhound-${compatibleVersion}.jar"))
148 | newClasspath.from(files(jar.archiveFile))
149 | // these onlyXxx parameters result in a report that is slightly too noisy, but better than
150 | // onlyBinaryIncompatibleModified = true which masks source-incompatible-only changes
151 | onlyBinaryIncompatibleModified = false
152 | onlyModified = true
153 | failOnModification = true
154 | failOnSourceIncompatibility = true
155 | txtOutputFile = file("${project.buildDir}/reports/japi.txt")
156 | ignoreMissingClasses = true
157 | includeSynthetic = true
158 |
159 | compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ]
160 |
161 | packageExcludes = [
162 | // Always ignore shaded packages
163 | 'reactor.blockhound.shaded.*'
164 | ]
165 |
166 | classExcludes = [
167 | // Ignores this transformer which is used internally
168 | 'reactor.blockhound.AllowancesByteBuddyTransformer$AllowedArgument$Factory',
169 | // Ignores this transformer which is used internally
170 | 'reactor.blockhound.BlockingCallsByteBuddyTransformer$ModifiersArgument$Factory'
171 | ]
172 |
173 | methodExcludes = [
174 | ]
175 | }
176 |
177 | tasks.japicmp.dependsOn(downloadBaseline)
178 |
179 | tasks.check.dependsOn(japicmp)
180 |
--------------------------------------------------------------------------------
/agent/src/jarFileTest/java/reactor/blockhoud/AbstractJarFileTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2019-Present Pivotal Software Inc, All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package reactor.blockhoud;
18 |
19 | import java.net.URI;
20 | import java.nio.file.FileSystem;
21 | import java.nio.file.FileSystems;
22 | import java.nio.file.Path;
23 | import java.nio.file.Paths;
24 |
25 | import static java.util.Collections.emptyMap;
26 |
27 | /**
28 | * A helper class to access the content of a shaded JAR
29 | */
30 | class AbstractJarFileTest {
31 |
32 | static Path root;
33 |
34 | static {
35 | try {
36 | Path jarFilePath = Paths.get(System.getProperty("jarFile"));
37 | URI jarFileUri = new URI("jar", jarFilePath.toUri().toString(), null);
38 | FileSystem fileSystem = FileSystems.newFileSystem(jarFileUri, emptyMap());
39 | root = fileSystem.getPath("/");
40 | }
41 | catch (Exception e) {
42 | throw new RuntimeException(e);
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/agent/src/jarFileTest/java/reactor/blockhoud/JarFileShadingTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2019-Present Pivotal Software Inc, All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package reactor.blockhoud;
18 |
19 | import java.io.IOException;
20 | import java.net.MalformedURLException;
21 | import java.nio.file.Files;
22 | import java.nio.file.Path;
23 |
24 | import org.assertj.core.api.ListAssert;
25 | import org.junit.Test;
26 |
27 | import static org.assertj.core.api.Assertions.assertThat;
28 | import static org.assertj.core.api.Assertions.linesOf;
29 |
30 | /**
31 | * This test must be executed with Gradle because it requires a shadow JAR
32 | */
33 | public class JarFileShadingTest extends AbstractJarFileTest {
34 |
35 | @Test
36 | public void testPackages() throws Exception {
37 | assertThatFileList(root).containsOnly(
38 | "reactor",
39 | "META-INF"
40 | );
41 |
42 | assertThatFileList(root.resolve("reactor")).containsOnly(
43 | "blockhound"
44 | );
45 | }
46 |
47 | @Test
48 | public void testMetaInf() throws Exception {
49 | assertThatFileList(root.resolve("META-INF")).containsOnly(
50 | "MANIFEST.MF",
51 | "services"
52 | );
53 | assertThatFileList(root.resolve("META-INF").resolve("services")).containsOnly(
54 | "reactor.blockhound.integration.BlockHoundIntegration"
55 | );
56 | }
57 |
58 | @Test
59 | public void testManifest() throws MalformedURLException {
60 | assertThat(linesOf(root.resolve("META-INF/MANIFEST.MF").toUri().toURL()))
61 | .anyMatch(s -> s.startsWith("Premain-Class: reactor"));
62 | }
63 |
64 | @SuppressWarnings("unchecked")
65 | private ListAssert assertThatFileList(Path path) throws IOException {
66 | return (ListAssert) assertThat(Files.list(path))
67 | .extracting(Path::getFileName)
68 | .extracting(Path::toString)
69 | .extracting(it -> it.endsWith("/") ? it.substring(0, it.length() - 1) : it);
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/agent/src/main/java/reactor/blockhound/AllowancesByteBuddyTransformer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2019-Present Pivotal Software Inc, All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package reactor.blockhound;
18 |
19 | import net.bytebuddy.agent.builder.AgentBuilder;
20 | import net.bytebuddy.asm.Advice;
21 | import net.bytebuddy.asm.AsmVisitorWrapper;
22 | import net.bytebuddy.description.annotation.AnnotationDescription;
23 | import net.bytebuddy.description.method.ParameterDescription;
24 | import net.bytebuddy.description.type.TypeDescription;
25 | import net.bytebuddy.dynamic.DynamicType;
26 | import net.bytebuddy.utility.JavaModule;
27 |
28 | import java.lang.annotation.Documented;
29 | import java.lang.annotation.ElementType;
30 | import java.lang.annotation.Retention;
31 | import java.lang.annotation.RetentionPolicy;
32 | import java.security.ProtectionDomain;
33 | import java.util.Map;
34 |
35 | /**
36 | * This transformer applies {@link AllowAdvice} to every method
37 | * registered with {@link BlockHound.Builder#allowBlockingCallsInside(String, String)}.
38 | */
39 | class AllowancesByteBuddyTransformer implements AgentBuilder.Transformer {
40 |
41 | private Map> allowances;
42 |
43 | AllowancesByteBuddyTransformer(Map> allowances) {
44 | this.allowances = allowances;
45 | }
46 |
47 | @Override
48 | public DynamicType.Builder> transform(
49 | DynamicType.Builder> builder,
50 | TypeDescription typeDescription,
51 | ClassLoader classLoader,
52 | JavaModule module,
53 | ProtectionDomain protectionDomain
54 | ) {
55 | Map methods = allowances.get(typeDescription.getName());
56 |
57 | if (methods == null) {
58 | return builder;
59 | }
60 |
61 | AsmVisitorWrapper advice = Advice
62 | .withCustomMapping()
63 | .bind(new AllowedArgument.Factory(methods))
64 | .to(AllowAdvice.class)
65 | .on(method -> methods.containsKey(method.getInternalName()));
66 |
67 | return builder.visit(advice);
68 | }
69 |
70 | @Documented
71 | @Retention(RetentionPolicy.RUNTIME)
72 | @java.lang.annotation.Target(ElementType.PARAMETER)
73 | @interface AllowedArgument {
74 |
75 | /**
76 | * Binds advice method's argument annotated with {@link AllowedArgument}
77 | * to boolean where `true` means "allowed" and `false" means "disallowed"
78 | */
79 | class Factory implements Advice.OffsetMapping.Factory {
80 |
81 | final Map methods;
82 |
83 | Factory(Map methods) {
84 | this.methods = methods;
85 | }
86 |
87 | @Override
88 | public Class getAnnotationType() {
89 | return AllowedArgument.class;
90 | }
91 |
92 | @Override
93 | public Advice.OffsetMapping make(
94 | ParameterDescription.InDefinedShape target,
95 | AnnotationDescription.Loadable annotation,
96 | AdviceType adviceType
97 | ) {
98 | return (instrumentedType, instrumentedMethod, assigner, argumentHandler, sort) -> {
99 | boolean allowed = methods.get(instrumentedMethod.getInternalName());
100 | return Advice.OffsetMapping.Target.ForStackManipulation.of(allowed);
101 | };
102 | }
103 | }
104 | }
105 |
106 | static class AllowAdvice {
107 |
108 | @Advice.OnMethodEnter
109 | static BlockHoundRuntime.State onEnter(
110 | @AllowancesByteBuddyTransformer.AllowedArgument boolean allowed
111 | ) {
112 | BlockHoundRuntime.State previous = BlockHoundRuntime.STATE.get();
113 | if (previous == null) {
114 | return null;
115 | }
116 |
117 | if (previous.isAllowed() == allowed) {
118 | // if we won't change the flag, return `null` and skip the `onExit` part
119 | return null;
120 | }
121 |
122 | // Otherwise, set to `allowed` and reset to `!allowed` in `onExit`
123 | previous.setAllowed(allowed);
124 | return previous;
125 | }
126 |
127 | @Advice.OnMethodExit(onThrowable = Throwable.class)
128 | static void onExit(
129 | @Advice.Enter BlockHoundRuntime.State previousState,
130 | @AllowancesByteBuddyTransformer.AllowedArgument boolean allowed
131 | ) {
132 | if (previousState != null) {
133 | previousState.setAllowed(!allowed);
134 | }
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/agent/src/main/java/reactor/blockhound/BlockHound.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018-2019 Pivotal Software Inc, All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package reactor.blockhound;
18 |
19 | import net.bytebuddy.agent.ByteBuddyAgent;
20 | import net.bytebuddy.agent.builder.AgentBuilder;
21 | import net.bytebuddy.agent.builder.AgentBuilder.DescriptionStrategy;
22 | import net.bytebuddy.agent.builder.AgentBuilder.InitializationStrategy;
23 | import net.bytebuddy.agent.builder.AgentBuilder.PoolStrategy;
24 | import net.bytebuddy.agent.builder.AgentBuilder.RedefinitionStrategy;
25 | import net.bytebuddy.agent.builder.AgentBuilder.RedefinitionStrategy.DiscoveryStrategy;
26 | import net.bytebuddy.agent.builder.AgentBuilder.TypeStrategy;
27 | import net.bytebuddy.asm.Advice;
28 | import net.bytebuddy.dynamic.ClassFileLocator;
29 | import net.bytebuddy.matcher.ElementMatchers;
30 | import net.bytebuddy.pool.TypePool;
31 | import net.bytebuddy.pool.TypePool.CacheProvider;
32 | import reactor.blockhound.integration.BlockHoundIntegration;
33 |
34 | import java.lang.instrument.ClassFileTransformer;
35 | import java.lang.instrument.Instrumentation;
36 | import java.util.*;
37 | import java.util.concurrent.ConcurrentHashMap;
38 | import java.util.concurrent.atomic.AtomicBoolean;
39 | import java.util.function.Consumer;
40 | import java.util.function.Function;
41 | import java.util.function.Predicate;
42 | import java.util.stream.Stream;
43 | import java.util.stream.StreamSupport;
44 |
45 | import static java.util.Collections.singleton;
46 | import static reactor.blockhound.NativeWrappingClassFileTransformer.BLOCK_HOUND_RUNTIME_TYPE;
47 |
48 | /**
49 | * BlockHound is a tool to detect blocking calls from non-blocking threads.
50 | *
51 | * To use it, you need to "install" it first with either {@link BlockHound#install(BlockHoundIntegration...)}
52 | * or {@link BlockHound#builder()}.
53 | *
54 | * On installation, it will run the instrumentation and add the check to the blocking methods.
55 | *
56 | * Note that the installation can (and should) only be done once, subsequent install calls will be ignored.
57 | * Hence, the best place to put the install call is before all tests
58 | * or in the beginning of your "public static void main" method.
59 | *
60 | * If you have it automatically installed (e.g. via a testing framework integration), you can apply the customizations
61 | * by using the SPI mechanism (see {@link BlockHoundIntegration}).
62 | */
63 | public class BlockHound {
64 |
65 | static final String PREFIX = "$$BlockHound$$_";
66 |
67 | private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
68 |
69 | /**
70 | * Creates a completely new {@link BlockHound.Builder} that *does not* have any integration applied.
71 | * Use it only if you want to ignore the built-in SPI mechanism (see {@link #install(BlockHoundIntegration...)}).
72 | *
73 | * @see BlockHound#install(BlockHoundIntegration...)
74 | * @return a fresh {@link BlockHound.Builder}
75 | */
76 | public static Builder builder() {
77 | return new Builder();
78 | }
79 |
80 | /**
81 | * Loads integrations with {@link ServiceLoader}, adds provided integrations,
82 | * and installs the BlockHound instrumentation.
83 | * If you don't want to load the integrations, use {@link #builder()} method.
84 | *
85 | * @param integrations an array of integrations to automatically apply on the intermediate builder
86 | * @see BlockHound#builder()
87 | */
88 | public static void install(BlockHoundIntegration... integrations) {
89 | builder()
90 | .loadIntegrations(integrations)
91 | .install();
92 | }
93 |
94 | /**
95 | * Entrypoint for installation via the {@code -javaagent=} command-line option.
96 | *
97 | * @param agentArgs Options for the agent.
98 | * @param inst Instrumentation API.
99 | *
100 | * @see java.lang.instrument
101 | */
102 | public static void premain(String agentArgs, Instrumentation inst) {
103 | builder()
104 | .loadIntegrations()
105 | .with(inst)
106 | .install();
107 | }
108 |
109 | private BlockHound() {
110 |
111 | }
112 |
113 | private static final class BlockHoundPoolStrategy implements PoolStrategy {
114 |
115 | public static final PoolStrategy INSTANCE = new BlockHoundPoolStrategy();
116 |
117 | private BlockHoundPoolStrategy() { }
118 |
119 | @Override
120 | public TypePool typePool(ClassFileLocator classFileLocator, ClassLoader classLoader, String name) {
121 | return typePool(classFileLocator, classLoader);
122 | }
123 |
124 | @Override
125 | public TypePool typePool(ClassFileLocator classFileLocator, ClassLoader classLoader) {
126 | return new TypePool.Default(
127 | new CacheProvider.Simple(),
128 | classFileLocator,
129 | TypePool.Default.ReaderMode.FAST
130 | );
131 | }
132 | }
133 |
134 | public static class Builder {
135 |
136 | private final Map>> blockingMethods = new HashMap>>() {{
137 | put("java/lang/Object", new HashMap>() {{
138 | put("wait", singleton("(J)V"));
139 | }});
140 |
141 | put("java/io/RandomAccessFile", new HashMap>() {{
142 | put("read0", singleton("()I"));
143 | put("readBytes", singleton("([BII)I"));
144 | put("write0", singleton("(I)V"));
145 | put("writeBytes", singleton("([BII)V"));
146 | }});
147 |
148 | put("java/net/Socket", new HashMap>() {{
149 | put("connect", singleton("(Ljava/net/SocketAddress;)V"));
150 | }});
151 |
152 | put("java/net/DatagramSocket", new HashMap>() {{
153 | put("connect", singleton("(Ljava/net/InetAddress;I)V"));
154 | }});
155 |
156 | put("java/net/PlainDatagramSocketImpl", new HashMap>() {{
157 | put("connect0", singleton("(Ljava/net/InetAddress;I)V"));
158 | put("peekData", singleton("(Ljava/net/DatagramPacket;)I"));
159 | put("send", singleton("(Ljava/net/DatagramPacket;)V"));
160 | put("send0", singleton("(Ljava/net/DatagramPacket;)V"));
161 | }});
162 |
163 | put("java/net/PlainSocketImpl", new HashMap>() {{
164 | put("socketAccept", singleton("(Ljava/net/SocketImpl;)V"));
165 | }});
166 |
167 | put("java/net/ServerSocket", new HashMap>() {{
168 | put("implAccept", singleton("(Ljava/net/Socket;)V"));
169 | }});
170 |
171 | put("java/net/SocketInputStream", new HashMap>() {{
172 | put("socketRead0", singleton("(Ljava/io/FileDescriptor;[BIII)I"));
173 | }});
174 |
175 | put("java/net/Socket$SocketInputStream", new HashMap>() {{
176 | put("read", singleton("([BII)I"));
177 | }});
178 |
179 | put("java/net/SocketOutputStream", new HashMap>() {{
180 | put("socketWrite0", singleton("(Ljava/io/FileDescriptor;[BII)V"));
181 | }});
182 |
183 | put("java/net/Socket$SocketOutputStream", new HashMap>() {{
184 | put("write", singleton("([BII)V"));
185 | }});
186 |
187 | put("java/io/FileInputStream", new HashMap>() {{
188 | put("read0", singleton("()I"));
189 | put("readBytes", singleton("([BII)I"));
190 | }});
191 |
192 | put("java/io/FileOutputStream", new HashMap>() {{
193 | put("write", singleton("(IZ)V"));
194 | put("writeBytes", singleton("([BIIZ)V"));
195 | }});
196 |
197 | if (InstrumentationUtils.jdkMajorVersion >= 9) {
198 | put("jdk/internal/misc/Unsafe", new HashMap>() {{
199 | put("park", singleton("(ZJ)V"));
200 | }});
201 | put("java/lang/ProcessImpl", new HashMap>() {{
202 | put("forkAndExec", singleton("(I[B[B[BI[BI[B[IZ)I"));
203 | }});
204 | }
205 | else {
206 | put("sun/misc/Unsafe", new HashMap>() {{
207 | put("park", singleton("(ZJ)V"));
208 | }});
209 | put("java/lang/UNIXProcess", new HashMap>() {{
210 | put("forkAndExec", singleton("(I[B[B[BI[BI[B[IZ)I"));
211 | }});
212 | }
213 |
214 | if (InstrumentationUtils.jdkMajorVersion < 19) {
215 | // for jdk version < 19, the native method for Thread.sleep is "sleep"
216 | put("java/lang/Thread", new HashMap>() {{
217 | put("sleep", singleton("(J)V"));
218 | put("yield", singleton("()V"));
219 | put("onSpinWait", singleton("()V"));
220 | }});
221 | }
222 | else if (InstrumentationUtils.jdkMajorVersion >= 19 && InstrumentationUtils.jdkMajorVersion <= 21) {
223 | // for jdk version in the range [19, 21], the native method for Thread.sleep is "sleep0"
224 | put("java/lang/Thread", new HashMap>() {{
225 | put("sleep0", singleton("(J)V"));
226 | put("yield0", singleton("()V"));
227 | put("onSpinWait", singleton("()V"));
228 | }});
229 | }
230 | else {
231 | // for jdk version >= 22, the native method for Thread.sleep is "sleepNanos0"
232 | put("java/lang/Thread", new HashMap>() {{
233 | put("sleepNanos0", singleton("(J)V"));
234 | put("yield0", singleton("()V"));
235 | put("onSpinWait", singleton("()V"));
236 | }});
237 | }
238 | }};
239 |
240 | private final Map> allowances = new HashMap>() {{
241 | put(ClassLoader.class.getName(), new HashMap() {{
242 | put("loadClass", true);
243 | }});
244 | put(Throwable.class.getName(), new HashMap() {{
245 | put("printStackTrace", true);
246 | }});
247 |
248 | put(ConcurrentHashMap.class.getName(), new HashMap() {{
249 | put("initTable", true);
250 | }});
251 |
252 | put(Advice.class.getName(), new HashMap() {{
253 | put("to", true);
254 | }});
255 | }};
256 |
257 | private Consumer onBlockingMethod = method -> {
258 | Error error = new BlockingOperationError(method);
259 |
260 | // Strip BlockHound's internal noisy frames from the stacktrace to not mislead the users
261 | StackTraceElement[] stackTrace = error.getStackTrace();
262 | int length = stackTrace.length;
263 | for (int i = 0; i < length; i++) {
264 | StackTraceElement stackTraceElement = stackTrace[i];
265 | if (!BlockHoundRuntime.class.getName().equals(stackTraceElement.getClassName())) {
266 | continue;
267 | }
268 |
269 | if ("checkBlocking".equals(stackTraceElement.getMethodName())) {
270 | if (i + 1 < length) {
271 | error.setStackTrace(Arrays.copyOfRange(stackTrace, i + 1, length));
272 | }
273 | break;
274 | }
275 | }
276 |
277 | throw error;
278 | };
279 |
280 | private Predicate threadPredicate = t -> false;
281 |
282 | private Predicate dynamicThreadPredicate = t -> false;
283 |
284 | private Instrumentation configuredInstrumentation;
285 |
286 | /**
287 | * Marks provided method of the provided class as "blocking".
288 | *
289 | * The descriptor should be in JVM's format:
290 | * https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html#wp276
291 | *
292 | * @param clazz a class reference
293 | * @param methodName a method name
294 | * @param signature a method descriptor in JVM's format
295 | * @return this
296 | */
297 | public Builder markAsBlocking(Class clazz, String methodName, String signature) {
298 | return markAsBlocking(clazz.getName(), methodName, signature);
299 | }
300 |
301 | /**
302 | * Marks provided method of the class identified by the provided name as "blocking".
303 | *
304 | * The descriptor should be in JVM's format:
305 | * https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html#wp276
306 | *
307 | * @param className class' name (e.g. "java.lang.Thread")
308 | * @param methodName a method name
309 | * @param signature a method signature (in JVM's format)
310 | * @return this
311 | */
312 | public Builder markAsBlocking(String className, String methodName, String signature) {
313 | blockingMethods.computeIfAbsent(className.replace(".", "/"), __ -> new HashMap<>())
314 | .computeIfAbsent(methodName, __ -> new HashSet<>())
315 | .add(signature);
316 | return this;
317 | }
318 |
319 | /**
320 | * Allows blocking calls inside any method of a class with name identified by the provided className
321 | * and which name matches the provided methodName.
322 | *
323 | * There are two special cases for {@code methodName}:
324 | *
325 | *
326 | * static initializers are currently supported by their JVM reserved name of {@code ""}
327 | *
328 | *
329 | * constructors are currently not supported (ByteBuddy cannot weave the necessary instrumentation around a constructor that throws an exception, see gh174)
330 | *
331 | *
332 | *
333 | * @param className class' name (e.g. "java.lang.Thread")
334 | * @param methodName a method name
335 | * @return this
336 | */
337 | // see https://github.com/reactor/BlockHound/issues/174
338 | public Builder allowBlockingCallsInside(String className, String methodName) {
339 | allowances.computeIfAbsent(className, __ -> new HashMap<>()).put(methodName, true);
340 | return this;
341 | }
342 |
343 | /**
344 | * Disallows blocking calls inside any method of a class with name identified by the provided className
345 | * and which name matches the provided methodName.
346 | *
347 | * There are two special cases for {@code methodName}:
348 | *
349 | *
350 | * static initializers are currently supported by their JVM reserved name of {@code ""}
351 | *
352 | *
353 | * constructors are currently not supported (ByteBuddy cannot weave the necessary instrumentation around a constructor that throws an exception, see gh174)
354 | *
355 | *
356 | *
357 | * @param className class' name (e.g. "java.lang.Thread")
358 | * @param methodName a method name
359 | * @return this
360 | */
361 | // see https://github.com/reactor/BlockHound/issues/174
362 | public Builder disallowBlockingCallsInside(String className, String methodName) {
363 | allowances.computeIfAbsent(className, __ -> new HashMap<>()).put(methodName, false);
364 | return this;
365 | }
366 |
367 | /**
368 | * Overrides the callback that is being triggered when a blocking method is detected
369 | * @param consumer a consumer of the detected blocking method call's description ({@link BlockingMethod}).
370 | *
371 | * @return this
372 | */
373 | public Builder blockingMethodCallback(Consumer consumer) {
374 | this.onBlockingMethod = consumer;
375 | return this;
376 | }
377 |
378 | /**
379 | * Replaces the current non-blocking thread predicate with the result of applying the provided function.
380 | *
381 | * Warning! Consider always using {@link Predicate#or(Predicate)} and not override the previous one:
382 | *
383 | * nonBlockingThreadPredicate(current -> current.or(MyMarker.class::isInstance))
384 | *
385 | *
386 | * @param function a function to immediately apply on the current instance of the predicate
387 | * @return this
388 | */
389 | public Builder nonBlockingThreadPredicate(Function, Predicate> function) {
390 | this.threadPredicate = function.apply(this.threadPredicate);
391 | return this;
392 | }
393 |
394 | /**
395 | * Replaces the current dynamic thread predicate with the result of applying the provided function.
396 | *
397 | * Warning! Consider always using {@link Predicate#or(Predicate)} and not override the previous one:
398 | *
399 | * dynamicThreadPredicate(current -> current.or(MyMarker.class::isInstance))
400 | *
401 | *
402 | * @param function a function to immediately apply on the current instance of the predicate
403 | * @return this
404 | */
405 | public Builder dynamicThreadPredicate(Function, Predicate> function) {
406 | this.dynamicThreadPredicate = function.apply(this.dynamicThreadPredicate);
407 | return this;
408 | }
409 |
410 | /**
411 | * Appends the provided predicate to the current one.
412 | *
413 | * @param predicate a predicate to append to the current instance of the predicate
414 | * @return this
415 | */
416 | public Builder addDynamicThreadPredicate(Predicate predicate) {
417 | return dynamicThreadPredicate(p -> p.or(predicate));
418 | }
419 |
420 | /**
421 | * Loads integrations with {@link ServiceLoader} and adds provided integrations
422 | * using {{@link #with(BlockHoundIntegration)}}.
423 | * If you don't want to load the integrations using service loader, only use
424 | * {@link #with(BlockHoundIntegration)} method.
425 | *
426 | * @param integrations an array of integrations to automatically apply on the builder using
427 | * {@link #with(BlockHoundIntegration)}
428 | * @return this
429 | * @see BlockHound#builder()
430 | */
431 | public Builder loadIntegrations(BlockHoundIntegration... integrations) {
432 | ServiceLoader serviceLoader = ServiceLoader.load(BlockHoundIntegration.class);
433 | Stream
434 | .concat(StreamSupport.stream(serviceLoader.spliterator(), false), Stream.of(integrations))
435 | .sorted()
436 | .forEach(this::with);
437 | return this;
438 | }
439 |
440 | /**
441 | * Applies the provided {@link BlockHoundIntegration} to the current builder
442 | * @param integration an integration to apply
443 | * @return this
444 | */
445 | public Builder with(BlockHoundIntegration integration) {
446 | integration.applyTo(this);
447 | return this;
448 | }
449 |
450 | /**
451 | * Configure the {@link Instrumentation} to use. If not provided, {@link ByteBuddyAgent#install()} is used.
452 | *
453 | * @param instrumentation The instrumentation instance to use.
454 | * @return this
455 | */
456 | public Builder with(Instrumentation instrumentation) {
457 | this.configuredInstrumentation = instrumentation;
458 | return this;
459 | }
460 |
461 | Builder() {
462 | }
463 |
464 | /**
465 | * Installs the agent and runs the instrumentation, but only if BlockHound wasn't installed yet (it is global).
466 | */
467 | public void install() {
468 | if (!INITIALIZED.compareAndSet(false, true)) {
469 | return;
470 | }
471 |
472 | Consumer originalOnBlockingMethod = onBlockingMethod;
473 | try {
474 | Instrumentation instrumentation = configuredInstrumentation == null ?
475 | ByteBuddyAgent.install() : configuredInstrumentation;
476 | InstrumentationUtils.injectBootstrapClasses(
477 | instrumentation,
478 | BLOCK_HOUND_RUNTIME_TYPE.getInternalName(),
479 | "reactor/blockhound/BlockHoundRuntime$State"
480 | );
481 |
482 | // Since BlockHoundRuntime is injected into the bootstrap classloader,
483 | // we use raw Object[] here instead of `BlockingMethod` to avoid classloading issues
484 | BlockHoundRuntime.blockingMethodConsumer = args -> {
485 | String className = (String) args[0];
486 | String methodName = (String) args[1];
487 | int modifiers = (Integer) args[2];
488 | onBlockingMethod.accept(new BlockingMethod(className, methodName, modifiers));
489 | };
490 |
491 | onBlockingMethod = m -> {
492 | Thread currentThread = Thread.currentThread();
493 | if (currentThread instanceof TestThread) {
494 | ((TestThread) currentThread).blockingCallDetected = true;
495 | }
496 | };
497 | BlockHoundRuntime.dynamicThreadPredicate = t -> false;
498 | BlockHoundRuntime.threadPredicate = TestThread.class::isInstance;
499 |
500 | instrument(instrumentation);
501 | }
502 | catch (Throwable e) {
503 | throw new RuntimeException(e);
504 | }
505 |
506 | testInstrumentation();
507 |
508 | // Eagerly trigger the classloading of `dynamicThreadPredicate` (since classloading is blocking)
509 | dynamicThreadPredicate.test(Thread.currentThread());
510 | BlockHoundRuntime.dynamicThreadPredicate = dynamicThreadPredicate;
511 |
512 | // Eagerly trigger the classloading of `threadPredicate` (since classloading is blocking)
513 | threadPredicate.test(Thread.currentThread());
514 | BlockHoundRuntime.threadPredicate = threadPredicate;
515 |
516 | onBlockingMethod = originalOnBlockingMethod;
517 |
518 | // Re-evaluate the current thread's state after assigning user-provided predicates
519 | BlockHoundRuntime.STATE.remove();
520 | }
521 |
522 | private void testInstrumentation() {
523 | TestThread thread = new TestThread();
524 | thread.startAndWait();
525 |
526 | // Set in the artificial blockingMethodConsumer, see install()
527 | if (thread.blockingCallDetected) {
528 | return;
529 | }
530 |
531 | String message = "The instrumentation have failed.";
532 | try {
533 | // Test some public API class added in Java 13
534 | Class.forName("sun.nio.ch.NioSocketImpl");
535 | message += "\n";
536 | message += "It looks like you're running on JDK 13+.\n";
537 | message += "You need to add '-XX:+AllowRedefinitionToAddDeleteMethods' JVM flag.\n";
538 | message += "See https://github.com/reactor/BlockHound/issues/33 for more info.";
539 | }
540 | catch (ClassNotFoundException ignored) {
541 | }
542 |
543 | throw new IllegalStateException(message);
544 | }
545 |
546 | private void instrument(Instrumentation instrumentation) {
547 | ClassFileTransformer transformer = new NativeWrappingClassFileTransformer(blockingMethods);
548 | instrumentation.addTransformer(transformer, true);
549 | instrumentation.setNativeMethodPrefix(transformer, PREFIX);
550 |
551 | new AgentBuilder.Default()
552 | .with(RedefinitionStrategy.RETRANSFORMATION)
553 | // Explicit strategy is almost 2 times faster than SinglePass
554 | // TODO https://github.com/raphw/byte-buddy/issues/715
555 | .with(new DiscoveryStrategy.Explicit(
556 | Stream
557 | .of(instrumentation.getAllLoadedClasses())
558 | .filter(it -> it.getName() != null)
559 | .filter(it -> {
560 | if (allowances.containsKey(it.getName())) {
561 | return true;
562 | }
563 |
564 | String internalClassName = it.getName().replace(".", "/");
565 | if (blockingMethods.containsKey(internalClassName)) {
566 | return true;
567 | }
568 |
569 | return false;
570 | })
571 | .toArray(Class[]::new)
572 | ))
573 | .with(TypeStrategy.Default.DECORATE)
574 | .with(InitializationStrategy.NoOp.INSTANCE)
575 | // this DescriptionStrategy is required to force ByteBuddy to parse the bytes
576 | // and not cache them, since we run another transformer (see NativeWrappingClassFileTransformer)
577 | // before ByteBuddy
578 | .with(DescriptionStrategy.Default.POOL_FIRST)
579 | // Override PoolStrategy because the default one will cache java.lang.Object,
580 | // and we need to instrument it.
581 | .with(BlockHoundPoolStrategy.INSTANCE)
582 | .with(AgentBuilder.Listener.StreamWriting.toSystemError().withErrorsOnly())
583 |
584 | // Do not ignore JDK classes
585 | .ignore(ElementMatchers.none())
586 |
587 | // Instrument blocking calls
588 | .type(it -> blockingMethods.containsKey(it.getInternalName()))
589 | .transform(new BlockingCallsByteBuddyTransformer(blockingMethods))
590 | .asTerminalTransformation()
591 |
592 | // Instrument allowed/disallowed methods
593 | .type(it -> allowances.containsKey(it.getName()))
594 | .transform(new AllowancesByteBuddyTransformer(allowances))
595 | .asTerminalTransformation()
596 |
597 | .installOn(instrumentation);
598 | }
599 | }
600 | }
601 |
--------------------------------------------------------------------------------
/agent/src/main/java/reactor/blockhound/BlockHoundRuntime.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018-2019 Pivotal Software Inc, All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package reactor.blockhound;
18 |
19 | import java.util.function.Consumer;
20 | import java.util.function.Predicate;
21 |
22 | // Warning!!! This class MUST NOT be loaded by any classloader other than the bootstrap one.
23 | // Otherwise, non-bootstrap classes will be referring to it, but only the bootstrap one gets
24 | // initialized.
25 | class BlockHoundRuntime {
26 |
27 | public static final class State {
28 |
29 | final boolean dynamic;
30 |
31 | boolean allowed = false;
32 |
33 | public State(boolean dynamic) {
34 | this(dynamic, false);
35 | }
36 |
37 | State(boolean dynamic, boolean allowed) {
38 | this.dynamic = dynamic;
39 | this.allowed = allowed;
40 | }
41 |
42 | public boolean isDynamic() {
43 | return dynamic;
44 | }
45 |
46 | public boolean isAllowed() {
47 | return allowed;
48 | }
49 |
50 | public void setAllowed(boolean allowed) {
51 | this.allowed = allowed;
52 | }
53 | }
54 |
55 | public static volatile Consumer