├── .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://img.shields.io/badge/dynamic/xml.svg?label=Release&color=green&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](https://repo1.maven.org/maven2/io/projectreactor/tools/blockhound/) 4 | [![](https://img.shields.io/badge/dynamic/xml.svg?label=Milestone&color=blue&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo.spring.io%2Fmilestone%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](https://repo.spring.io/milestone/io/projectreactor/tools/blockhound/) 5 | [![](https://img.shields.io/badge/dynamic/xml.svg?label=Snapshot&color=orange&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo.spring.io%2Fsnapshot%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](https://repo.spring.io/snapshot/io/projectreactor/tools/blockhound/) 6 | [![Gitter](https://badges.gitter.im/reactor/BlockHound.svg)](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://img.shields.io/badge/dynamic/xml.svg?label=&color=green&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](https://repo1.maven.org/maven2/io/projectreactor/tools/blockhound/)| 91 | |`$LATEST_MILESTONE`|[![](https://img.shields.io/badge/dynamic/xml.svg?label=&color=blue&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo.spring.io%2Fmilestone%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](https://repo.spring.io/milestone/io/projectreactor/tools/blockhound/)| 92 | |`$LATEST_SNAPSHOT`|[![](https://img.shields.io/badge/dynamic/xml.svg?label=&color=orange&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo.spring.io%2Fsnapshot%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](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 blockingMethodConsumer; 56 | 57 | public static volatile Predicate threadPredicate; 58 | 59 | public static volatile Predicate dynamicThreadPredicate; 60 | 61 | public static final ThreadLocal STATE = ThreadLocal.withInitial(() -> { 62 | boolean isDynamic = dynamicThreadPredicate.test(Thread.currentThread()); 63 | if (isDynamic) { 64 | return new State(true); 65 | } 66 | 67 | boolean isNonBlocking = threadPredicate.test(Thread.currentThread()); 68 | if (isNonBlocking) { 69 | return new State(false); 70 | } 71 | 72 | // Optimization: return `null` if not dynamic and `not non-blocking` 73 | return null; 74 | }); 75 | 76 | @SuppressWarnings("unused") 77 | public static void checkBlocking(String internalClassName, String methodName, int modifiers) { 78 | State state = STATE.get(); 79 | if (state == null || state.isAllowed()) { 80 | return; 81 | } 82 | 83 | if (state.isDynamic()) { 84 | boolean isNonBlocking = threadPredicate.test(Thread.currentThread()); 85 | if (!isNonBlocking) { 86 | return; 87 | } 88 | } 89 | blockingMethodConsumer.accept(new Object[] { 90 | internalClassName.replace("/", "."), 91 | methodName, 92 | modifiers 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/BlockingCallsByteBuddyTransformer.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 | import java.util.Set; 35 | 36 | /** 37 | * This transformer applies {@link BlockingCallAdvice} to every method 38 | * registered with {@link BlockHound.Builder#markAsBlocking(Class, String, String)}. 39 | */ 40 | class BlockingCallsByteBuddyTransformer implements AgentBuilder.Transformer { 41 | 42 | private Map>> blockingMethods; 43 | 44 | BlockingCallsByteBuddyTransformer(Map>> blockingMethods) { 45 | this.blockingMethods = blockingMethods; 46 | } 47 | 48 | @Override 49 | public DynamicType.Builder transform( 50 | DynamicType.Builder builder, 51 | TypeDescription typeDescription, 52 | ClassLoader classLoader, 53 | JavaModule module, 54 | ProtectionDomain protectionDomain 55 | ) { 56 | Map> methods = blockingMethods.get(typeDescription.getInternalName()); 57 | 58 | if (methods == null) { 59 | return builder; 60 | } 61 | 62 | AsmVisitorWrapper advice = Advice.withCustomMapping() 63 | .bind(ModifiersArgument.Factory.INSTANCE) 64 | .to(BlockingCallAdvice.class) 65 | .on(method -> { 66 | Set descriptors = methods.get(method.getInternalName()); 67 | return descriptors != null && descriptors.contains(method.getDescriptor()); 68 | }); 69 | 70 | return builder.visit(advice); 71 | } 72 | 73 | @Documented 74 | @Retention(RetentionPolicy.RUNTIME) 75 | @java.lang.annotation.Target(ElementType.PARAMETER) 76 | @interface ModifiersArgument { 77 | 78 | /** 79 | * Binds advice method's argument annotated with {@link ModifiersArgument} 80 | * to method's modifiers (static, final, private, etc) 81 | */ 82 | enum Factory implements Advice.OffsetMapping.Factory { 83 | INSTANCE; 84 | 85 | @Override 86 | public Class getAnnotationType() { 87 | return ModifiersArgument.class; 88 | } 89 | 90 | @Override 91 | public Advice.OffsetMapping make( 92 | ParameterDescription.InDefinedShape target, 93 | AnnotationDescription.Loadable annotation, 94 | AdviceType adviceType 95 | ) { 96 | return (instrumentedType, instrumentedMethod, assigner, argumentHandler, sort) -> { 97 | int modifiers = instrumentedMethod.getModifiers(); 98 | return Advice.OffsetMapping.Target.ForStackManipulation.of(modifiers); 99 | }; 100 | } 101 | } 102 | } 103 | 104 | static class BlockingCallAdvice { 105 | 106 | @Advice.OnMethodEnter 107 | static void onEnter( 108 | @Advice.Origin("#t") String declaringType, 109 | @Advice.Origin("#m") String methodName, 110 | @BlockingCallsByteBuddyTransformer.ModifiersArgument int modifiers 111 | ) { 112 | BlockHoundRuntime.checkBlocking(declaringType, methodName, modifiers); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/BlockingMethod.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.io.Serializable; 20 | 21 | import static net.bytebuddy.jar.asm.Opcodes.ACC_STATIC; 22 | 23 | public class BlockingMethod implements Serializable { 24 | 25 | private final String className; 26 | 27 | private final String name; 28 | 29 | private final int modifiers; 30 | 31 | public BlockingMethod(String className, String name, int modifiers) { 32 | this.className = className; 33 | this.name = name; 34 | this.modifiers = modifiers; 35 | } 36 | 37 | /** 38 | * @return a class' name of the detected blocking call (e.g. "java.lang.Thread") 39 | */ 40 | public String getClassName() { 41 | return className; 42 | } 43 | 44 | /** 45 | * @return a blocking method's name (e.g. "sleep"). 46 | */ 47 | public String getName() { 48 | return name; 49 | } 50 | 51 | /** 52 | * @return a blocking methods' modifiers. see https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html 53 | */ 54 | public int getModifiers() { 55 | return modifiers; 56 | } 57 | 58 | public boolean isStatic() { 59 | return (getModifiers() & ACC_STATIC) != 0; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return String.format("%s%s%s", className, isStatic() ? "." : "#", name); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/BlockingOperationError.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 | public final class BlockingOperationError extends Error { 20 | 21 | private static final long serialVersionUID = 4980196508457280342L; 22 | 23 | private final BlockingMethod method; 24 | 25 | public BlockingOperationError(BlockingMethod method) { 26 | super(String.format("Blocking call! %s", method)); 27 | this.method = method; 28 | } 29 | 30 | public BlockingMethod getMethod() { 31 | return method; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/InstrumentationUtils.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.jar.asm.ClassReader; 20 | import net.bytebuddy.jar.asm.ClassVisitor; 21 | import net.bytebuddy.jar.asm.ClassWriter; 22 | import net.bytebuddy.jar.asm.Opcodes; 23 | 24 | import java.io.File; 25 | import java.io.FileOutputStream; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.lang.instrument.Instrumentation; 29 | import java.lang.reflect.InvocationTargetException; 30 | import java.lang.reflect.Method; 31 | import java.util.jar.JarFile; 32 | import java.util.zip.ZipEntry; 33 | import java.util.zip.ZipOutputStream; 34 | 35 | class InstrumentationUtils { 36 | 37 | /** 38 | * Constant used to indicate the current JDK major version (8,9,..22,...) 39 | */ 40 | static final int jdkMajorVersion; 41 | 42 | static { 43 | try { 44 | jdkMajorVersion = getJdkMajorVersion(); 45 | } 46 | catch (InvocationTargetException | IllegalAccessException e) { 47 | throw new ExceptionInInitializerError(e); 48 | } 49 | } 50 | 51 | static void injectBootstrapClasses(Instrumentation instrumentation, String... classNames) throws IOException { 52 | File tempJarFile = File.createTempFile("BlockHound", ".jar"); 53 | tempJarFile.deleteOnExit(); 54 | 55 | ClassLoader classLoader = BlockHound.class.getClassLoader(); 56 | try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(tempJarFile))) { 57 | for (String className : classNames) { 58 | String classFile = className.replace(".", "/") + ".class"; 59 | try (InputStream inputStream = classLoader.getResourceAsStream(classFile)) { 60 | ZipEntry entry = new ZipEntry(classFile); 61 | zipOutputStream.putNextEntry(entry); 62 | 63 | ClassReader cr = new ClassReader(inputStream); 64 | ClassWriter cw = new ClassWriter(cr, 0); 65 | 66 | cr.accept(new MakePublicClassVisitor(cw), 0); 67 | 68 | zipOutputStream.write(cw.toByteArray()); 69 | } 70 | 71 | zipOutputStream.closeEntry(); 72 | } 73 | } 74 | instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(tempJarFile)); 75 | } 76 | 77 | /** 78 | * Makes the class, fields and methods public 79 | */ 80 | static class MakePublicClassVisitor extends ClassVisitor { 81 | 82 | MakePublicClassVisitor(ClassWriter cw) { 83 | super(Opcodes.ASM7, cw); 84 | } 85 | 86 | @Override 87 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 88 | super.visit(version, access | Opcodes.ACC_PUBLIC, name, signature, superName, interfaces); 89 | } 90 | } 91 | 92 | /** 93 | * Helper method to detect the current JDK major version. 94 | * For security reasons, we don't rely on "java.version" system property, but on Runtime.version() method, which is 95 | * available from JDK9 + 96 | * And starting from JDK10+, we rely on Runtime.version().feature() method. 97 | * 98 | * @return the current jdk major version (8, 9, 10, ... 22) 99 | */ 100 | private static int getJdkMajorVersion() throws InvocationTargetException, IllegalAccessException { 101 | Object version = getRuntimeVersion(); 102 | 103 | if (version == null) { 104 | return 8; // Runtime.version() not available, JDK 8 105 | } 106 | 107 | return getRuntimeVersionFeature(version); 108 | } 109 | 110 | /** 111 | * Detects the Runtime.version() object, or null if JDK version is < JDK 9 112 | * 113 | * @return the detected JDK version object or null if not available 114 | */ 115 | private static Object getRuntimeVersion() throws InvocationTargetException, IllegalAccessException { 116 | Runtime runtime = Runtime.getRuntime(); 117 | try { 118 | Method versionMethod = runtime.getClass().getMethod("version"); 119 | return versionMethod.invoke(null); 120 | } 121 | 122 | catch (NoSuchMethodException e) { 123 | // Method Runtime.version() not found -> return null, meaning JDK 8 124 | return null; // JDK 8 125 | } 126 | } 127 | 128 | /** 129 | * Extracts the major version from the JDK version object. 130 | * 131 | * @param version the JDK version object 132 | * @return the major version (9, 10, ...) 133 | */ 134 | private static int getRuntimeVersionFeature(Object version) throws InvocationTargetException, IllegalAccessException { 135 | try { 136 | Method featureMethod = version.getClass().getMethod("feature"); 137 | Object feature = featureMethod.invoke(version); 138 | return (int) feature; 139 | } 140 | 141 | catch (NoSuchMethodException e) { 142 | // Version.feature() method not found -> JDK 9 (because feature method is only available starting from JDK10 +) 143 | return 9; 144 | } 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/NativeWrappingClassFileTransformer.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.jar.asm.*; 20 | 21 | import java.lang.instrument.ClassFileTransformer; 22 | import java.security.ProtectionDomain; 23 | import java.util.Map; 24 | import java.util.Set; 25 | 26 | import static net.bytebuddy.jar.asm.Opcodes.*; 27 | 28 | /** 29 | * This ASM-based transformer finds all methods defined in {@link NativeWrappingClassFileTransformer#blockingMethods} 30 | * and creates a delegating method by prefixing the original native method. 31 | * 32 | */ 33 | class NativeWrappingClassFileTransformer implements ClassFileTransformer { 34 | 35 | static final Type BLOCK_HOUND_RUNTIME_TYPE = Type.getType("Lreactor/blockhound/BlockHoundRuntime;"); 36 | 37 | private final Map>> blockingMethods; 38 | 39 | private static final int JDK_18 = 18; 40 | 41 | NativeWrappingClassFileTransformer(final Map>> blockingMethods) { 42 | this.blockingMethods = blockingMethods; 43 | } 44 | 45 | @Override 46 | public byte[] transform( 47 | ClassLoader loader, 48 | String className, 49 | Class classBeingRedefined, 50 | ProtectionDomain protectionDomain, 51 | byte[] classfileBuffer 52 | ) { 53 | Map> blockingMethodsOfClass = blockingMethods.get(className); 54 | if (blockingMethodsOfClass == null) { 55 | return null; 56 | } 57 | 58 | ClassReader cr = new ClassReader(classfileBuffer); 59 | ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); 60 | 61 | try { 62 | cr.accept(new NativeWrappingClassVisitor(cw, blockingMethodsOfClass, className), 0); 63 | 64 | classfileBuffer = cw.toByteArray(); 65 | } 66 | catch (Throwable e) { 67 | e.printStackTrace(); 68 | throw e; 69 | } 70 | 71 | return classfileBuffer; 72 | } 73 | 74 | static class NativeWrappingClassVisitor extends ClassVisitor { 75 | 76 | private final String className; 77 | 78 | private final Map> methods; 79 | 80 | NativeWrappingClassVisitor(ClassVisitor cw, Map> methods, String internalClassName) { 81 | super(ASM7, cw); 82 | this.className = internalClassName; 83 | this.methods = methods; 84 | } 85 | 86 | @Override 87 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 88 | if ((access & ACC_NATIVE) == 0) { 89 | return super.visitMethod(access, name, descriptor, signature, exceptions); 90 | } 91 | 92 | Set descriptors = methods.get(name); 93 | 94 | if (descriptors == null || !descriptors.contains(descriptor)) { 95 | return super.visitMethod(access, name, descriptor, signature, exceptions); 96 | } 97 | 98 | super.visitMethod( 99 | ACC_NATIVE | ACC_PRIVATE | ACC_FINAL | (access & ACC_STATIC), 100 | BlockHound.PREFIX + name, 101 | descriptor, 102 | signature, 103 | exceptions 104 | ); 105 | 106 | MethodVisitor delegatingMethodVisitor = super.visitMethod(access & ~ACC_NATIVE, name, descriptor, signature, exceptions); 107 | delegatingMethodVisitor.visitCode(); 108 | 109 | return new MethodVisitor(ASM7, delegatingMethodVisitor) { 110 | @Override 111 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { 112 | // See #392 113 | if (InstrumentationUtils.jdkMajorVersion >= JDK_18 && descriptor.equals("Ljdk/internal/vm/annotation/IntrinsicCandidate;")) { 114 | return null; // remove the intrinsic annotation 115 | } 116 | return super.visitAnnotation(descriptor, visible); 117 | } 118 | 119 | @Override 120 | public void visitEnd() { 121 | Type returnType = Type.getReturnType(descriptor); 122 | Type[] argumentTypes = Type.getArgumentTypes(descriptor); 123 | boolean isStatic = (access & ACC_STATIC) != 0; 124 | if (!isStatic) { 125 | visitVarInsn(ALOAD, 0); 126 | } 127 | int index = isStatic ? 0 : 1; 128 | for (Type argumentType : argumentTypes) { 129 | visitVarInsn(argumentType.getOpcode(ILOAD), index); 130 | index += argumentType.getSize(); 131 | } 132 | 133 | visitMethodInsn( 134 | isStatic ? INVOKESTATIC : INVOKESPECIAL, 135 | className, 136 | BlockHound.PREFIX + name, 137 | descriptor, 138 | false 139 | ); 140 | visitInsn(returnType.getOpcode(IRETURN)); 141 | visitMaxs(0, 0); 142 | super.visitEnd(); 143 | } 144 | }; 145 | } 146 | 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/TestThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-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 java.util.concurrent.FutureTask; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | /** 23 | * This is an internal class for performing the instrumentation test 24 | */ 25 | class TestThread extends Thread { 26 | 27 | volatile boolean blockingCallDetected = false; 28 | 29 | final FutureTask task = new FutureTask<>(() -> { 30 | Thread.sleep(0); 31 | return null; 32 | }); 33 | 34 | TestThread() { 35 | super(); 36 | setName("blockhound-test-thread"); 37 | setDaemon(true); 38 | } 39 | 40 | @Override 41 | public void run() { 42 | task.run(); 43 | } 44 | 45 | public void startAndWait() { 46 | start(); 47 | try { 48 | task.get(5, TimeUnit.SECONDS); 49 | } 50 | catch (Exception e) { 51 | if (e instanceof InterruptedException) { 52 | Thread.currentThread().interrupt(); 53 | } 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/integration/BlockHoundIntegration.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.integration; 18 | 19 | import reactor.blockhound.BlockHound; 20 | 21 | /** 22 | * An interface that defines the contract for the BlockHound integrations. 23 | * 24 | * {@link BlockHoundIntegration#applyTo(BlockHound.Builder)} will receive an instance 25 | * of the builder that is being installed. 26 | * 27 | * One can override {@link Comparable#compareTo(Object)} to ensure the order in case 28 | * one needs to run an integration before or after another. 29 | */ 30 | public interface BlockHoundIntegration extends Comparable { 31 | 32 | /** 33 | * Lets an integration apply the customizations (see {@link BlockHound.Builder}) 34 | * before BlockHound is installed. 35 | * 36 | * @param builder an instance of {@link BlockHound.Builder} that is being installed 37 | */ 38 | void applyTo(BlockHound.Builder builder); 39 | 40 | /** 41 | * Returns the default priority level for this integration. The priority level 42 | * controls the ordering of the {@link BlockHoundIntegration} plugins. 43 | * Plugins which do not provide a priority are sorted using natural ordering, and 44 | * their {@link #applyTo(BlockHound.Builder)} method will be called using the order 45 | * in which the plugins are loaded. 46 | * 47 | * @return The {@link BlockHoundIntegration} plugin priority, 0 by default. 48 | * @see #compareTo(BlockHoundIntegration) 49 | */ 50 | default int getPriority() { 51 | return 0; 52 | } 53 | 54 | @Override 55 | default int compareTo(BlockHoundIntegration o) { 56 | return Integer.compare(getPriority(), o.getPriority()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/integration/LoggingIntegration.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.integration; 18 | 19 | import com.google.auto.service.AutoService; 20 | import reactor.blockhound.BlockHound; 21 | 22 | /** 23 | * 24 | * @deprecated replaced by a more generic stdout/stderr solution: {@link StandardOutputIntegration} 25 | */ 26 | @Deprecated 27 | @AutoService(BlockHoundIntegration.class) 28 | public class LoggingIntegration implements BlockHoundIntegration { 29 | 30 | @Override 31 | public void applyTo(BlockHound.Builder builder) { 32 | try { 33 | Class.forName("org.gradle.internal.io.LineBufferingOutputStream"); 34 | builder.allowBlockingCallsInside("org.gradle.internal.io.LineBufferingOutputStream", "write"); 35 | } catch (ClassNotFoundException __) { 36 | } 37 | 38 | try { 39 | Class.forName("ch.qos.logback.classic.Logger"); 40 | builder.allowBlockingCallsInside("ch.qos.logback.classic.Logger", "callAppenders"); 41 | } catch (ClassNotFoundException e) { 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/integration/ReactorIntegration.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.integration; 18 | 19 | import com.google.auto.service.AutoService; 20 | import reactor.blockhound.BlockHound; 21 | import reactor.core.scheduler.NonBlocking; 22 | 23 | import java.util.concurrent.ScheduledThreadPoolExecutor; 24 | 25 | @AutoService(BlockHoundIntegration.class) 26 | public class ReactorIntegration implements BlockHoundIntegration { 27 | 28 | @Override 29 | public void applyTo(BlockHound.Builder builder) { 30 | try { 31 | Class.forName("reactor.core.publisher.Flux"); 32 | } 33 | catch (ClassNotFoundException ignored) { 34 | return; 35 | } 36 | 37 | try { 38 | // Reactor 3.3.x comes with built-in integration 39 | Class.forName("reactor.core.CorePublisher"); 40 | return; 41 | } 42 | catch (ClassNotFoundException ignored) { 43 | } 44 | 45 | // `ScheduledThreadPoolExecutor$DelayedWorkQueue.offer` parks the Thread with Unsafe#park. 46 | builder.allowBlockingCallsInside(ScheduledThreadPoolExecutor.class.getName(), "scheduleAtFixedRate"); 47 | builder.allowBlockingCallsInside(ScheduledThreadPoolExecutor.class.getName() + "$DelayedWorkQueue", "take"); 48 | 49 | builder.nonBlockingThreadPredicate(current -> current.or(NonBlocking.class::isInstance)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/integration/RxJava2Integration.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.integration; 18 | 19 | import com.google.auto.service.AutoService; 20 | import io.reactivex.internal.schedulers.NonBlockingThread; 21 | import reactor.blockhound.BlockHound; 22 | 23 | @AutoService(BlockHoundIntegration.class) 24 | public class RxJava2Integration implements BlockHoundIntegration { 25 | 26 | @Override 27 | public void applyTo(BlockHound.Builder builder) { 28 | try { 29 | Class.forName("io.reactivex.Flowable"); 30 | } 31 | catch (ClassNotFoundException ignored) { 32 | return; 33 | } 34 | 35 | builder.nonBlockingThreadPredicate(current -> current.or(NonBlockingThread.class::isInstance)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /agent/src/main/java/reactor/blockhound/integration/StandardOutputIntegration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-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.integration; 18 | 19 | import com.google.auto.service.AutoService; 20 | import reactor.blockhound.BlockHound; 21 | 22 | import java.io.IOException; 23 | import java.io.PrintStream; 24 | import java.util.Locale; 25 | 26 | @AutoService(BlockHoundIntegration.class) 27 | public class StandardOutputIntegration implements BlockHoundIntegration { 28 | 29 | @Override 30 | public void applyTo(BlockHound.Builder builder) { 31 | System.setOut(new PrintStreamDelegate(System.out)); 32 | System.setErr(new PrintStreamDelegate(System.err)); 33 | 34 | for (String method : new String[]{ 35 | "flush", 36 | "close", 37 | "checkError", 38 | "write", 39 | "print", 40 | "println", 41 | "printf", 42 | "format", 43 | "append", 44 | "write" 45 | }) { 46 | builder.allowBlockingCallsInside(PrintStreamDelegate.class.getName(), method); 47 | } 48 | } 49 | 50 | static class PrintStreamDelegate extends PrintStream { 51 | 52 | final PrintStream delegate; 53 | 54 | PrintStreamDelegate(PrintStream delegate) { 55 | super(delegate); 56 | this.delegate = delegate; 57 | } 58 | 59 | @Override 60 | public void flush() { 61 | delegate.flush(); 62 | } 63 | 64 | @Override 65 | public void close() { 66 | delegate.close(); 67 | } 68 | 69 | @Override 70 | public boolean checkError() { 71 | return delegate.checkError(); 72 | } 73 | 74 | @Override 75 | public void write(int b) { 76 | delegate.write(b); 77 | } 78 | 79 | @Override 80 | public void write(byte[] buf, int off, int len) { 81 | delegate.write(buf, off, len); 82 | } 83 | 84 | @Override 85 | public void print(boolean b) { 86 | delegate.print(b); 87 | } 88 | 89 | @Override 90 | public void print(char c) { 91 | delegate.print(c); 92 | } 93 | 94 | @Override 95 | public void print(int i) { 96 | delegate.print(i); 97 | } 98 | 99 | @Override 100 | public void print(long l) { 101 | delegate.print(l); 102 | } 103 | 104 | @Override 105 | public void print(float f) { 106 | delegate.print(f); 107 | } 108 | 109 | @Override 110 | public void print(double d) { 111 | delegate.print(d); 112 | } 113 | 114 | @Override 115 | public void print(char[] s) { 116 | delegate.print(s); 117 | } 118 | 119 | @Override 120 | public void print(String s) { 121 | delegate.print(s); 122 | } 123 | 124 | @Override 125 | public void print(Object obj) { 126 | delegate.print(obj); 127 | } 128 | 129 | @Override 130 | public void println() { 131 | delegate.println(); 132 | } 133 | 134 | @Override 135 | public void println(boolean x) { 136 | delegate.println(x); 137 | } 138 | 139 | @Override 140 | public void println(char x) { 141 | delegate.println(x); 142 | } 143 | 144 | @Override 145 | public void println(int x) { 146 | delegate.println(x); 147 | } 148 | 149 | @Override 150 | public void println(long x) { 151 | delegate.println(x); 152 | } 153 | 154 | @Override 155 | public void println(float x) { 156 | delegate.println(x); 157 | } 158 | 159 | @Override 160 | public void println(double x) { 161 | delegate.println(x); 162 | } 163 | 164 | @Override 165 | public void println(char[] x) { 166 | delegate.println(x); 167 | } 168 | 169 | @Override 170 | public void println(String x) { 171 | delegate.println(x); 172 | } 173 | 174 | @Override 175 | public void println(Object x) { 176 | delegate.println(x); 177 | } 178 | 179 | @Override 180 | public PrintStream printf(String format, Object... args) { 181 | return delegate.printf(format, args); 182 | } 183 | 184 | @Override 185 | public PrintStream printf(Locale l, String format, Object... args) { 186 | return delegate.printf(l, format, args); 187 | } 188 | 189 | @Override 190 | public PrintStream format(String format, Object... args) { 191 | return delegate.format(format, args); 192 | } 193 | 194 | @Override 195 | public PrintStream format(Locale l, String format, Object... args) { 196 | return delegate.format(l, format, args); 197 | } 198 | 199 | @Override 200 | public PrintStream append(CharSequence csq) { 201 | return delegate.append(csq); 202 | } 203 | 204 | @Override 205 | public PrintStream append(CharSequence csq, int start, int end) { 206 | return delegate.append(csq, start, end); 207 | } 208 | 209 | @Override 210 | public PrintStream append(char c) { 211 | return delegate.append(c); 212 | } 213 | 214 | @Override 215 | public void write(byte[] b) throws IOException { 216 | delegate.write(b); 217 | } 218 | 219 | @Override 220 | public String toString() { 221 | return delegate.toString(); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /benchmarks/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "me.champeau.gradle.jmh" version "0.5.3" 4 | } 5 | 6 | sourceCompatibility = targetCompatibility = 8 7 | 8 | repositories { 9 | maven { url 'https://repo.spring.io/libs-milestone' } 10 | } 11 | 12 | jmh { 13 | jmhVersion = '1.23' 14 | duplicateClassesStrategy DuplicatesStrategy.INCLUDE 15 | failOnError = true 16 | 17 | def outputType = "baseline" == project.findProperty("jmhTarget") ? "jmhBaseline" : "jmh" 18 | resultFormat = "text" 19 | resultsFile = project.file("${project.buildDir}/reports/${outputType}/result.${resultFormat}") 20 | } 21 | 22 | dependencies { 23 | switch (project.findProperty("jmhTarget")) { 24 | case "baseline": 25 | jmh "io.projectreactor.tools:blockhound:${compatibleVersion}" 26 | break 27 | default: 28 | jmh project(path: ":agent", configuration: 'shadow') 29 | } 30 | 31 | jmh "org.openjdk.jmh:jmh-generator-annprocess:${jmh.jmhVersion}" 32 | } 33 | -------------------------------------------------------------------------------- /benchmarks/src/jmh/java/reactor/blockhound/BlockHoundBenchmark.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 | package reactor.blockhound; 17 | 18 | import org.openjdk.jmh.annotations.Benchmark; 19 | import org.openjdk.jmh.annotations.BenchmarkMode; 20 | import org.openjdk.jmh.annotations.Measurement; 21 | import org.openjdk.jmh.annotations.Mode; 22 | import org.openjdk.jmh.annotations.OperationsPerInvocation; 23 | import org.openjdk.jmh.annotations.OutputTimeUnit; 24 | import org.openjdk.jmh.annotations.Scope; 25 | import org.openjdk.jmh.annotations.Setup; 26 | import org.openjdk.jmh.annotations.State; 27 | import org.openjdk.jmh.annotations.Warmup; 28 | 29 | import java.util.concurrent.TimeUnit; 30 | import java.util.concurrent.atomic.AtomicLong; 31 | 32 | @SuppressWarnings("WeakerAccess") 33 | @Warmup(iterations = 5) 34 | @Measurement(iterations = 3) 35 | @BenchmarkMode({Mode.AverageTime}) 36 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 37 | @OperationsPerInvocation(BlockHoundBenchmark.OPERATIONS_PER_INVOCATION) 38 | public class BlockHoundBenchmark { 39 | 40 | static { 41 | Thread.setDefaultUncaughtExceptionHandler((t, e) -> e.printStackTrace()); 42 | } 43 | 44 | static final int OPERATIONS_PER_INVOCATION = 1_000; 45 | 46 | static AtomicLong counter = new AtomicLong(); 47 | 48 | static void nonBlockingCall() { 49 | counter.incrementAndGet(); 50 | } 51 | 52 | static void blockingCall() { 53 | try { 54 | Thread.sleep(0); 55 | } 56 | catch (InterruptedException e) { 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | 61 | static void allowsBlockingCalls() { 62 | blockingCall(); 63 | } 64 | 65 | @State(Scope.Benchmark) 66 | public static class BlockHoundInstalledState { 67 | 68 | @Setup 69 | public void prepare() { 70 | System.out.println("Installing BlockHound"); 71 | BlockHound.builder() 72 | .nonBlockingThreadPredicate(p -> p.or(NonBlockingThread.class::isInstance)) 73 | .allowBlockingCallsInside(BlockHoundBenchmark.class.getName(), "allowsBlockingCalls") 74 | .blockingMethodCallback(m -> {}) // Do not throw 75 | .install(); 76 | } 77 | } 78 | 79 | @Benchmark 80 | public void baselineNonBlockingCall() throws Exception { 81 | Thread thread = new Thread(runMultipleTimes(BlockHoundBenchmark::nonBlockingCall)); 82 | thread.start(); 83 | thread.join(5_000); 84 | } 85 | 86 | @Benchmark 87 | public void measureNonBlockingCall(BlockHoundInstalledState state) throws Exception { 88 | Thread thread = new NonBlockingThread(runMultipleTimes(BlockHoundBenchmark::nonBlockingCall)); 89 | thread.start(); 90 | thread.join(5_000); 91 | } 92 | 93 | @Benchmark 94 | public void baselineBlockingCallInBlockingThread() throws Exception { 95 | Thread thread = new Thread(runMultipleTimes(BlockHoundBenchmark::blockingCall)); 96 | thread.start(); 97 | thread.join(5_000); 98 | } 99 | 100 | @Benchmark 101 | public void measureBlockingCallInBlockingThread(BlockHoundInstalledState state) throws Exception { 102 | Thread thread = new Thread(runMultipleTimes(BlockHoundBenchmark::blockingCall)); 103 | thread.start(); 104 | thread.join(5_000); 105 | } 106 | 107 | @Benchmark 108 | public void baselineAllowedBlockingCall() throws Exception { 109 | Thread thread = new Thread(runMultipleTimes(BlockHoundBenchmark::allowsBlockingCalls)); 110 | thread.start(); 111 | thread.join(5_000); 112 | } 113 | 114 | @Benchmark 115 | public void measureAllowedBlockingCall(BlockHoundInstalledState state) throws Exception { 116 | Thread thread = new NonBlockingThread(runMultipleTimes(BlockHoundBenchmark::allowsBlockingCalls)); 117 | thread.start(); 118 | thread.join(5_000); 119 | } 120 | 121 | static Runnable runMultipleTimes(Runnable runnable) { 122 | return () -> { 123 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 124 | runnable.run(); 125 | } 126 | }; 127 | } 128 | 129 | static final class NonBlockingThread extends Thread { 130 | public NonBlockingThread(Runnable target) { 131 | super(target); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | subprojects { 2 | apply from: "$rootDir/gradle/publishing.gradle" 3 | 4 | group = "io.projectreactor.tools" 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | tasks.withType(Test).all { 11 | def javaMajorVersion = System.getProperty("java.version").split("\\.", 2)[0].toInteger() 12 | if (javaMajorVersion >= 13) { 13 | jvmArgs += [ 14 | "-XX:+AllowRedefinitionToAddDeleteMethods" 15 | ] 16 | } 17 | } 18 | 19 | 20 | plugins.withType(SigningPlugin) { 21 | //skipping if not .RELEASE. Note the task graph is still being evaluated here. 22 | //This works because the version is only defined by CI scripts, and only the release one should use .RELEASE suffix 23 | def shouldSign = version.toString().endsWith(".RELEASE") 24 | project.signing { 25 | required { shouldSign } 26 | 27 | //skip the configuration entirely if !shouldSign. the task shouldn't even be available 28 | if (shouldSign) { 29 | def signingKey = System.getenv('GRADLE_SIGNING_KEY') 30 | def signingPassword = System.getenv('GRADLE_SIGNING_PASSWORD') 31 | useInMemoryPgpKeys(signingKey, signingPassword) 32 | 33 | afterEvaluate { 34 | sign publishing.publications.mavenJava 35 | } 36 | } 37 | } 38 | } 39 | 40 | plugins.withType(MavenPublishPlugin) { 41 | project.publishing { 42 | repositories { 43 | maven { 44 | url System.getenv('GRADLE_PUBLISH_REPO_URL') 45 | credentials { 46 | username = System.getenv('GRADLE_PUBLISH_MAVEN_USER') 47 | password = System.getenv('GRADLE_PUBLISH_MAVEN_PASSWORD') 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | tasks.withType(GenerateModuleMetadata) { 55 | enabled = false 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Table of contents 2 | * [Quick Start](quick_start.md) 3 | * [Supported testing frameworks](supported_testing_frameworks.md) 4 | * [Customization](customization.md) 5 | * [How it works](how_it_works.md) 6 | * [Writing custom integrations](custom_integrations.md) 7 | * [Tips & Tricks](tips.md) 8 | -------------------------------------------------------------------------------- /docs/custom_integrations.md: -------------------------------------------------------------------------------- 1 | # Custom integrations 2 | 3 | BlockHound can be extended without changing its code by using 4 | [the JVM's SPI mechanism](https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html). 5 | 6 | You will need to implement `reactor.blockhound.integration.BlockHoundIntegration` interface 7 | and add the implementor to `META-INF/services/reactor.blockhound.integration.BlockHoundIntegration` file. 8 | 9 | > ℹ️ **Hint:** consider using [Google's AutoService](https://github.com/google/auto/tree/master/service) for it: 10 | > ```java 11 | > @AutoService(BlockHoundIntegration.class) 12 | > public class MyIntegration implements BlockHoundIntegration { 13 | > // ... 14 | > } 15 | > ``` 16 | 17 | ## Writing integrations 18 | An integration is just a consumer of BlockHound's `Builder` and uses the same API as described in [customization](customization.md). 19 | 20 | Here is an example: 21 | ```java 22 | public class MyIntegration implements BlockHoundIntegration { 23 | 24 | @Override 25 | public void applyTo(BlockHound.Builder builder) { 26 | builder.nonBlockingThreadPredicate(current -> { 27 | return current.or(t -> { 28 | if (t.getName() == null) { 29 | return false; 30 | } 31 | return t.getName().contains("my-pool-"); 32 | }); 33 | }); 34 | } 35 | } 36 | ``` 37 | 38 | 39 | BlockHound's built-in integrations use the same mechanism and can be used as more advanced examples: 40 | https://github.com/reactor/BlockHound/tree/master/agent/src/main/java/reactor/blockhound/integration 41 | -------------------------------------------------------------------------------- /docs/customization.md: -------------------------------------------------------------------------------- 1 | # Customization 2 | 3 | BlockHound provides three means of usage: 4 | 1. `BlockHound.install()` - will use `ServiceLoader` to load all known `reactor.blockhound.integration.BlockHoundIntegration`s 5 | 1. `BlockHound.install(BlockHoundIntegration... integrations)` - same as `BlockHound.install()`, but adds user-provided integrations to the list. 6 | 1. `BlockHound.builder().install()` - will create a **new** builder, **without** discovering any integrations. 7 | You may install them manually by using `BlockHound.builder().with(new MyIntegration()).install()`. 8 | 9 | ## Marking more methods as blocking 10 | * `Builder#markAsBlocking(Class clazz, String methodName, String signature)` 11 | * `Builder#markAsBlocking(String className, String methodName, String signature)` 12 | 13 | Example: 14 | ```java 15 | builder.markAsBlocking("com.example.NativeHelper", "doSomethingBlocking", "(I)V"); 16 | ``` 17 | 18 | Note that the `signature` argument is 19 | [JVM's notation for the method signature](https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html#wp276). 20 | 21 | ## (Dis-)allowing blocking calls inside methods 22 | * `Builder#allowBlockingCallsInside(String className, String methodName)` 23 | * `Builder#disallowBlockingCallsInside(String className, String methodName)` 24 | 25 | Example: 26 | 27 | This will allow blocking method calls inside `Logger#callAppenders` down the callstack: 28 | ```java 29 | builder.allowBlockingCallsInside( 30 | "ch.qos.logback.classic.Logger", 31 | "callAppenders" 32 | ); 33 | ``` 34 | 35 | While this disallows blocking calls unless there is an allowed method down the callstack: 36 | ```java 37 | builder.disallowBlockingCallsInside( 38 | "reactor.core.publisher.Flux", 39 | "subscribe" 40 | ); 41 | ``` 42 | 43 | Example using Allow/Disalow 44 | 45 | The below example demonstrates how to allow the `NonBlockingClass.outer()` method to block, but not 46 | the `NonBlockingClass.inner()` method, which is called by the `outer()` method: 47 | 48 | ```java 49 | public class BlockingDisallowTest { 50 | 51 | static { 52 | BlockHound.install(b -> b 53 | .allowBlockingCallsInside(NonBlockingClass.class.getName(), "outer") 54 | .disallowBlockingCallsInside(NonBlockingClass.class.getName(), "inner") 55 | ); 56 | } 57 | 58 | static class NonBlockingClass { 59 | 60 | String inner() { 61 | try { 62 | //if this trips BlockHound, the test fails (inner not in the stacktrace) 63 | Thread.sleep(50); 64 | } 65 | catch (InterruptedException e) { 66 | e.printStackTrace(); 67 | } 68 | return "example"; 69 | } 70 | 71 | String outer() { 72 | try { 73 | Thread.sleep(50); 74 | } 75 | catch (InterruptedException e) { 76 | e.printStackTrace(); 77 | } 78 | Thread.yield(); 79 | return inner(); 80 | } 81 | } 82 | ``` 83 | The `NonBlockingClass.outer()` method is allowed to block and all the methods called down the stack, except the `inner()` method 84 | which is called by the `outer()` method. 85 | 86 | ## Custom blocking method callback 87 | * `Builder#blockingMethodCallback(Consumer consumer)` 88 | 89 | By default, BlockHound will throw an error when it detects a blocking call. 90 | But you can implement your own logic by setting a callback. 91 | 92 | Example: 93 | ```java 94 | builder.blockingMethodCallback(it -> { 95 | new Error(it.toString()).printStackTrace(); 96 | }); 97 | ``` 98 | Here we dump the stacktrace instead of throwing the error, so that we do not alter an execution of the code. 99 | 100 | ## Custom non-blocking thread predicate 101 | * `Builder#nonBlockingThreadPredicate(Function, Predicate> predicate)` 102 | 103 | If you integrate with exotic technologies, or implement your own thread pooling, 104 | you might want to mark those threads as non-blocking. Example: 105 | ```java 106 | builder.nonBlockingThreadPredicate(current -> { 107 | return current.or(it -> it.getName().contains("my-thread-")) 108 | }); 109 | ``` 110 | 111 | ⚠️ **Warning:** do not ignore the `current` predicate unless you're absolutely sure you know what you're doing. 112 | Other integrations will not work if you override it instead of using `Predicate#or`. 113 | -------------------------------------------------------------------------------- /docs/how_it_works.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | BlockHound is a Java Agent. 4 | 5 | It instruments the pre-defined set of blocking methods (see [customization](customization.md)) 6 | in the JVM and adds a special check before calling the callback 7 | (see [Blocking call decision](#Blocking-call-decision)). 8 | 9 | ## Blocking Java method detection 10 | To detect blocking Java methods, BlockHound alters the bytecode of a method and adds the following line at the beginning of the method's body: 11 | ```java 12 | // java.net.Socket 13 | public void connect(SocketAddress endpoint, int timeout) { 14 | reactor.blockhound.BlockHoundRuntime.checkBlocking( 15 | "java.net.Socket", 16 | "connect", 17 | /*method modifiers*/ 18 | ); 19 | ``` 20 | 21 | See [Blocking call decision](#Blocking-call-decision) for the details of how `checkBlocking` works. 22 | 23 | ## Blocking JVM native method detection 24 | Since native methods in JVM can't be instrumented (they have no body), we use JVM's native method instrumentation technique. 25 | 26 | Consider the following blocking method: 27 | ```java 28 | // java.lang.Thread 29 | public static native void sleep(long millis); 30 | ``` 31 | 32 | The method is public and we can't instrument the wrapping Java method. Instead, we relocate the old native method: 33 | ```java 34 | private static native void $$BlockHound$$_sleep(long millis); 35 | ``` 36 | 37 | Then we create a new Java method, with exactly same signature as the old one, delegating to the old implementation: 38 | ```java 39 | public static void sleep(long millis) { 40 | $$BlockHound$$_sleep(millis); 41 | } 42 | ``` 43 | 44 | As you can see, the cost of such instrumentation is minimal and only adds 1 hop to the original method. 45 | 46 | Now, we add the blocking call detection, [the same way as we do it with Java methods](#Blocking-Java-method-detection): 47 | ```java 48 | public static void sleep(long millis) { 49 | reactor.blockhound.BlockHoundRuntime.checkBlocking( 50 | "java.lang.Thread", 51 | "sleep", 52 | /*method modifiers*/ 53 | ); 54 | $$BlockHound$$_sleep(millis); 55 | } 56 | ``` 57 | 58 | ## Blocking call decision 59 | We could throw an error (or call the user-provided callback) on every blocking call, 60 | but sometimes there are blocking calls that must be called (class-loading is a good example). 61 | 62 | For this reason, BlockHound supports white- and blacklisting of different methods 63 | by checking the current state: 64 | ```java 65 | static void checkBlocking(String className, String methodName, int modifiers) { 66 | if (Boolean.FALSE == IS_ALLOWED.get()) { 67 | // Report 68 | } 69 | } 70 | ``` 71 | 72 | Where `IS_ALLOWED` ("is blocking call allowed in this thread or not") is a `ThreadLocal` variable that is defined as: 73 | ```java 74 | public static final ThreadLocal IS_ALLOWED = ThreadLocal.withInitial(() -> { 75 | if (threadPredicate.test(Thread.currentThread())) { 76 | return false; 77 | } 78 | else { 79 | // Optimization: use Three-state (true, false, null) where `null` is `not non-blocking` 80 | return null; 81 | } 82 | }); 83 | ``` 84 | 85 | it defaults to `false` ("not allowed") if the current thread is non-blocking, 86 | and to `null` otherwise. 87 | 88 | Then, we instrument every "allowed" (or "disallowed") method and set `IS_ALLOWED`: 89 | ```java 90 | class ClassLoader { 91 | // ... 92 | 93 | public Class loadClass(String name) { 94 | Boolean previous = BlockHoundRuntime.IS_ALLOWED.get(); 95 | BlockHoundRuntime.IS_ALLOWED.set(previous != null ? true : null); 96 | 97 | try { 98 | // Original call 99 | return loadClass(name, false); 100 | } finally { 101 | BlockHoundRuntime.IS_ALLOWED.set(previous); 102 | } 103 | } 104 | } 105 | ``` 106 | 107 | This way, unless there is a "disallowed" method inside `loadClass(String, boolean)`, 108 | `checkBlocking` will not report the blocking call. 109 | 110 | Note that the check is O(1) and equals to a single `ThreadLocal` read that is supposed 111 | to be fast enough for this use case. -------------------------------------------------------------------------------- /docs/quick_start.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | 3 | ## Getting it 4 | Download it from repo.spring.io or Maven Central repositories (stable releases only): 5 | 6 | ```groovy 7 | repositories { 8 | mavenCentral() 9 | // maven { url 'https://repo.spring.io/milestone' } 10 | // maven { url 'https://repo.spring.io/snapshot' } 11 | } 12 | 13 | dependencies { 14 | testImplementation 'io.projectreactor.tools:blockhound:$LATEST_RELEASE' 15 | // testImplementation 'io.projectreactor.tools:blockhound:$LATEST_MILESTONE' 16 | // testImplementation 'io.projectreactor.tools:blockhound:$LATEST_SNAPSHOT' 17 | } 18 | ``` 19 | Where: 20 | 21 | ||| 22 | |-|-| 23 | |`$LATEST_RELEASE`|[![](https://img.shields.io/badge/dynamic/xml.svg?label=&color=green&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](https://repo1.maven.org/maven2/io/projectreactor/tools/blockhound/)| 24 | |`$LATEST_MILESTONE`|[![](https://img.shields.io/badge/dynamic/xml.svg?label=&color=blue&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo.spring.io%2Fmilestone%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](https://repo.spring.io/milestone/io/projectreactor/tools/blockhound/)| 25 | |`$LATEST_SNAPSHOT`|[![](https://img.shields.io/badge/dynamic/xml.svg?label=&color=orange&query=%2F%2Fmetadata%2Fversioning%2Flatest&url=https%3A%2F%2Frepo.spring.io%2Fsnapshot%2Fio%2Fprojectreactor%2Ftools%2Fblockhound%2Fmaven-metadata.xml)](https://repo.spring.io/snapshot/io/projectreactor/tools/blockhound/)| 26 | 27 | ## JDK13+ support 28 | 29 | for JDK 13+, it is no longer allowed redefining native methods. So for the moment, as a temporary work around, please use the 30 | `-XX:+AllowRedefinitionToAddDeleteMethods` jvm argument: 31 | 32 | _Maven_ 33 | 34 | ```xml 35 | 36 | org.apache.maven.plugins 37 | maven-surefire-plugin 38 | 2.22.2 39 | 40 | -XX:+AllowRedefinitionToAddDeleteMethods 41 | 42 | 43 | ``` 44 | 45 | _Gradle_ 46 | 47 | ```groovy 48 | tasks.withType(Test).all { 49 | if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) { 50 | jvmArgs += [ 51 | "-XX:+AllowRedefinitionToAddDeleteMethods" 52 | ] 53 | } 54 | } 55 | ``` 56 | 57 | ## Using Tomcat 58 | 59 | When using BlockHound from a Tomcat webapp, do not embedd blockhound dependency within your webapp. Instead of that, just drop 60 | the blockhound jar in the Tomcat shared "lib" directory. 61 | If you are using `Cargo` maven plugin, this can be done using a [shared classpath](https://codehaus-cargo.github.io/cargo/Application+Classpath.html) 62 | 63 | Here is an example using Cargo maven plugin: 64 | 65 | ````xml 66 | 67 | 68 | io.projectreactor.tools 69 | blockhound 70 | (latest blockhound version) 71 | provided 72 | 73 | ... 74 | 75 | 76 | 77 | 78 | 79 | org.codehaus.cargo 80 | cargo-maven3-plugin 81 | 1.10.4 82 | 83 | 84 | tomcat9x 85 | embedded 86 | 87 | 88 | io.projectreactor.tools 89 | blockhound 90 | shared 91 | 92 | 93 | 94 | 95 | 96 | war 97 | ${project.build.directory}/${project.build.finalName}.war 98 | 99 | / 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ... 108 | ```` 109 | 110 | 111 | ## Installation 112 | BlockHound is a JVM agent. You need to "install" it before it starts detecting the issues: 113 | ```java 114 | BlockHound.install(); 115 | ``` 116 | 117 | On install, it will discover all known integrations (see [writing custom integrations](custom_integrations.md) for details) 118 | and perform a one time instrumentation (see [how it works](how_it_works.md)). 119 | The method is idempotent, you can call it multiple times. 120 | 121 | The best place to put this line is before *any* code gets executed, e.g. `@BeforeClass`, or `static {}` block, or test listener. 122 | BlockHound also [supports some testing frameworks](supported_testing_frameworks.md). 123 | 124 | **NB:** it is highly recommended to add a dummy test with a well-known blocking call to ensure that it installed correctly. 125 | Something like this will work: 126 | ```java 127 | Mono.delay(Duration.ofMillis(1)) 128 | .doOnNext(it -> { 129 | try { 130 | Thread.sleep(10); 131 | } 132 | catch (InterruptedException e) { 133 | throw new RuntimeException(e); 134 | } 135 | }) 136 | .block(); // should throw an exception about Thread.sleep 137 | ``` 138 | 139 | If you can't change the application code, you can also activate BlockHound using the -javaagent: JVM option, something like this 140 | (you need at least version 1.0.7): 141 | ```shell 142 | java -javaagent:BlockHound/agent/build/libs/agent.jar -jar my-application.jar 143 | ``` 144 | Notice that when using JPMS, for the moment BlockHound needs to be installed using `-javaavant` JVM option. 145 | 146 | ## What's Next? 147 | You can further customize Blockhound's behavior, see [customization](customization.md). 148 | -------------------------------------------------------------------------------- /docs/supported_testing_frameworks.md: -------------------------------------------------------------------------------- 1 | # Supported Testing Frameworks 2 | 3 | ## JUnit Platform (JUnit Jupiter or JUnit Vintage) 4 | 5 | BlockHound ships with an optional module providing a [JUnit Platform TestExecutionListener](https://junit.org/junit5/docs/current/user-guide/#launcher-api-listeners-custom): 6 | 7 | ```groovy 8 | 'io.projectreactor.tools:blockhound-junit-platform:$VERSION' 9 | ``` 10 | 11 | > ⚠️ Due to a [bug in Gradle](http://github.com/gradle/gradle/issues/8806), Gradle users must add the following dependency: 12 | > `testRuntime 'org.junit.platform:junit-platform-launcher'` (version 1.0.0 or higher) 13 | 14 | Once you add the `blockhound-junit-platform` artifact as a dependency, the BlockHound `TestExecutionListener` will be automatically registered and executed by the JUnit Platform. 15 | 16 | The implementation invokes `BlockHound.install()` (see [customization](./customization.md)). 17 | 18 | Should you need any customizations, you can implement them as [custom integrations](./custom_integrations.md). Simply implement the `reactor.blockhound.integration.BlockHoundIntegration` interface and register it in `META-INF/services/reactor.blockhound.integration.BlockHoundIntegration`. 19 | 20 | ## JUnit 3/4 21 | 22 | Unfortunatelly, there is no simple way to add a global lifecycle listener in JUnit 3 or JUnit 4. However, you can use the JUnit Platform integration by running your JUnit 3/4 tests as described in the [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4-running). 23 | -------------------------------------------------------------------------------- /docs/tips.md: -------------------------------------------------------------------------------- 1 | # Tips & Tricks 2 | 3 | ## Adding BlockHound to your tests 4 | 5 | When you add BlockHound, **make sure to add a test that will assert the integration.** 6 | Otherwise, you may get false positives due to the incorrectly installed agent (or not installed at all!). 7 | 8 | The simpliest test using Project Reactor would look like this: 9 | ```java 10 | @Test 11 | public void blockHoundWorks() throws TimeoutException, InterruptedException { 12 | try { 13 | FutureTask task = new FutureTask<>(() -> { 14 | Thread.sleep(0); 15 | return ""; 16 | }); 17 | Schedulers.parallel().schedule(task); 18 | 19 | task.get(10, TimeUnit.SECONDS); 20 | Assert.fail("should fail"); 21 | } catch (ExecutionException e) { 22 | Assert.assertTrue("detected", e.getCause() instanceof BlockingOperationError); 23 | } 24 | } 25 | ``` 26 | 27 | ## Debugging 28 | 29 | If your tests hang after adding BlockHound, it may be that some blocking call is detected 30 | and the event loop didn't handle the error properly. 31 | Even worse if there is `try {} catch {}` that ignores (swallows) the error. 32 | 33 | If you see such behaviour, **consider [overriding the callback](customization.md) and printing instead of throwing:** 34 | ```java 35 | BlockHound.install(builder -> { 36 | builder.blockingMethodCallback(it -> { 37 | new Exception(it.toString()).printStackTrace(); 38 | }); 39 | }); 40 | ``` 41 | 42 | This way you will run BlockHound in a "soft mode" where blocking operations 43 | are detected but won't cause the code to fail and help you to pinpoint the issue. 44 | 45 | But don't forget to change it back after debugging! 46 | 47 | ## How to select what to whitelist 48 | 49 | Sometimes some calls have to be whitelisted and cannot be avoided. 50 | 51 | BlockHound provides an API to whitelist them, but you have to be careful! 52 | **Whitelisting a common method (think `Thread#run`) may cause false positives!** 53 | 54 | Instead, whitelist the least common denominator you can find by iterating 55 | the stacktrace of the reported call. 56 | 57 | Consider the following code: 58 | ```java 59 | class OperationRunnable implements Runnable { 60 | 61 | TaskRunner runner; 62 | 63 | public void run() { 64 | while (true) { 65 | runner.run(); 66 | } 67 | } 68 | } 69 | 70 | class TaskRunner { 71 | 72 | TaskExecutor executor; 73 | 74 | public void run() { 75 | var task = executor.takeTask(); 76 | 77 | task.run(); 78 | } 79 | } 80 | ``` 81 | 82 | And the following stacktrace: 83 | ```java 84 | java.lang.Error: sun.misc.Unsafe#park 85 | at sun.misc.Unsafe.park(Unsafe.java) 86 | at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) 87 | at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078) 88 | at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467) 89 | at com.example.TaskExecutor.takeTask(GlobalEventExecutor.java:95) 90 | at com.example.TaskExecutor$TaskRunner.run(GlobalEventExecutor.java:239) 91 | at com.example.OperationRunnable.run(OperationRunnable.java:30) 92 | at com.example.NonBlockingThread.run(NonBlockingThread.java:18) 93 | ``` 94 | 95 | Whitelisting `NonBlockingThread#run`, `OperationRunnable#run` or even `TaskRunner#run` would 96 | prevent BlockHound from detecting blocking code in the tasks. 97 | 98 | Whitelisting `LinkedBlockingQueue#poll` or `LockSupport#parkNanos` would affect 99 | other places that may call this API and actually block what shouldn't be blocked. 100 | 101 | This is why the best candidate to be whitelisted is `TaskExecutor#takeTask` (unless this is public API!). 102 | It is blocking, but we need it to run our task polling logic. 103 | Since `TaskExecutor#takeTask` does not call any user provided code, we know how it will behave 104 | and can safely whitelist it. 105 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | } 4 | 5 | repositories { 6 | maven { url 'https://repo.spring.io/libs-milestone' } 7 | } 8 | 9 | test { 10 | dependsOn(tasks.getByPath(":agent:shadowJar")) 11 | 12 | sourceCompatibility = targetCompatibility = 10 13 | 14 | // Creates a JVM per test because the agent can be installed only once, 15 | // and we test different installations. 16 | // This is not required in normal testing scenarios 17 | forkEvery = 1 18 | maxParallelForks = 1 19 | jvmArgs += [ 20 | "-Xverify:all", 21 | JavaVersion.current().isJava9Compatible() 22 | ? "-Xlog:redefine+class*=warning" 23 | : "-XX:TraceRedefineClasses=2" 24 | ] 25 | } 26 | 27 | configurations { 28 | reactor_3_3_x { 29 | extendsFrom(testRuntimeOnly, testImplementation) 30 | } 31 | } 32 | 33 | task testReactor3_3_x(type: Test) { 34 | group = 'verification' 35 | 36 | testClassesDirs = sourceSets.test.output.classesDirs 37 | classpath = sourceSets.test.output.classesDirs + configurations.reactor_3_3_x 38 | 39 | include 'com/example/ReactorTest**' 40 | } 41 | check.dependsOn(testReactor3_3_x) 42 | 43 | dependencies { 44 | testImplementation project(path: ":agent", configuration: 'shadow') 45 | testImplementation 'io.projectreactor:reactor-core:3.2.5.RELEASE' 46 | testImplementation 'io.reactivex.rxjava2:rxjava:2.2.18' 47 | 48 | testImplementation 'junit:junit:4.13.2' 49 | testImplementation 'org.assertj:assertj-core:3.27.3' 50 | 51 | reactor_3_3_x 'io.projectreactor:reactor-core:3.2.5.RELEASE' 52 | } 53 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/BlockingDisallowTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023-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 com.example; 18 | 19 | import org.junit.Test; 20 | import reactor.blockhound.BlockHound; 21 | import reactor.blockhound.BlockingOperationError; 22 | import reactor.core.scheduler.Schedulers; 23 | 24 | import java.util.concurrent.CountDownLatch; 25 | import java.util.concurrent.TimeUnit; 26 | import java.util.concurrent.atomic.AtomicReference; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | public class BlockingDisallowTest { 31 | 32 | static { 33 | BlockHound.install(b -> b 34 | .allowBlockingCallsInside(NonBlockingClass.class.getName(), "outer") 35 | .disallowBlockingCallsInside(NonBlockingClass.class.getName(), "inner") 36 | ); 37 | } 38 | 39 | @Test 40 | public void shouldDisallow() throws InterruptedException { 41 | NonBlockingClass nbc = new NonBlockingClass(); 42 | AtomicReference boeRef = new AtomicReference<>(); 43 | 44 | //to trip BlockHound in the first place, we must be in a nonblocking thread 45 | CountDownLatch latch = new CountDownLatch(1); 46 | Schedulers.parallel().schedule(() -> { 47 | try { 48 | nbc.outer(); 49 | } catch (BlockingOperationError boe) { 50 | boeRef.set(boe); 51 | } finally { 52 | latch.countDown(); 53 | } 54 | }); 55 | 56 | latch.await(5, TimeUnit.SECONDS); 57 | 58 | //given the configuration we expect that Thread.yield() is allowed, but Thread.sleep() inside inner() isn't 59 | assertThat(boeRef.get()) 60 | .isNotNull() 61 | .hasMessageContaining("Blocking call! java.lang.Thread.sleep") 62 | .hasStackTraceContaining("at com.example.BlockingDisallowTest$NonBlockingClass.inner") 63 | .hasStackTraceContaining("at com.example.BlockingDisallowTest$NonBlockingClass.outer"); 64 | } 65 | 66 | static class NonBlockingClass { 67 | 68 | String inner() { 69 | try { 70 | //if this trips BlockHound, the test fails (inner not in the stacktrace) 71 | Thread.sleep(50); 72 | } catch (InterruptedException e) { 73 | e.printStackTrace(); 74 | } 75 | return "example"; 76 | } 77 | 78 | String outer() { 79 | try { 80 | Thread.sleep(50); 81 | } catch (InterruptedException e) { 82 | e.printStackTrace(); 83 | } 84 | Thread.yield(); 85 | return inner(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/BuilderTest.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 com.example; 18 | 19 | import org.junit.Test; 20 | import reactor.blockhound.BlockHound; 21 | import reactor.core.publisher.Mono; 22 | import reactor.core.scheduler.Schedulers; 23 | 24 | public class BuilderTest { 25 | 26 | static { 27 | BlockHound.builder().install(); 28 | } 29 | 30 | @Test 31 | public void builderShouldNotLoadIntegrations() { 32 | Mono.fromCallable(() -> { 33 | Thread.sleep(1); 34 | return ""; 35 | }).subscribeOn(Schedulers.parallel()).block(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/CustomBlockingMethodTest.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 com.example; 18 | 19 | import org.assertj.core.api.Assertions; 20 | import org.junit.Test; 21 | import reactor.blockhound.BlockHound; 22 | import reactor.blockhound.BlockingMethod; 23 | import reactor.blockhound.BlockingOperationError; 24 | import reactor.core.publisher.Mono; 25 | import reactor.core.scheduler.Schedulers; 26 | 27 | import java.time.Duration; 28 | 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | 31 | public class CustomBlockingMethodTest { 32 | 33 | static { 34 | // Load the class, so that we test the retransform too 35 | Blocking.block(); 36 | BlockHound.install(b -> { 37 | b.markAsBlocking(Blocking.class, "block", "()V"); 38 | }); 39 | } 40 | 41 | @Test 42 | public void shouldReportCustomBlockingMethods() { 43 | Throwable e = Assertions.catchThrowable(() -> { 44 | Mono.fromRunnable(Blocking::block).hide().subscribeOn(Schedulers.parallel()).block(Duration.ofMillis(100)); 45 | }); 46 | assertThat(e) 47 | .as("exception") 48 | .hasCauseInstanceOf(BlockingOperationError.class); 49 | 50 | assertThat(e.getCause()).isInstanceOfSatisfying(BlockingOperationError.class, cause -> { 51 | assertThat(cause.getMethod()) 52 | .isNotNull() 53 | .returns(Blocking.class.getName(), BlockingMethod::getClassName) 54 | .returns("block", BlockingMethod::getName); 55 | }); 56 | } 57 | 58 | static class Blocking { 59 | 60 | static void block() { 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/DynamicCurrentThreadTest.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.assertj.core.api.Assertions; 4 | import org.junit.After; 5 | import org.junit.Test; 6 | import reactor.blockhound.BlockHound; 7 | import reactor.blockhound.BlockingOperationError; 8 | 9 | public class DynamicCurrentThreadTest { 10 | 11 | private static final ThreadLocal CAN_BLOCK = ThreadLocal.withInitial(() -> true); 12 | 13 | static { 14 | var testThread = Thread.currentThread(); 15 | BlockHound.install(b -> { 16 | b.addDynamicThreadPredicate(testThread::equals); 17 | 18 | b.nonBlockingThreadPredicate(p -> p.or(thread -> { 19 | return !CAN_BLOCK.get(); 20 | })); 21 | }); 22 | } 23 | 24 | @After 25 | public void tearDown() { 26 | CAN_BLOCK.remove(); 27 | } 28 | 29 | @Test 30 | public void testChangingCurrentThreadsStatus() throws Exception { 31 | Thread.sleep(0); 32 | 33 | CAN_BLOCK.set(false); 34 | Assertions.assertThatThrownBy(() -> Thread.sleep(0)) 35 | .isInstanceOf(BlockingOperationError.class); 36 | 37 | // Reset to default 38 | CAN_BLOCK.remove(); 39 | Thread.sleep(0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/DynamicThreadsTest.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 com.example; 18 | 19 | import org.junit.Test; 20 | import reactor.blockhound.BlockHound; 21 | import reactor.blockhound.BlockingOperationError; 22 | 23 | import java.util.concurrent.CompletableFuture; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | import static org.junit.Assert.fail; 27 | 28 | public class DynamicThreadsTest { 29 | 30 | static { 31 | BlockHound.install(b -> { 32 | b.addDynamicThreadPredicate(DynamicThread.class::isInstance); 33 | 34 | b.nonBlockingThreadPredicate(p -> p.or(thread -> { 35 | return thread instanceof DynamicThread && ((DynamicThread) thread).isNonBlocking; 36 | })); 37 | }); 38 | } 39 | 40 | @Test 41 | public void shouldNotCacheDynamicThreads() throws Exception { 42 | CompletableFuture future = new CompletableFuture<>(); 43 | DynamicThread thread = new DynamicThread() { 44 | @Override 45 | public void run() { 46 | try { 47 | try { 48 | isNonBlocking = true; 49 | Thread.sleep(0); 50 | fail("should fail"); 51 | } 52 | catch (BlockingOperationError ignored) { 53 | } 54 | 55 | isNonBlocking = false; 56 | Thread.sleep(0); 57 | 58 | try { 59 | isNonBlocking = true; 60 | Thread.sleep(0); 61 | fail("should fail"); 62 | } 63 | catch (BlockingOperationError ignored) { 64 | } 65 | 66 | future.complete(null); 67 | } 68 | catch (Throwable e) { 69 | future.completeExceptionally(e); 70 | } 71 | } 72 | }; 73 | 74 | thread.start(); 75 | 76 | future.get(5, TimeUnit.SECONDS); 77 | } 78 | 79 | static class DynamicThread extends Thread { 80 | 81 | boolean isNonBlocking = true; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/FalseNegativesTest.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 com.example; 18 | 19 | import org.assertj.core.api.Assertions; 20 | import org.junit.Rule; 21 | import org.junit.Test; 22 | import org.junit.rules.Timeout; 23 | import reactor.blockhound.BlockHound; 24 | import reactor.core.publisher.Mono; 25 | import reactor.core.scheduler.Schedulers; 26 | 27 | import java.time.Duration; 28 | import java.util.concurrent.TimeUnit; 29 | 30 | import static org.assertj.core.api.Assertions.assertThat; 31 | 32 | public class FalseNegativesTest { 33 | 34 | static { 35 | BlockHound.install(); 36 | } 37 | 38 | @Rule 39 | public Timeout timeout = new Timeout(20, TimeUnit.SECONDS); 40 | 41 | @Test 42 | public void shouldNotReportClassLoader() throws Exception { 43 | ClassLoader classLoader = new ClassLoader() { 44 | @Override 45 | protected Class loadClass(String name, boolean resolve) { 46 | try { 47 | Thread.sleep(10); 48 | return null; 49 | } catch (InterruptedException e) { 50 | throw new RuntimeException(e); 51 | } 52 | } 53 | }; 54 | Throwable e = Assertions.catchThrowable(() -> { 55 | Mono.fromCallable(() -> { 56 | return classLoader.loadClass("does.not.exist"); 57 | }).hide().subscribeOn(Schedulers.parallel()).block(Duration.ofSeconds(10)); 58 | }); 59 | 60 | if (e != null) { 61 | e.printStackTrace(System.out); 62 | } 63 | 64 | assertThat(e).isNull(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/IntegrationOrderingTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023-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 com.example; 18 | 19 | import org.junit.Test; 20 | import reactor.blockhound.BlockHound; 21 | import reactor.blockhound.integration.BlockHoundIntegration; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | 28 | /** 29 | * BlockHoundIntegration plugins ordering tests. 30 | */ 31 | public class IntegrationOrderingTest { 32 | 33 | final static List applied = new ArrayList<>(); 34 | 35 | /** 36 | * Plugin with priority=-1, loaded from 37 | * META-INF/services/reactor.blockhound.integration.BlockHoundIntegration (2nd position in file) 38 | */ 39 | public static final class First implements BlockHoundIntegration { 40 | 41 | public First() { 42 | } 43 | 44 | @Override 45 | public int getPriority() { 46 | return -1; 47 | } 48 | 49 | @Override 50 | public void applyTo(BlockHound.Builder builder) { 51 | applied.add(1); 52 | } 53 | } 54 | 55 | /** 56 | * Plugin with default priority=0, loaded from 57 | * META-INF/services/reactor.blockhound.integration.BlockHoundIntegration (1st position in file) 58 | */ 59 | public static final class Second implements BlockHoundIntegration { 60 | @Override 61 | public void applyTo(BlockHound.Builder builder) { 62 | applied.add(2); 63 | } 64 | } 65 | 66 | /** 67 | * Plugin with priority=1, installed using {@link BlockHound#install(BlockHoundIntegration...)}} 68 | */ 69 | public static final class Third implements BlockHoundIntegration { 70 | @Override 71 | public int getPriority() { 72 | return 1; 73 | } 74 | 75 | @Override 76 | public void applyTo(BlockHound.Builder builder) { 77 | applied.add(3); 78 | } 79 | } 80 | 81 | /** 82 | * Plugin with priority=2, installed using {@link BlockHound#install(BlockHoundIntegration...)}} 83 | */ 84 | public static final class Fourth implements BlockHoundIntegration { 85 | @Override 86 | public int getPriority() { 87 | return 2; 88 | } 89 | 90 | @Override 91 | public void applyTo(BlockHound.Builder builder) { 92 | applied.add(4); 93 | } 94 | } 95 | 96 | /** 97 | * Plugin with default priority=0, installed using {@link BlockHound#install(BlockHoundIntegration...)}} 98 | */ 99 | public static final class Fifth implements BlockHoundIntegration { 100 | @Override 101 | public void applyTo(BlockHound.Builder builder) { 102 | applied.add(5); 103 | } 104 | } 105 | 106 | /** 107 | * Plugin with default priority=0, installed using {@link BlockHound#install(BlockHoundIntegration...)}} 108 | */ 109 | public static final class Sixth implements BlockHoundIntegration { 110 | @Override 111 | public void applyTo(BlockHound.Builder builder) { 112 | applied.add(6); 113 | } 114 | } 115 | 116 | /** 117 | * In this test, we install 6 blockhound integrations plugins. 118 | *
    119 | *
  • First: priority=-1, defined in META-INF/services/reactor.blockhound.integration.BlockHoundIntegration at 2nd position
  • 120 | *
  • Second: no priority (default=0), defined in META-INF/services/reactor.blockhound.integration.BlockHoundIntegration at 1st position
  • 121 | *
  • Third: priority=1, added using {link {@link BlockHound#install(BlockHoundIntegration...)}}, passed in 2nd parameter
  • 122 | *
  • Fourth: priority=2, added using {link {@link BlockHound#install(BlockHoundIntegration...)}}, passed in 1st parameter
  • 123 | *
  • Fifth: no priority (default=0), added using {link {@link BlockHound#install(BlockHoundIntegration...)}}, passed in 3rd parameter
  • 124 | *
  • Sixth: no priority, by default: 0, added using {link {@link BlockHound#install(BlockHoundIntegration...)}}, passed in 4th parameter
  • 125 | *
126 | * 127 | * We expect to see the 6 plugins applied in this order: First, Second, Fifth, Sixth, Third, Four. 128 | * And plugins without any priority should be loaded in natural order, as before. 129 | */ 130 | @Test 131 | public void checkIntegrationsOrdering() { 132 | // Do not install BlockHound in our static initialized, because other tests 133 | // will load our inner integrations classes ... 134 | BlockHound.install(new Fourth(), new Third(), new Fifth(), new Sixth()); 135 | Integer[] expectedApplies = new Integer[] { 1, 2, 5, 6, 3, 4}; 136 | assertThat(applied).containsExactly(expectedApplies); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/JDKTest.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 com.example; 18 | 19 | import org.junit.Test; 20 | import reactor.blockhound.BlockHound; 21 | import reactor.core.publisher.Flux; 22 | import reactor.core.scheduler.Schedulers; 23 | 24 | import java.util.concurrent.ConcurrentHashMap; 25 | 26 | public class JDKTest { 27 | 28 | static { 29 | BlockHound.install(); 30 | } 31 | 32 | @Test 33 | public void shouldAllowConcurrentHashMapInit() { 34 | for (int i = 0; i < 100; i++) { 35 | var map = new ConcurrentHashMap(); 36 | Flux.range(0, Runtime.getRuntime().availableProcessors()) 37 | .parallel() 38 | .runOn(Schedulers.parallel()) 39 | .doOnNext(it -> map.put(it, true)) 40 | .sequential() 41 | .blockLast(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/ReactorTest.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 com.example; 18 | 19 | import org.assertj.core.api.Assertions; 20 | import org.assertj.core.api.Assumptions; 21 | import org.junit.Rule; 22 | import org.junit.Test; 23 | import org.junit.rules.Timeout; 24 | import org.junit.runner.RunWith; 25 | import org.junit.runners.Parameterized; 26 | import org.junit.runners.Parameterized.Parameter; 27 | import org.junit.runners.Parameterized.Parameters; 28 | import reactor.blockhound.BlockHound; 29 | import reactor.blockhound.BlockingMethod; 30 | import reactor.blockhound.BlockingOperationError; 31 | import reactor.core.publisher.Flux; 32 | import reactor.core.publisher.Mono; 33 | import reactor.core.scheduler.Schedulers; 34 | 35 | import java.io.File; 36 | import java.io.FileInputStream; 37 | import java.io.FileOutputStream; 38 | import java.io.RandomAccessFile; 39 | import java.net.*; 40 | import java.time.Duration; 41 | import java.util.HashMap; 42 | import java.util.Map; 43 | import java.util.concurrent.Callable; 44 | import java.util.concurrent.TimeUnit; 45 | import java.util.concurrent.locks.LockSupport; 46 | import java.util.stream.Collectors; 47 | 48 | import static org.assertj.core.api.Assertions.assertThat; 49 | 50 | @RunWith(Parameterized.class) 51 | public class ReactorTest { 52 | 53 | static { 54 | BlockHound.install(); 55 | } 56 | 57 | @Parameters(name = "{index}: {0}") 58 | public static Iterable data() { 59 | Runtime.Version version = Runtime.version(); 60 | 61 | Map>> tests = new HashMap>>(); 62 | 63 | tests.put("java.net.Socket#connect", () -> { 64 | var socket = new Socket(); 65 | var inetSocketAddress = new InetSocketAddress("www.google.com", 80); 66 | return Mono.fromCallable(() -> { 67 | socket.connect(inetSocketAddress); 68 | return ""; 69 | }); 70 | }); 71 | 72 | tests.put("java.net.SocketInputStream#socketRead0", () -> { 73 | Assumptions.assumeThat(Runtime.version().feature()).isLessThan(13); 74 | var socket = new Socket("www.google.com", 80); 75 | socket.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes()); 76 | return Mono.fromCallable(() -> { 77 | return socket.getInputStream().read(new byte[1]); 78 | }); 79 | }); 80 | 81 | tests.put("java.net.Socket$SocketInputStream#read", () -> { 82 | Assumptions.assumeThat(Runtime.version().feature()).isGreaterThanOrEqualTo(13); 83 | var socket = new Socket("www.google.com", 80); 84 | socket.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes()); 85 | return Mono.fromCallable(() -> { 86 | return socket.getInputStream().read(new byte[1]); 87 | }); 88 | }); 89 | 90 | tests.put("java.net.SocketOutputStream#socketWrite0", () -> { 91 | Assumptions.assumeThat(version.feature()).isLessThan(13); 92 | var socket = new Socket("www.google.com", 80); 93 | return Mono.fromCallable(() -> { 94 | socket.getOutputStream().write(1); 95 | return null; 96 | }); 97 | }); 98 | 99 | tests.put("java.net.Socket$SocketOutputStream#write", () -> { 100 | Assumptions.assumeThat(version.feature()).isGreaterThanOrEqualTo(13); 101 | var socket = new Socket("www.google.com", 80); 102 | return Mono.fromCallable(() -> { 103 | socket.getOutputStream().write(1); 104 | return null; 105 | }); 106 | }); 107 | 108 | tests.put("jdk.internal.misc.Unsafe#park", () -> { 109 | return Mono.fromCallable(() -> { 110 | Thread thread = Thread.currentThread(); 111 | Mono.delay(Duration.ofMillis(100)).subscribe(__ -> LockSupport.unpark(thread)); 112 | LockSupport.park(); 113 | return ""; 114 | }); 115 | }); 116 | 117 | tests.put("java.lang.Object#wait", () -> { 118 | return Mono.fromCallable(() -> { 119 | Object lock = new Object(); 120 | synchronized (lock) { 121 | lock.wait(10); 122 | } 123 | return null; 124 | }); 125 | }); 126 | 127 | tests.put("java.lang.Thread." + (version.feature() >= 19 ? "sleep0" : "sleep"), () -> { 128 | return Mono.fromCallable(() -> { 129 | Thread.sleep(10); 130 | return ""; 131 | }); 132 | }); 133 | 134 | tests.put("java.lang.Thread." + (version.feature() >= 19 ? "yield0" : "yield"), () -> { 135 | return Mono.fromCallable(() -> { 136 | Thread.yield(); 137 | return ""; 138 | }); 139 | }); 140 | 141 | tests.put("java.lang.Thread.onSpinWait", () -> { 142 | return Mono.fromCallable(() -> { 143 | Thread.onSpinWait(); 144 | return ""; 145 | }); 146 | }); 147 | 148 | tests.put("java.io.FileOutputStream#write", () -> { 149 | var file = File.createTempFile("test", ""); 150 | var fileOutputStream = new FileOutputStream(file); 151 | return Mono.fromCallable(() -> { 152 | fileOutputStream.write(1); 153 | return ""; 154 | }); 155 | }); 156 | 157 | tests.put("java.io.FileInputStream#read0", () -> { 158 | var file = File.createTempFile("test", ""); 159 | var fileOutputStream = new FileInputStream(file); 160 | return Mono.fromCallable(() -> { 161 | fileOutputStream.read(); 162 | return ""; 163 | }); 164 | }); 165 | 166 | tests.put("java.io.RandomAccessFile#read0", () -> { 167 | var randomAccessFile = new RandomAccessFile(File.createTempFile("test", ""), "rw"); 168 | return Mono.fromCallable(() -> { 169 | randomAccessFile.read(); 170 | return ""; 171 | }); 172 | }); 173 | 174 | tests.put("java.io.RandomAccessFile#write0", () -> { 175 | var randomAccessFile = new RandomAccessFile(File.createTempFile("test", ""), "rw"); 176 | return Mono.fromCallable(() -> { 177 | randomAccessFile.write(1); 178 | return ""; 179 | }); 180 | }); 181 | 182 | tests.put("java.io.RandomAccessFile#writeBytes", () -> { 183 | var randomAccessFile = new RandomAccessFile(File.createTempFile("test", ""), "rw"); 184 | return Mono.fromCallable(() -> { 185 | randomAccessFile.writeBytes(""); 186 | return ""; 187 | }); 188 | }); 189 | 190 | tests.put("java.lang.ProcessImpl#forkAndExec", () -> { 191 | // TODO find which implementation is used on Windows 192 | Assumptions.assumeThat(System.getProperty("os.name").toLowerCase()).doesNotContain("win"); 193 | var processBuilder = new ProcessBuilder("date"); 194 | return Mono.fromCallable(() -> { 195 | processBuilder.start(); 196 | return ""; 197 | }); 198 | }); 199 | 200 | tests.put("java.net.DatagramSocket#connect", () -> { 201 | var socket = new DatagramSocket(); 202 | return Mono.fromCallable(() -> { 203 | socket.connect(InetAddress.getLoopbackAddress(), 0); 204 | return ""; 205 | }); 206 | }); 207 | 208 | tests.put("java.net.PlainDatagramSocketImpl#" + (version.feature() >= 13 ? "send0" : "send"), () -> { 209 | // TODO find which implementation is used on Windows 210 | Assumptions.assumeThat(System.getProperty("os.name").toLowerCase()).doesNotContain("win"); 211 | var socket = new DatagramSocket(); 212 | socket.connect(InetAddress.getByName("8.8.8.8"), 53); 213 | return Mono.fromCallable(() -> { 214 | socket.send(new DatagramPacket(new byte[1], 1)); 215 | return ""; 216 | }); 217 | }); 218 | 219 | tests.put("java.net.PlainDatagramSocketImpl#peekData", () -> { 220 | // TODO figure out why it fails on Linux 221 | Assumptions.assumeThat(System.getProperty("os.name")).containsIgnoringCase("mac"); 222 | var socket = new DatagramSocket(); 223 | socket.setSoTimeout(10); 224 | socket.connect(InetAddress.getByName("8.8.8.8"), 53); 225 | return Mono.fromCallable(() -> { 226 | socket.receive(new DatagramPacket(new byte[1], 1)); 227 | return ""; 228 | }).onErrorReturn(SocketTimeoutException.class, ""); 229 | }); 230 | 231 | tests.put("java.net.ServerSocket#implAccept", () -> { 232 | var socket = new ServerSocket(0); 233 | socket.setSoTimeout(100); 234 | return Mono.fromCallable(() -> { 235 | socket.accept(); 236 | return ""; 237 | }).onErrorReturn(SocketTimeoutException.class, ""); 238 | }); 239 | 240 | // tests.entrySet().removeIf(it -> !"java.lang.Thread.sleep".equals(it.getKey())); 241 | 242 | return tests.entrySet().stream().map(it -> new Object[]{it.getKey(), it.getValue()}).collect(Collectors.toList()); 243 | } 244 | 245 | @Parameter(0) 246 | public String method; 247 | 248 | @Parameter(1) 249 | public Callable> publisher; 250 | 251 | @Rule 252 | public Timeout timeout = new Timeout(2, TimeUnit.SECONDS); 253 | 254 | @Test 255 | public void positive() throws Exception { 256 | publisher.call().hide().subscribeOn(Schedulers.elastic()).block(Duration.ofSeconds(1)); 257 | } 258 | 259 | @Test 260 | public void negative() throws Exception { 261 | var mono = publisher.call().hide(); 262 | var e = Assertions.catchThrowable(() -> { 263 | mono.subscribeOn(Schedulers.parallel()).block(Duration.ofSeconds(1)); 264 | }); 265 | assertThat(e).hasCauseInstanceOf(BlockingOperationError.class); 266 | 267 | e = e.getCause(); 268 | e.printStackTrace(System.out); 269 | 270 | assertThat(e).isInstanceOfSatisfying(BlockingOperationError.class, it -> { 271 | assertThat(it.getMethod()) 272 | .isNotNull() 273 | .returns(method, BlockingMethod::toString); 274 | }); 275 | } 276 | 277 | @Test 278 | public void negativeWithFlux() throws Exception { 279 | var mono = Flux.from(publisher.call()).hide(); 280 | var e = Assertions.catchThrowable(() -> { 281 | mono.subscribeOn(Schedulers.parallel()).blockLast(Duration.ofSeconds(1)); 282 | }); 283 | assertThat(e).hasCauseInstanceOf(BlockingOperationError.class); 284 | 285 | e = e.getCause(); 286 | e.printStackTrace(System.out); 287 | 288 | assertThat(e).isInstanceOfSatisfying(BlockingOperationError.class, it -> { 289 | assertThat(it.getMethod()) 290 | .isNotNull() 291 | .returns(method, BlockingMethod::toString); 292 | }); 293 | } 294 | 295 | } 296 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/RxJavaTest.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 com.example; 18 | 19 | import io.reactivex.Flowable; 20 | import io.reactivex.Single; 21 | import io.reactivex.exceptions.CompositeException; 22 | import org.junit.Test; 23 | import reactor.blockhound.BlockHound; 24 | import reactor.blockhound.BlockingOperationError; 25 | import reactor.core.publisher.Flux; 26 | 27 | import java.util.concurrent.TimeUnit; 28 | 29 | public class RxJavaTest { 30 | 31 | static { 32 | BlockHound.install(); 33 | } 34 | 35 | @Test(expected = BlockingOperationError.class) 36 | public void testBlockingCallInsideRxJavaSingle() { 37 | Single.timer(10, TimeUnit.MILLISECONDS) 38 | .doOnSuccess(it -> Thread.sleep(10)) 39 | .blockingGet(); 40 | } 41 | 42 | @Test(expected = BlockingOperationError.class) 43 | public void testBlockingCallInsideRxJavaFlowable() throws Throwable { 44 | try { 45 | Flowable.timer(10, TimeUnit.MILLISECONDS) 46 | .doOnEach(it -> Thread.sleep(10)) 47 | .blockingFirst(); 48 | } catch (CompositeException e) { 49 | throw e.getExceptions().get(0); 50 | } 51 | } 52 | 53 | @Test(expected = BlockingOperationError.class) 54 | public void testBlockingCallInsideRxJavaInterop() throws Throwable { 55 | try { 56 | Flux.from(Flowable.timer(10, TimeUnit.MILLISECONDS)) 57 | .doOnEach(it -> { 58 | try { 59 | Thread.sleep(10); 60 | } catch (InterruptedException e) { 61 | e.printStackTrace(); 62 | } 63 | }) 64 | .blockFirst(); 65 | } catch (Exception e) { 66 | if (e.getClass().getName().contains("$ReactiveException")) { 67 | throw e.getCause(); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/StackTraceTest.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 com.example; 18 | 19 | import org.assertj.core.api.Assertions; 20 | import org.junit.Test; 21 | import reactor.blockhound.BlockHound; 22 | import reactor.blockhound.BlockingOperationError; 23 | import reactor.core.scheduler.NonBlocking; 24 | 25 | import java.util.concurrent.CompletableFuture; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | import static org.assertj.core.api.Assertions.tuple; 29 | 30 | public class StackTraceTest { 31 | 32 | static { 33 | BlockHound.install(); 34 | } 35 | 36 | @Test 37 | public void shouldHideInternalFrames() { 38 | CompletableFuture future = new CompletableFuture<>(); 39 | class TestThread extends Thread implements NonBlocking { 40 | @Override 41 | public void run() { 42 | try { 43 | Thread.sleep(0); 44 | future.complete(null); 45 | } 46 | catch (Throwable e) { 47 | future.completeExceptionally(e); 48 | } 49 | } 50 | } 51 | new TestThread().start(); 52 | 53 | assertThat(Assertions.catchThrowable(future::join)) 54 | .as("exception") 55 | .isNotNull() 56 | .hasCauseInstanceOf(BlockingOperationError.class) 57 | .satisfies(e -> { 58 | assertThat(e.getCause().getStackTrace()) 59 | .as("Cause's stacktrace") 60 | .extracting(StackTraceElement::getClassName, StackTraceElement::getMethodName) 61 | .containsExactly( 62 | tuple("java.lang.Thread", "sleep"), 63 | tuple(TestThread.class.getName(), "run") 64 | ); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/StandardOutputTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-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 com.example; 18 | 19 | import org.junit.Test; 20 | import reactor.blockhound.BlockHound; 21 | import reactor.core.scheduler.Schedulers; 22 | 23 | import java.util.concurrent.FutureTask; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | public class StandardOutputTest { 27 | 28 | static { 29 | BlockHound.install(); 30 | } 31 | 32 | @Test 33 | public void shouldNotReportStdout() throws Exception { 34 | FutureTask task = new FutureTask<>(() -> { 35 | System.out.println("Hello"); 36 | return null; 37 | }); 38 | Schedulers.parallel().schedule(task); 39 | task.get(10, TimeUnit.SECONDS); 40 | } 41 | 42 | @Test 43 | public void shouldNotReportStderr() throws Exception { 44 | FutureTask task = new FutureTask<>(() -> { 45 | System.err.println("Hello"); 46 | return null; 47 | }); 48 | Schedulers.parallel().schedule(task); 49 | task.get(10, TimeUnit.SECONDS); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/StaticInitTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-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 com.example; 18 | 19 | import org.junit.Test; 20 | import reactor.blockhound.BlockHound; 21 | import reactor.core.publisher.Mono; 22 | import reactor.core.scheduler.Schedulers; 23 | 24 | public class StaticInitTest { 25 | 26 | static { 27 | BlockHound.install(b -> { 28 | b.allowBlockingCallsInside(ClassWithStaticInit.class.getName(), ""); 29 | }); 30 | } 31 | 32 | @Test 33 | public void shouldInstrumentStaticInitializers() { 34 | Mono.fromCallable(ClassWithStaticInit::new).subscribeOn(Schedulers.parallel()).block(); 35 | } 36 | 37 | static class ClassWithStaticInit { 38 | static { 39 | try { 40 | Thread.sleep(0); 41 | } catch (InterruptedException e) { 42 | throw new RuntimeException(e); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/src/test/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2023-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 | com.example.IntegrationOrderingTest$Second 17 | com.example.IntegrationOrderingTest$First 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | compatibleVersion=1.0.13.RELEASE 3 | -------------------------------------------------------------------------------- /gradle/publishing.gradle: -------------------------------------------------------------------------------- 1 | plugins.withType(MavenPublishPlugin) { 2 | project.publishing { 3 | publications { 4 | mavenJava(MavenPublication) { publication -> 5 | pom { 6 | afterEvaluate { 7 | name = project.description 8 | description = project.detailedDescription 9 | } 10 | url = 'https://github.com/reactor/BlockHound' 11 | licenses { 12 | license { 13 | name = 'Apache License, Version 2.0' 14 | url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' 15 | distribution = 'repo' 16 | } 17 | } 18 | scm { 19 | url = 'https://github.com/reactor/BlockHound/' 20 | connection = 'scm:git:git://github.com/reactor/BlockHound.git' 21 | developerConnection = 'scm:git:ssh://git@github.com/reactor/BlockHound.git' 22 | } 23 | developers { 24 | developer { 25 | id = 'simonbasle' 26 | name = 'Simon Baslé' 27 | email = 'sbasle@vmware.com' 28 | } 29 | developer { 30 | id = 'bsideup' 31 | name = 'Sergei Egorov' 32 | email = 'bsideup@gmail.com' 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactor/BlockHound/e31c1cd5804b9ea352ff2594b97065c117698f66/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | -------------------------------------------------------------------------------- /junit-platform/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java-library" 3 | id "maven-publish" 4 | id "signing" 5 | } 6 | 7 | description = "BlockHound JUnit Platform Integration" 8 | ext.detailedDescription = "Integrates the BlockHound Java agent to detect blocking calls in JUnit Platform-based tests." 9 | 10 | sourceCompatibility = targetCompatibility = 8 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | test { 17 | useJUnitPlatform() 18 | 19 | forkEvery = 1 20 | maxParallelForks = 1 21 | } 22 | 23 | dependencies { 24 | compileOnly 'com.google.auto.service:auto-service-annotations:1.1.1' 25 | annotationProcessor 'com.google.auto.service:auto-service:1.1.1' 26 | 27 | //the api configuration ensures we publish blockhound at compile scope in pom 28 | api project(path: ":agent", configuration: 'shadow') 29 | 30 | compileOnly 'org.junit.platform:junit-platform-launcher:1.0.0' 31 | 32 | testImplementation 'org.assertj:assertj-core:3.27.3' 33 | testImplementation 'io.projectreactor:reactor-core:3.2.5.RELEASE' 34 | 35 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.13.0' 36 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.13.0' 37 | 38 | 39 | // FIXME remove once https://github.com/gradle/gradle/issues/8806 is fixed 40 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.0.0' 41 | } 42 | 43 | task sourcesJar(type: Jar) { 44 | archiveClassifier.set('sources') 45 | from sourceSets.main.allJava 46 | } 47 | 48 | task javadocJar(type: Jar) { 49 | from javadoc 50 | archiveClassifier.set('javadoc') 51 | } 52 | 53 | jar { 54 | manifest { 55 | attributes 'Automatic-Module-Name': 'reactor.blockhound.junit' 56 | } 57 | } 58 | 59 | publishing { 60 | publications { 61 | mavenJava(MavenPublication) { publication -> 62 | from components.java 63 | artifact sourcesJar 64 | artifact javadocJar 65 | 66 | artifactId 'blockhound-junit-platform' 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /junit-platform/src/main/java/reactor/blockhound/junit/platform/BlockHoundTestExecutionListener.java: -------------------------------------------------------------------------------- 1 | package reactor.blockhound.junit.platform; 2 | 3 | import com.google.auto.service.AutoService; 4 | import org.junit.platform.launcher.TestExecutionListener; 5 | import reactor.blockhound.BlockHound; 6 | import reactor.blockhound.integration.BlockHoundIntegration; 7 | 8 | /** 9 | * This {@link TestExecutionListener} installs BlockHound (via {@link BlockHound#install(BlockHoundIntegration...)} 10 | * as soon as JUnit Platform loads it. 11 | * 12 | * Although the class is public, it is only so due to SPI's limitation and SHOULD NOT be considered a public API. 13 | */ 14 | @AutoService(TestExecutionListener.class) 15 | public class BlockHoundTestExecutionListener implements TestExecutionListener { 16 | 17 | static { 18 | // Install it as early as possible (when JUnit Runner loads this class) 19 | BlockHound.install(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /junit-platform/src/test/java/reactor/blockhound/junit/platform/JUnitPlatformDynamicIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package reactor.blockhound.junit.platform; 2 | 3 | import org.assertj.core.api.AbstractThrowableAssert; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.DynamicTest; 6 | import org.junit.jupiter.api.TestFactory; 7 | import reactor.core.publisher.Mono; 8 | import reactor.core.scheduler.Schedulers; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | import static org.junit.jupiter.api.DynamicTest.dynamicTest; 14 | 15 | class JUnitPlatformDynamicIntegrationTest { 16 | 17 | @TestFactory 18 | List tests() { 19 | return Arrays.asList( 20 | dynamicTest("simple dynamic test", () -> { 21 | assertThatBlockingCall().hasMessageContaining("Blocking call!"); 22 | }) 23 | ); 24 | } 25 | 26 | private AbstractThrowableAssert assertThatBlockingCall() { 27 | return Assertions.assertThatCode(() -> { 28 | Mono 29 | .fromCallable(() -> { 30 | Thread.sleep(1); 31 | return ""; 32 | }) 33 | .subscribeOn(Schedulers.parallel()) 34 | .block(); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /junit-platform/src/test/java/reactor/blockhound/junit/platform/JUnitPlatformIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package reactor.blockhound.junit.platform; 2 | 3 | import org.assertj.core.api.AbstractThrowableAssert; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import reactor.core.publisher.Mono; 7 | import reactor.core.scheduler.Schedulers; 8 | 9 | class JUnitPlatformIntegrationTest { 10 | 11 | @Test 12 | void shouldApplyAutomatically() { 13 | assertThatBlockingCall().hasMessageContaining("Blocking call!"); 14 | } 15 | 16 | private AbstractThrowableAssert assertThatBlockingCall() { 17 | return Assertions.assertThatCode(() -> { 18 | Mono 19 | .fromCallable(() -> { 20 | Thread.sleep(1); 21 | return ""; 22 | }) 23 | .subscribeOn(Schedulers.parallel()) 24 | .block(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'reactor-block-hound' 2 | 3 | 4 | include 'agent' 5 | include 'example' 6 | include 'junit-platform' 7 | include 'benchmarks' 8 | --------------------------------------------------------------------------------