├── .DS_Store ├── .editorconfig ├── .github ├── maven-cd-settings.xml ├── maven-ci-settings.xml └── workflows │ ├── ci-4.x.yml │ ├── ci-5.x-stable.yml │ ├── ci-5.x.yml │ ├── ci-matrix-5.x.yml │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── LICENSE.txt ├── README.adoc ├── pom.xml └── src ├── main ├── asciidoc │ └── index.adoc ├── generated │ └── io │ │ └── vertx │ │ └── circuitbreaker │ │ └── CircuitBreakerOptionsConverter.java └── java │ ├── examples │ ├── CircuitBreakerExamples.java │ └── package-info.java │ ├── io │ └── vertx │ │ └── circuitbreaker │ │ ├── CircuitBreaker.java │ │ ├── CircuitBreakerOptions.java │ │ ├── CircuitBreakerState.java │ │ ├── FailurePolicy.java │ │ ├── OpenCircuitException.java │ │ ├── RetryPolicy.java │ │ ├── TimeoutException.java │ │ ├── impl │ │ ├── CircuitBreakerImpl.java │ │ └── CircuitBreakerMetrics.java │ │ └── package-info.java │ └── module-info.java └── test ├── java ├── io │ └── vertx │ │ └── circuitbreaker │ │ └── tests │ │ ├── JsonFactory.java │ │ ├── PredefinedRetryPolicyTest.java │ │ └── impl │ │ ├── APITest.java │ │ ├── AsyncBreakerTest.java │ │ ├── CircuitBreakerImplTest.java │ │ ├── CircuitBreakerMetricsTest.java │ │ ├── CircuitBreakerWithHTTPTest.java │ │ ├── MyAsyncOperations.java │ │ ├── NumberOfRetryTest.java │ │ ├── RetryPolicyTest.java │ │ └── UsageTest.java └── module-info.java └── resources └── META-INF └── services └── io.vertx.core.spi.JsonFactory /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vert-x3/vertx-circuit-breaker/51df782f51ebbd21a5ee67568c0a9d44ac779b85/.DS_Store -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/maven-cd-settings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | false 20 | 21 | 22 | 23 | vertx-snapshots-repository 24 | ${env.VERTX_NEXUS_USERNAME} 25 | ${env.VERTX_NEXUS_PASSWORD} 26 | 27 | 28 | 29 | 30 | 31 | google-mirror 32 | 33 | true 34 | 35 | 36 | 37 | google-maven-central 38 | GCS Maven Central mirror EU 39 | https://maven-central.storage-download.googleapis.com/maven2/ 40 | 41 | true 42 | 43 | 44 | false 45 | 46 | 47 | 48 | 49 | 50 | google-maven-central 51 | GCS Maven Central mirror 52 | https://maven-central.storage-download.googleapis.com/maven2/ 53 | 54 | true 55 | 56 | 57 | false 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /.github/maven-ci-settings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | false 20 | 21 | 22 | 23 | google-mirror 24 | 25 | true 26 | 27 | 28 | 29 | google-maven-central 30 | GCS Maven Central mirror EU 31 | https://maven-central.storage-download.googleapis.com/maven2/ 32 | 33 | true 34 | 35 | 36 | false 37 | 38 | 39 | 40 | 41 | 42 | google-maven-central 43 | GCS Maven Central mirror 44 | https://maven-central.storage-download.googleapis.com/maven2/ 45 | 46 | true 47 | 48 | 49 | false 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/ci-4.x.yml: -------------------------------------------------------------------------------- 1 | name: vertx-circuit-breaker (4.x) 2 | on: 3 | schedule: 4 | - cron: '0 4 * * *' 5 | jobs: 6 | CI: 7 | strategy: 8 | matrix: 9 | include: 10 | - os: ubuntu-latest 11 | jdk: 8 12 | - os: ubuntu-latest 13 | jdk: 17 14 | uses: ./.github/workflows/ci.yml 15 | with: 16 | branch: 4.x 17 | jdk: ${{ matrix.jdk }} 18 | os: ${{ matrix.os }} 19 | secrets: inherit 20 | Deploy: 21 | if: ${{ github.repository_owner == 'vert-x3' && (github.event_name == 'push' || github.event_name == 'schedule') }} 22 | needs: CI 23 | uses: ./.github/workflows/deploy.yml 24 | with: 25 | branch: 4.x 26 | jdk: 8 27 | secrets: inherit 28 | -------------------------------------------------------------------------------- /.github/workflows/ci-5.x-stable.yml: -------------------------------------------------------------------------------- 1 | name: vertx-circuit-breaker (5.x-stable) 2 | on: 3 | push: 4 | branches: 5 | - '5.[0-9]+' 6 | pull_request: 7 | branches: 8 | - '5.[0-9]+' 9 | schedule: 10 | - cron: '0 6 * * *' 11 | jobs: 12 | CI-CD: 13 | uses: ./.github/workflows/ci-matrix-5.x.yml 14 | secrets: inherit 15 | with: 16 | branch: ${{ github.event_name == 'schedule' && vars.VERTX_5_STABLE_BRANCH || github.event.pull_request.head.sha || github.ref_name }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ci-5.x.yml: -------------------------------------------------------------------------------- 1 | name: vertx-circuit-breaker (5.x) 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | schedule: 10 | - cron: '0 5 * * *' 11 | jobs: 12 | CI-CD: 13 | uses: ./.github/workflows/ci-matrix-5.x.yml 14 | secrets: inherit 15 | with: 16 | branch: ${{ github.event.pull_request.head.sha || github.ref_name }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ci-matrix-5.x.yml: -------------------------------------------------------------------------------- 1 | name: CI matrix (5.x) 2 | on: 3 | workflow_call: 4 | inputs: 5 | branch: 6 | required: true 7 | type: string 8 | jobs: 9 | CI: 10 | strategy: 11 | matrix: 12 | include: 13 | - os: ubuntu-latest 14 | jdk: 11 15 | - os: ubuntu-latest 16 | jdk: 21 17 | uses: ./.github/workflows/ci.yml 18 | with: 19 | branch: ${{ inputs.branch }} 20 | jdk: ${{ matrix.jdk }} 21 | os: ${{ matrix.os }} 22 | secrets: inherit 23 | Deploy: 24 | if: ${{ github.repository_owner == 'vert-x3' && (github.event_name == 'push' || github.event_name == 'schedule') }} 25 | needs: CI 26 | uses: ./.github/workflows/deploy.yml 27 | with: 28 | branch: ${{ inputs.branch }} 29 | jdk: 11 30 | secrets: inherit 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_call: 4 | inputs: 5 | branch: 6 | required: true 7 | type: string 8 | jdk: 9 | default: 8 10 | type: string 11 | os: 12 | default: ubuntu-latest 13 | type: string 14 | jobs: 15 | Test: 16 | name: Run tests 17 | runs-on: ${{ inputs.os }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | ref: ${{ inputs.branch }} 23 | - name: Install JDK 24 | uses: actions/setup-java@v2 25 | with: 26 | java-version: ${{ inputs.jdk }} 27 | distribution: temurin 28 | - name: Run tests 29 | run: mvn -s .github/maven-ci-settings.xml -q clean verify -B 30 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | workflow_call: 4 | inputs: 5 | branch: 6 | required: true 7 | type: string 8 | jdk: 9 | default: 8 10 | type: string 11 | jobs: 12 | Deploy: 13 | name: Deploy to OSSRH 14 | runs-on: ubuntu-latest 15 | env: 16 | VERTX_NEXUS_USERNAME: ${{ secrets.VERTX_NEXUS_USERNAME }} 17 | VERTX_NEXUS_PASSWORD: ${{ secrets.VERTX_NEXUS_PASSWORD }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | ref: ${{ inputs.branch }} 23 | - name: Install JDK 24 | uses: actions/setup-java@v2 25 | with: 26 | java-version: ${{ inputs.jdk }} 27 | distribution: temurin 28 | - name: Get project version 29 | run: echo "PROJECT_VERSION=$(mvn org.apache.maven.plugins:maven-help-plugin:evaluate -Dexpression=project.version -q -DforceStdout | grep -v '\[')" >> $GITHUB_ENV 30 | - name: Maven deploy 31 | if: ${{ endsWith(env.PROJECT_VERSION, '-SNAPSHOT') }} 32 | run: mvn deploy -s .github/maven-cd-settings.xml -DskipTests -B 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea 3 | *.iml 4 | 5 | .settings 6 | .project 7 | .classpath 8 | .vertx 9 | 10 | 11 | 12 | ### Java template 13 | *.class 14 | 15 | # Mobile Tools for Java (J2ME) 16 | .mtj.tmp/ 17 | 18 | # Package Files # 19 | *.jar 20 | *.war 21 | *.ear 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | ### Maven template 26 | target/ 27 | pom.xml.tag 28 | pom.xml.releaseBackup 29 | pom.xml.versionsBackup 30 | pom.xml.next 31 | release.properties 32 | dependency-reduced-pom.xml 33 | buildNumber.properties 34 | .mvn/timing.properties 35 | # Created by .ignore support plugin (hsz.mobi) 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://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 | http://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.adoc: -------------------------------------------------------------------------------- 1 | = Vert.x Circuit Breaker 2 | 3 | image:https://github.com/vert-x3/vertx-circuit-breaker/actions/workflows/ci-5.x.yml/badge.svg["Build Status (5.x)",link="https://github.com/vert-x3/vertx-circuit-breaker/actions/workflows/ci-5.x.yml"] 4 | image:https://github.com/vert-x3/vertx-circuit-breaker/actions/workflows/ci-4.x.yml/badge.svg["Build Status (4.x)",link="https://github.com/vert-x3/vertx-circuit-breaker/actions/workflows/ci-4.x.yml"] 5 | 6 | Vert.x Circuit Breaker is an implementation of the Circuit Breaker _pattern_ for Vert.x. More details on this pattern on https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern 7 | 8 | It keeps track of the 9 | number of failures and _opens the circuit_ when a threshold is reached. Optionally, a fallback is executed. 10 | 11 | Supported failures are: 12 | 13 | * failures reported by your code 14 | * exception thrown by your code 15 | * uncompleted futures (timeout) 16 | 17 | Operations guarded by a circuit breaker are intended to be non-blocking and asynchronous in order to benefit from 18 | the Vert.x execution model. 19 | 20 | == Using the Vert.x Circuit Breaker 21 | 22 | To use the Vert.x Circuit Breaker, add the following dependency to the _dependencies_ section of your build 23 | descriptor: 24 | 25 | * Maven (in your `pom.xml`): 26 | 27 | ---- 28 | 29 | io.vertx 30 | vertx-circuit-breaker 31 | $VERSION 32 | 33 | ---- 34 | 35 | * Gradle (in your `build.gradle` file): 36 | 37 | ---- 38 | compile 'io.vertx:vertx-circuit-breaker:$VERSION' 39 | ---- 40 | 41 | == Using the circuit breaker 42 | 43 | The manual is available from: 44 | 45 | * http://vertx.io/docs/vertx-circuit-breaker/java/[Java documentation] 46 | * http://vertx.io/docs/vertx-circuit-breaker/js/[JavaScript documentation] 47 | * http://vertx.io/docs/vertx-circuit-breaker/kotlin/[Kotlin documentation] 48 | * http://vertx.io/docs/vertx-circuit-breaker/groovy/[Groovy documentation] 49 | * http://vertx.io/docs/vertx-circuit-breaker/ruby/[Ruby documentation] 50 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 4.0.0 18 | 19 | 20 | io.vertx 21 | vertx5-parent 22 | 12 23 | 24 | 25 | vertx-circuit-breaker 26 | 5.1.0-SNAPSHOT 27 | 28 | 29 | scm:git:git@github.com:vert-x3/vertx-circuit-breaker.git 30 | scm:git:git@github.com:vert-x3/vertx-circuit-breaker.git 31 | git@github.com:vert-x3/vertx-circuit-breaker.git 32 | 33 | 34 | 35 | 36 | 37 | io.vertx 38 | vertx-dependencies 39 | ${project.version} 40 | pom 41 | import 42 | 43 | 44 | 45 | 46 | 47 | 48 | io.vertx 49 | vertx-core 50 | 51 | 52 | io.vertx 53 | vertx-codegen-api 54 | true 55 | 56 | 57 | io.vertx 58 | vertx-codegen-json 59 | true 60 | 61 | 62 | io.vertx 63 | vertx-docgen-api 64 | true 65 | 66 | 67 | 68 | 69 | org.hdrhistogram 70 | HdrHistogram 71 | 2.2.2 72 | true 73 | 74 | 75 | 76 | 77 | org.hamcrest 78 | hamcrest-core 79 | 3.0 80 | test 81 | 82 | 83 | junit 84 | junit 85 | 4.13.2 86 | test 87 | 88 | 89 | org.awaitility 90 | awaitility 91 | 4.2.0 92 | test 93 | 94 | 95 | io.vertx 96 | vertx-unit 97 | test 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | maven-compiler-plugin 106 | 107 | 108 | default-compile 109 | 110 | 111 | 112 | io.vertx 113 | vertx-codegen 114 | processor 115 | 116 | 117 | io.vertx 118 | vertx-docgen-processor 119 | processor 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | maven-assembly-plugin 131 | 132 | 133 | package-docs 134 | 135 | single 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/main/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = Vert.x Circuit Breaker 2 | 3 | Vert.x Circuit Breaker is an implementation of the _circuit breaker_ pattern for Vert.x. It keeps track of the 4 | number of recent failures and prevents further executions when a threshold is reached. Optionally, a fallback is executed. 5 | 6 | Supported failures are: 7 | 8 | * failures reported by your code in a {@link io.vertx.core.Future} 9 | * exception thrown by your code 10 | * uncompleted futures (timeout) 11 | 12 | Operations guarded by a circuit breaker are intended to be non-blocking and asynchronous in order to benefit from 13 | the Vert.x execution model. 14 | 15 | == Using Vert.x Circuit Breaker 16 | 17 | To use Vert.x Circuit Breaker, add the following dependency to the _dependencies_ section of your build 18 | descriptor: 19 | 20 | * Maven (in your `pom.xml`): 21 | 22 | [source,xml,subs="+attributes"] 23 | ---- 24 | 25 | io.vertx 26 | vertx-circuit-breaker 27 | ${maven.version} 28 | 29 | ---- 30 | 31 | * Gradle (in your `build.gradle` file): 32 | 33 | [source,groovy,subs="+attributes"] 34 | ---- 35 | compile 'io.vertx:vertx-circuit-breaker:${maven.version}' 36 | ---- 37 | 38 | == Using the circuit breaker 39 | 40 | To use the circuit breaker you need to: 41 | 42 | 1. Create a circuit breaker, with the configuration you want (timeout, failure threshold) 43 | 2. Execute some code using the circuit breaker 44 | 45 | **Important**: Don't recreate a circuit breaker on every call. A circuit breaker is a stateful entity. It is recommended 46 | to store the circuit breaker instance in a field. 47 | 48 | Here is an example: 49 | 50 | [source,$lang] 51 | ---- 52 | {@link examples.CircuitBreakerExamples#example1(io.vertx.core.Vertx)} 53 | ---- 54 | 55 | The executed block receives a {@link io.vertx.core.Promise} object as parameter, to denote the 56 | success or failure of the operation as well as the result. In the following example, the result is the 57 | output of a REST endpoint invocation: 58 | 59 | [source,$lang] 60 | ---- 61 | {@link examples.CircuitBreakerExamples#example2(io.vertx.core.Vertx)} 62 | ---- 63 | 64 | The result of the operation is provided using the: 65 | 66 | * returned {@link io.vertx.core.Future} when calling `execute` methods 67 | * provided {@link io.vertx.core.Promise} when calling the `executeAndReport` methods 68 | 69 | Optionally, you can provide a fallback which is executed when the circuit breaker is open: 70 | 71 | [source,$lang] 72 | ---- 73 | {@link examples.CircuitBreakerExamples#example3(io.vertx.core.Vertx)} 74 | ---- 75 | 76 | The fallback is called when the circuit breaker is open, or when 77 | {@link io.vertx.circuitbreaker.CircuitBreakerOptions#isFallbackOnFailure()} is enabled. When fallback is 78 | set, the overall result is obtained by calling the fallback function. The fallback function takes as parameter a 79 | {@link java.lang.Throwable} object and returns an object of the expected type. 80 | 81 | The fallback can also be set on the {@link io.vertx.circuitbreaker.CircuitBreaker} object directly: 82 | 83 | [source,$lang] 84 | ---- 85 | {@link examples.CircuitBreakerExamples#example4(io.vertx.core.Vertx)} 86 | ---- 87 | 88 | === Reported exceptions 89 | 90 | The fallback receives: 91 | 92 | * {@link io.vertx.circuitbreaker.OpenCircuitException} when the circuit breaker is open 93 | * {@link io.vertx.circuitbreaker.TimeoutException} when the operation timed out 94 | 95 | == Retries 96 | 97 | You can also specify how often the circuit breaker should execute your code before failing with {@link io.vertx.circuitbreaker.CircuitBreakerOptions#setMaxRetries(int)}. 98 | If you set this to something higher than 0, your code gets executed several times before finally failing in the last execution. 99 | If the code succeeds in one of the retries, your handler gets notified and no more retries occur. 100 | Retries are only supported when the circuit breaker is closed. 101 | 102 | NOTE: If you set `maxRetries` to 2, your operation may be called 3 times: the initial attempt and 2 retries. 103 | 104 | By default, the delay between retries is set to 0, which means that retries will be executed one after another immediately. 105 | This, however, will result in increased load on the called service and may delay its recovery. 106 | In order to mitigate this problem, it is recommended to execute retries with a delay. 107 | 108 | The {@link io.vertx.circuitbreaker.CircuitBreaker#retryPolicy(io.vertx.circuitbreaker.RetryPolicy)} method can be used to specify a retry policy. 109 | A retry policy is a function which receives the operation failure and retry count as arguments and returns a delay in milliseconds before retry should be executed. 110 | 111 | It allows to implement complex policies, e.g. using the value of the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After[`Retry-After`] header sent by an unavailable service. 112 | Some common policies are provided out of the box: {@link io.vertx.circuitbreaker.RetryPolicy#constantDelay}, {@link io.vertx.circuitbreaker.RetryPolicy#linearDelay} and {@link io.vertx.circuitbreaker.RetryPolicy#exponentialDelayWithJitter} 113 | 114 | Below is an example of exponential delay with jitter: 115 | 116 | [source,$lang] 117 | ---- 118 | {@link examples.CircuitBreakerExamples#example8(io.vertx.core.Vertx)} 119 | ---- 120 | 121 | == Failure Policy 122 | 123 | By default, the failure policy of a circuit breaker is to report a failure if the command doesn't complete successfully. 124 | Alternatively, you may configure the failure policy of the circuit breaker with {@link io.vertx.circuitbreaker.CircuitBreaker#failurePolicy}. 125 | This will let you specify the criteria in which an {@link io.vertx.core.AsyncResult} is treated as a failure by the circuit breaker. 126 | If you decide to override the failure policy, just be aware that it could allow failed results in the future provided in functions like `executeAndReport`. 127 | 128 | Below is an example of using a custom defined failure policy. 129 | 130 | [source,$lang] 131 | ---- 132 | {@link examples.CircuitBreakerExamples#example9(io.vertx.core.Vertx)} 133 | ---- 134 | 135 | == Callbacks 136 | 137 | You can also configure callbacks invoked when the circuit breaker is opened or closed: 138 | 139 | [source,$lang] 140 | ---- 141 | {@link examples.CircuitBreakerExamples#example5(io.vertx.core.Vertx)} 142 | ---- 143 | 144 | You can also be notified when the circuit breaker moves to the half-open state, in an attempt to reset. You can register 145 | such a callback with {@link io.vertx.circuitbreaker.CircuitBreaker#halfOpenHandler(io.vertx.core.Handler)}. 146 | 147 | == Event bus notification 148 | 149 | Every time the circuit breaker state changes, an event can be published on the event bus. 150 | 151 | To enable this feature, set the {@link io.vertx.circuitbreaker.CircuitBreakerOptions#setNotificationAddress(java.lang.String) notification address} to a value that is not `null`: 152 | 153 | [source,$lang] 154 | ---- 155 | {@link examples.CircuitBreakerExamples#enableNotifications} 156 | ---- 157 | 158 | The event contains circuit breaker metrics. 159 | Computing these metrics requires the following dependency to be added the _dependencies_ section of your build descriptor: 160 | 161 | * Maven (in your `pom.xml`): 162 | 163 | [source,xml,subs="+attributes"] 164 | ---- 165 | 166 | org.hdrhistogram 167 | HdrHistogram 168 | 2.1.12 169 | 170 | ---- 171 | 172 | * Gradle (in your `build.gradle` file): 173 | 174 | [source,groovy,subs="+attributes"] 175 | ---- 176 | compile 'org.hdrhistogram:HdrHistogram:2.1.12' 177 | ---- 178 | 179 | [NOTE] 180 | ==== 181 | When enabled, notifications are delivered only to local consumers by default. 182 | If the notification must be sent to all consumers in a cluster, you can change this behavior with {@link io.vertx.circuitbreaker.CircuitBreakerOptions#setNotificationLocalOnly}. 183 | ==== 184 | 185 | Each event contains a Json Object with: 186 | 187 | * `state`: the new circuit breaker state (`OPEN`, `CLOSED`, `HALF_OPEN`) 188 | * `name`: the name of the circuit breaker 189 | * `failures`: the number of failures 190 | * `node`: the identifier of the node (`local` if Vert.x is not running in cluster mode) 191 | * metrics 192 | 193 | == The half-open state 194 | 195 | When the circuit breaker is `open`, calls to the circuit breaker fail immediately, without any attempt to execute the real 196 | operation. After a suitable amount of time (configured by 197 | {@link io.vertx.circuitbreaker.CircuitBreakerOptions#setResetTimeout(long)}), the circuit breaker decides that the 198 | operation has a chance of succeeding, so it goes into the `half-open` state. In this state, the next call to the 199 | circuit breaker is allowed to execute the guarded operation. Should the call succeed, the circuit breaker resets 200 | and returns to the `closed` state, ready for more routine operation. If this trial call fails, however, the circuit 201 | breaker returns to the `open` state until another timeout elapses. 202 | 203 | == Using Resilience4j 204 | 205 | link:https://resilience4j.readme.io/[Resilience4j] is a popular library that implements common fault tolerance strategies: 206 | 207 | * bulkhead (concurrency limiter) 208 | * circuit breaker 209 | * rate limiter 210 | * retry 211 | * time limiter (timeout) 212 | 213 | A link:https://how-to.vertx.io/resilience4j-howto/[how-to] has been published that demonstrates the usage of Resilience4j with Vert.x. 214 | The link:https://github.com/vertx-howtos/resilience4j-howto[repository] of that how-to contains Vert.x adapters for all the fault tolerance strategies listed above. 215 | These adapters glue together the Resilience4j API and Vert.x ``Future``s. 216 | 217 | WARNING: Resilience4j 2.0 requires Java 17. 218 | -------------------------------------------------------------------------------- /src/main/generated/io/vertx/circuitbreaker/CircuitBreakerOptionsConverter.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.core.json.JsonArray; 5 | import java.time.Instant; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | /** 9 | * Converter and mapper for {@link io.vertx.circuitbreaker.CircuitBreakerOptions}. 10 | * NOTE: This class has been automatically generated from the {@link io.vertx.circuitbreaker.CircuitBreakerOptions} original class using Vert.x codegen. 11 | */ 12 | public class CircuitBreakerOptionsConverter { 13 | 14 | static void fromJson(Iterable> json, CircuitBreakerOptions obj) { 15 | for (java.util.Map.Entry member : json) { 16 | switch (member.getKey()) { 17 | case "maxFailures": 18 | if (member.getValue() instanceof Number) { 19 | obj.setMaxFailures(((Number)member.getValue()).intValue()); 20 | } 21 | break; 22 | case "timeout": 23 | if (member.getValue() instanceof Number) { 24 | obj.setTimeout(((Number)member.getValue()).longValue()); 25 | } 26 | break; 27 | case "fallbackOnFailure": 28 | if (member.getValue() instanceof Boolean) { 29 | obj.setFallbackOnFailure((Boolean)member.getValue()); 30 | } 31 | break; 32 | case "resetTimeout": 33 | if (member.getValue() instanceof Number) { 34 | obj.setResetTimeout(((Number)member.getValue()).longValue()); 35 | } 36 | break; 37 | case "notificationLocalOnly": 38 | if (member.getValue() instanceof Boolean) { 39 | obj.setNotificationLocalOnly((Boolean)member.getValue()); 40 | } 41 | break; 42 | case "notificationAddress": 43 | if (member.getValue() instanceof String) { 44 | obj.setNotificationAddress((String)member.getValue()); 45 | } 46 | break; 47 | case "notificationPeriod": 48 | if (member.getValue() instanceof Number) { 49 | obj.setNotificationPeriod(((Number)member.getValue()).longValue()); 50 | } 51 | break; 52 | case "metricsRollingWindow": 53 | if (member.getValue() instanceof Number) { 54 | obj.setMetricsRollingWindow(((Number)member.getValue()).longValue()); 55 | } 56 | break; 57 | case "failuresRollingWindow": 58 | if (member.getValue() instanceof Number) { 59 | obj.setFailuresRollingWindow(((Number)member.getValue()).longValue()); 60 | } 61 | break; 62 | case "metricsRollingBuckets": 63 | if (member.getValue() instanceof Number) { 64 | obj.setMetricsRollingBuckets(((Number)member.getValue()).intValue()); 65 | } 66 | break; 67 | case "maxRetries": 68 | if (member.getValue() instanceof Number) { 69 | obj.setMaxRetries(((Number)member.getValue()).intValue()); 70 | } 71 | break; 72 | } 73 | } 74 | } 75 | 76 | static void toJson(CircuitBreakerOptions obj, JsonObject json) { 77 | toJson(obj, json.getMap()); 78 | } 79 | 80 | static void toJson(CircuitBreakerOptions obj, java.util.Map json) { 81 | json.put("maxFailures", obj.getMaxFailures()); 82 | json.put("timeout", obj.getTimeout()); 83 | json.put("fallbackOnFailure", obj.isFallbackOnFailure()); 84 | json.put("resetTimeout", obj.getResetTimeout()); 85 | json.put("notificationLocalOnly", obj.isNotificationLocalOnly()); 86 | if (obj.getNotificationAddress() != null) { 87 | json.put("notificationAddress", obj.getNotificationAddress()); 88 | } 89 | json.put("notificationPeriod", obj.getNotificationPeriod()); 90 | json.put("metricsRollingWindow", obj.getMetricsRollingWindow()); 91 | json.put("failuresRollingWindow", obj.getFailuresRollingWindow()); 92 | json.put("metricsRollingBuckets", obj.getMetricsRollingBuckets()); 93 | json.put("maxRetries", obj.getMaxRetries()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/examples/CircuitBreakerExamples.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package examples; 18 | 19 | import io.vertx.circuitbreaker.CircuitBreaker; 20 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 21 | import io.vertx.circuitbreaker.RetryPolicy; 22 | import io.vertx.core.Future; 23 | import io.vertx.core.Vertx; 24 | import io.vertx.core.buffer.Buffer; 25 | import io.vertx.core.http.HttpClientResponse; 26 | import io.vertx.core.http.HttpMethod; 27 | 28 | /** 29 | * @author Clement Escoffier 30 | */ 31 | public class CircuitBreakerExamples { 32 | 33 | public void example1(Vertx vertx) { 34 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 35 | new CircuitBreakerOptions() 36 | .setMaxFailures(5) // number of failures before opening the circuit breaker 37 | .setTimeout(2000) // considered a failure if the operation does not succeed in time 38 | .setFallbackOnFailure(true) // call the fallback on failure 39 | .setResetTimeout(10000) // time spent in open state before attempting to retry 40 | ); 41 | 42 | // --- 43 | // Store the circuit breaker in a field and access it as follows 44 | // --- 45 | 46 | breaker.execute(promise -> { 47 | // some code executing with the circuit breaker 48 | // the code reports failures or success on the given promise 49 | // if this promise is marked as failed, the circuit breaker 50 | // increases the number of failures 51 | }).onComplete(ar -> { 52 | // Get the operation result. 53 | }); 54 | } 55 | 56 | public void example2(Vertx vertx) { 57 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 58 | new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) 59 | ); 60 | 61 | // --- 62 | // Store the circuit breaker in a field and access it as follows 63 | // --- 64 | 65 | breaker.execute(promise -> { 66 | vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") 67 | .compose(req -> req 68 | .send() 69 | .compose(resp -> { 70 | if (resp.statusCode() != 200) { 71 | return Future.failedFuture("HTTP error"); 72 | } else { 73 | return resp.body().map(Buffer::toString); 74 | } 75 | })) 76 | .onComplete(promise); 77 | }).onComplete(ar -> { 78 | // Do something with the result 79 | }); 80 | } 81 | 82 | public void example3(Vertx vertx) { 83 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 84 | new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) 85 | ); 86 | 87 | // --- 88 | // Store the circuit breaker in a field and access it as follows 89 | // --- 90 | 91 | breaker.executeWithFallback(promise -> { 92 | vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") 93 | .compose(req -> req 94 | .send() 95 | .compose(resp -> { 96 | if (resp.statusCode() != 200) { 97 | return Future.failedFuture("HTTP error"); 98 | } else { 99 | return resp.body().map(Buffer::toString); 100 | } 101 | })) 102 | .onComplete(promise); 103 | }, v -> { 104 | // Executed when the circuit breaker is open 105 | return "Hello"; 106 | }).onComplete(ar -> { 107 | // Do something with the result 108 | }); 109 | } 110 | 111 | public void example4(Vertx vertx) { 112 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 113 | new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) 114 | ).fallback(v -> { 115 | // Executed when the circuit breaker is open. 116 | return "hello"; 117 | }); 118 | 119 | breaker.execute(promise -> { 120 | vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") 121 | .compose(req -> req 122 | .send() 123 | .compose(resp -> { 124 | if (resp.statusCode() != 200) { 125 | return Future.failedFuture("HTTP error"); 126 | } else { 127 | return resp.body().map(Buffer::toString); 128 | } 129 | })) 130 | .onComplete(promise); 131 | }); 132 | } 133 | 134 | public void example5(Vertx vertx) { 135 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 136 | new CircuitBreakerOptions().setMaxFailures(5).setTimeout(2000) 137 | ).openHandler(v -> { 138 | System.out.println("Circuit breaker opened"); 139 | }).closeHandler(v -> { 140 | System.out.println("Circuit breaker closed"); 141 | }); 142 | 143 | breaker.execute(promise -> { 144 | vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") 145 | .compose(req -> req 146 | .send() 147 | .compose(resp -> { 148 | if (resp.statusCode() != 200) { 149 | return Future.failedFuture("HTTP error"); 150 | } else { 151 | return resp.body().map(Buffer::toString); 152 | } 153 | })) 154 | .onComplete(promise); 155 | }); 156 | } 157 | 158 | public void example8(Vertx vertx) { 159 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 160 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(5).setTimeout(2000) 161 | ).retryPolicy(RetryPolicy.exponentialDelayWithJitter(50, 500)); 162 | 163 | breaker.execute(promise -> { 164 | vertx.createHttpClient().request(HttpMethod.GET, 8080, "localhost", "/") 165 | .compose(req -> req 166 | .send() 167 | .compose(resp -> { 168 | if (resp.statusCode() != 200) { 169 | return Future.failedFuture("HTTP error"); 170 | } else { 171 | return resp.body().map(Buffer::toString); 172 | } 173 | })) 174 | .onComplete(promise); 175 | }); 176 | } 177 | 178 | public void example9(Vertx vertx) { 179 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx); 180 | breaker.failurePolicy(ar -> { 181 | // A failure will be either a failed operation or a response with a status code other than 200 182 | if (ar.failed()) { 183 | return true; 184 | } 185 | HttpClientResponse resp = ar.result(); 186 | return resp.statusCode() != 200; 187 | }); 188 | 189 | Future future = breaker.execute(promise -> { 190 | vertx.createHttpClient() 191 | .request(HttpMethod.GET, 8080, "localhost", "/") 192 | .compose(request -> request.send() 193 | // Complete when the body is fully received 194 | .compose(response -> response.body().map(response))) 195 | .onComplete(promise); 196 | }); 197 | } 198 | 199 | public void enableNotifications(CircuitBreakerOptions options) { 200 | options.setNotificationAddress(CircuitBreakerOptions.DEFAULT_NOTIFICATION_ADDRESS); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/examples/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | @Source 18 | package examples; 19 | 20 | import io.vertx.docgen.Source; -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/CircuitBreaker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.vertx.circuitbreaker; 18 | 19 | import io.vertx.circuitbreaker.impl.CircuitBreakerImpl; 20 | import io.vertx.codegen.annotations.CacheReturn; 21 | import io.vertx.codegen.annotations.Fluent; 22 | import io.vertx.codegen.annotations.VertxGen; 23 | import io.vertx.core.Future; 24 | import io.vertx.core.Handler; 25 | import io.vertx.core.Promise; 26 | import io.vertx.core.Vertx; 27 | 28 | import java.util.function.Function; 29 | import java.util.function.Supplier; 30 | 31 | /** 32 | * An implementation of the circuit breaker pattern for Vert.x 33 | * 34 | * @author Clement Escoffier 35 | */ 36 | @VertxGen 37 | public interface CircuitBreaker { 38 | 39 | /** 40 | * Creates a new instance of {@link CircuitBreaker}. 41 | * 42 | * @param name the name 43 | * @param vertx the Vert.x instance 44 | * @param options the configuration options 45 | * @return the created instance 46 | */ 47 | static CircuitBreaker create(String name, Vertx vertx, CircuitBreakerOptions options) { 48 | return new CircuitBreakerImpl(name, vertx, options == null ? new CircuitBreakerOptions() : options); 49 | } 50 | 51 | /** 52 | * Creates a new instance of {@link CircuitBreaker}, with default options. 53 | * 54 | * @param name the name 55 | * @param vertx the Vert.x instance 56 | * @return the created instance 57 | */ 58 | static CircuitBreaker create(String name, Vertx vertx) { 59 | return new CircuitBreakerImpl(name, vertx, new CircuitBreakerOptions()); 60 | } 61 | 62 | /** 63 | * Closes the circuit breaker. It stops sending events on its state on the event bus. 64 | *

65 | * This method is not related to the {@code closed} state of the circuit breaker. To move the circuit breaker to the 66 | * {@code closed} state, use {@link #reset()}. 67 | */ 68 | @Fluent 69 | CircuitBreaker close(); 70 | 71 | /** 72 | * Sets a {@link Handler} to be invoked when the circuit breaker state switches to open. 73 | * 74 | * @param handler the handler, must not be {@code null} 75 | * @return this {@link CircuitBreaker} 76 | */ 77 | @Fluent 78 | CircuitBreaker openHandler(Handler handler); 79 | 80 | /** 81 | * Sets a {@link Handler} to be invoked when the circuit breaker state switches to half-open. 82 | * 83 | * @param handler the handler, must not be {@code null} 84 | * @return this {@link CircuitBreaker} 85 | */ 86 | @Fluent 87 | CircuitBreaker halfOpenHandler(Handler handler); 88 | 89 | /** 90 | * Sets a {@link Handler} to be invoked when the circuit breaker state switches to closed. 91 | * 92 | * @param handler the handler, must not be {@code null} 93 | * @return this {@link CircuitBreaker} 94 | */ 95 | @Fluent 96 | CircuitBreaker closeHandler(Handler handler); 97 | 98 | /** 99 | * Executes the given operation with the circuit breaker control. The operation is generally calling an 100 | * external system. The operation receives a {@link Promise} object as parameter and must 101 | * call {@link Promise#complete(Object)} when the operation has terminated successfully. The operation must also 102 | * call {@link Promise#fail(Throwable)} in case of a failure. 103 | *

104 | * The operation is not invoked if the circuit breaker is open, and the given fallback is called instead. 105 | * The circuit breaker also monitors whether the operation completes in time. The operation is considered failed 106 | * if it does not terminate before the configured timeout. 107 | *

108 | * This method returns a {@link Future} object to retrieve the status and result of the operation, with the status 109 | * being a success or a failure. If the fallback is called, the returned future is successfully completed with the 110 | * value returned from the fallback. If the fallback throws an exception, the returned future is marked as failed. 111 | * 112 | * @param command the operation 113 | * @param fallback the fallback function; gets an exception as parameter and returns the fallback result 114 | * @param the type of result 115 | * @return a future object completed when the operation or the fallback completes 116 | */ 117 | Future executeWithFallback(Handler> command, Function fallback); 118 | 119 | /** 120 | * Executes the given operation with the circuit breaker control. The operation is generally calling an 121 | * external system. The operation receives a {@link Promise} object as parameter and must 122 | * call {@link Promise#complete(Object)} when the operation has terminated successfully. The operation must also 123 | * call {@link Promise#fail(Throwable)} in case of a failure. 124 | *

125 | * The operation is not invoked if the circuit breaker is open, and the given fallback is called instead. 126 | * The circuit breaker also monitors whether the operation completes in time. The operation is considered failed 127 | * if it does not terminate before the configured timeout. 128 | *

129 | * This method returns a {@link Future} object to retrieve the status and result of the operation, with the status 130 | * being a success or a failure. If the fallback is called, the returned future is successfully completed with the 131 | * value returned from the fallback. If the fallback throws an exception, the returned future is marked as failed. 132 | * 133 | * @param command the operation 134 | * @param fallback the fallback function; gets an exception as parameter and returns the fallback result 135 | * @param the type of result 136 | * @return a future object completed when the operation or the fallback completes 137 | */ 138 | Future executeWithFallback(Supplier> command, Function fallback); 139 | 140 | /** 141 | * Same as {@link #executeWithFallback(Handler, Function)} but using the circuit breaker 142 | * {@linkplain #fallback(Function) default fallback}. 143 | * 144 | * @param command the operation 145 | * @param the type of result 146 | * @return a future object completed when the operation or its fallback completes 147 | */ 148 | Future execute(Handler> command); 149 | 150 | /** 151 | * Same as {@link #executeWithFallback(Supplier, Function)} but using the circuit breaker 152 | * {@linkplain #fallback(Function) default fallback}. 153 | * 154 | * @param command the operation 155 | * @param the type of result 156 | * @return a future object completed when the operation or its fallback completes 157 | */ 158 | Future execute(Supplier> command); 159 | 160 | /** 161 | * Same as {@link #executeAndReportWithFallback(Promise, Handler, Function)} but using the circuit breaker 162 | * {@linkplain #fallback(Function) default fallback}. 163 | * 164 | * @param resultPromise the promise on which the operation result is reported 165 | * @param command the operation 166 | * @param the type of result 167 | * @return this {@link CircuitBreaker} 168 | */ 169 | @Fluent 170 | CircuitBreaker executeAndReport(Promise resultPromise, Handler> command); 171 | 172 | /** 173 | * Executes the given operation with the circuit breaker control. The operation is generally calling an 174 | * external system. The operation receives a {@link Promise} object as parameter and must 175 | * call {@link Promise#complete(Object)} when the operation has terminated successfully. The operation must also 176 | * call {@link Promise#fail(Throwable)} in case of a failure. 177 | *

178 | * The operation is not invoked if the circuit breaker is open, and the given fallback is called instead. 179 | * The circuit breaker also monitors whether the operation completes in time. The operation is considered failed 180 | * if it does not terminate before the configured timeout. 181 | *

182 | * Unlike {@link #executeWithFallback(Handler, Function)}, this method does not return a {@link Future} object, but 183 | * lets the caller pass a {@link Promise} object on which the result is reported. If the fallback is called, the promise 184 | * is successfully completed with the value returned by the fallback function. If the fallback throws an exception, 185 | * the promise is marked as failed. 186 | * 187 | * @param resultPromise the promise on which the operation result is reported 188 | * @param command the operation 189 | * @param fallback the fallback function; gets an exception as parameter and returns the fallback result 190 | * @param the type of result 191 | * @return this {@link CircuitBreaker} 192 | */ 193 | @Fluent 194 | CircuitBreaker executeAndReportWithFallback(Promise resultPromise, Handler> command, 195 | Function fallback); 196 | 197 | /** 198 | * Sets a default fallback {@link Function} to be invoked when the circuit breaker is open or when failure 199 | * occurs and {@link CircuitBreakerOptions#isFallbackOnFailure()} is enabled. 200 | *

201 | * The function gets the exception as parameter and returns the fallback result. 202 | * 203 | * @param handler the fallback handler 204 | * @return this {@link CircuitBreaker} 205 | */ 206 | @Fluent 207 | CircuitBreaker fallback(Function handler); 208 | 209 | /** 210 | * Configures the failure policy for this circuit-breaker. 211 | * 212 | * @return the current {@link CircuitBreaker} 213 | * @see FailurePolicy 214 | */ 215 | @Fluent 216 | default CircuitBreaker failurePolicy(FailurePolicy failurePolicy) { 217 | return this; 218 | } 219 | 220 | /** 221 | * Resets the circuit breaker state. The number of recent failures is set to 0 and if the state is half-open, 222 | * it is set to closed. 223 | * 224 | * @return this {@link CircuitBreaker} 225 | */ 226 | @Fluent 227 | CircuitBreaker reset(); 228 | 229 | /** 230 | * Explicitly opens the circuit breaker. 231 | * 232 | * @return this {@link CircuitBreaker} 233 | */ 234 | @Fluent 235 | CircuitBreaker open(); 236 | 237 | /** 238 | * @return the current state of this circuit breaker 239 | */ 240 | CircuitBreakerState state(); 241 | 242 | /** 243 | * @return the current number of recorded failures 244 | */ 245 | long failureCount(); 246 | 247 | /** 248 | * @return the name of this circuit breaker 249 | */ 250 | @CacheReturn 251 | String name(); 252 | 253 | /** 254 | * Set a {@link RetryPolicy} which computes a delay before a retry attempt. 255 | */ 256 | @Fluent 257 | CircuitBreaker retryPolicy(RetryPolicy retryPolicy); 258 | } 259 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/CircuitBreakerOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.vertx.circuitbreaker; 18 | 19 | import io.vertx.codegen.annotations.DataObject; 20 | import io.vertx.codegen.json.annotations.JsonGen; 21 | import io.vertx.core.json.JsonObject; 22 | 23 | /** 24 | * Circuit breaker configuration options. All time values are in milliseconds. 25 | * 26 | * @author Clement Escoffier 27 | */ 28 | @DataObject 29 | @JsonGen(publicConverter = false) 30 | public class CircuitBreakerOptions { 31 | 32 | /** 33 | * Default timeout in milliseconds. 34 | */ 35 | public static final long DEFAULT_TIMEOUT = 10_000L; 36 | 37 | /** 38 | * Default number of failures after which a closed circuit breaker moves to open. 39 | */ 40 | public static final int DEFAULT_MAX_FAILURES = 5; 41 | 42 | /** 43 | * Default value of the {@linkplain #isFallbackOnFailure() fallback on failure} property. 44 | */ 45 | public static final boolean DEFAULT_FALLBACK_ON_FAILURE = false; 46 | 47 | /** 48 | * Default time after which an open circuit breaker moves to half-open (in an attempt to re-close) in milliseconds. 49 | */ 50 | public static final long DEFAULT_RESET_TIMEOUT = 30_000; 51 | 52 | /** 53 | * Default value of whether circuit breaker state events should be delivered only to local consumers. 54 | */ 55 | public static final boolean DEFAULT_NOTIFICATION_LOCAL_ONLY = true; 56 | 57 | /** 58 | * A default address on which the circuit breakers can send their updates. 59 | */ 60 | public static final String DEFAULT_NOTIFICATION_ADDRESS = "vertx.circuit-breaker"; 61 | 62 | /** 63 | * Default notification period in milliseconds. 64 | */ 65 | public static final long DEFAULT_NOTIFICATION_PERIOD = 2_000; 66 | 67 | /** 68 | * Default length of rolling window for metrics in milliseconds. 69 | */ 70 | public static final long DEFAULT_METRICS_ROLLING_WINDOW = 10_000; 71 | 72 | /** 73 | * Default number of buckets used for the metrics rolling window. 74 | */ 75 | public static final int DEFAULT_METRICS_ROLLING_BUCKETS = 10; 76 | 77 | /** 78 | * Default number of retries. 79 | */ 80 | private static final int DEFAULT_MAX_RETRIES = 0; 81 | 82 | /** 83 | * Default length of rolling window for failures in milliseconds. 84 | */ 85 | private static final int DEFAULT_FAILURES_ROLLING_WINDOW = 10_000; 86 | 87 | /** 88 | * The operation timeout. 89 | */ 90 | private long timeout = DEFAULT_TIMEOUT; 91 | 92 | /** 93 | * The max failures. 94 | */ 95 | private int maxFailures = DEFAULT_MAX_FAILURES; 96 | 97 | /** 98 | * Whether the fallback should be called upon failures. 99 | */ 100 | private boolean fallbackOnFailure = DEFAULT_FALLBACK_ON_FAILURE; 101 | 102 | /** 103 | * The reset timeout. 104 | */ 105 | private long resetTimeout = DEFAULT_RESET_TIMEOUT; 106 | 107 | /** 108 | * Whether circuit breaker state should be delivered only to local consumers. 109 | */ 110 | private boolean notificationLocalOnly = DEFAULT_NOTIFICATION_LOCAL_ONLY; 111 | 112 | /** 113 | * The event bus address on which the circuit breaker state is published. 114 | */ 115 | private String notificationAddress = null; 116 | 117 | /** 118 | * The state publication period in ms. 119 | */ 120 | private long notificationPeriod = DEFAULT_NOTIFICATION_PERIOD; 121 | 122 | /** 123 | * The number of retries 124 | */ 125 | private int maxRetries = DEFAULT_MAX_RETRIES; 126 | 127 | /** 128 | * The metric rolling window in ms. 129 | */ 130 | private long metricsRollingWindow = DEFAULT_METRICS_ROLLING_WINDOW; 131 | 132 | /** 133 | * The number of buckets used for the metric rolling window. 134 | */ 135 | private int metricsRollingBuckets = DEFAULT_METRICS_ROLLING_BUCKETS; 136 | 137 | /** 138 | * The failure rolling window in ms. 139 | */ 140 | private long failuresRollingWindow = DEFAULT_FAILURES_ROLLING_WINDOW; 141 | 142 | /** 143 | * Creates a new instance of {@link CircuitBreakerOptions} using the default values. 144 | */ 145 | public CircuitBreakerOptions() { 146 | // Empty constructor 147 | } 148 | 149 | /** 150 | * Creates a new instance of {@link CircuitBreakerOptions} by copying the other instance. 151 | * 152 | * @param other the instance fo copy 153 | */ 154 | public CircuitBreakerOptions(CircuitBreakerOptions other) { 155 | this.timeout = other.timeout; 156 | this.maxFailures = other.maxFailures; 157 | this.fallbackOnFailure = other.fallbackOnFailure; 158 | this.notificationLocalOnly = other.notificationLocalOnly; 159 | this.notificationAddress = other.notificationAddress; 160 | this.notificationPeriod = other.notificationPeriod; 161 | this.resetTimeout = other.resetTimeout; 162 | this.maxRetries = other.maxRetries; 163 | this.metricsRollingBuckets = other.metricsRollingBuckets; 164 | this.metricsRollingWindow = other.metricsRollingWindow; 165 | this.failuresRollingWindow = other.failuresRollingWindow; 166 | } 167 | 168 | /** 169 | * Creates a new instance of {@link CircuitBreakerOptions} from the given JSON object. 170 | * 171 | * @param json the JSON object 172 | */ 173 | public CircuitBreakerOptions(JsonObject json) { 174 | this(); 175 | CircuitBreakerOptionsConverter.fromJson(json, this); 176 | } 177 | 178 | /** 179 | * @return a JSON object representing this configuration 180 | */ 181 | public JsonObject toJson() { 182 | JsonObject json = new JsonObject(); 183 | CircuitBreakerOptionsConverter.toJson(this, json); 184 | return json; 185 | } 186 | 187 | /** 188 | * @return the maximum number of failures before opening the circuit breaker 189 | */ 190 | public int getMaxFailures() { 191 | return maxFailures; 192 | } 193 | 194 | /** 195 | * Sets the maximum number of failures before opening the circuit breaker. 196 | * 197 | * @param maxFailures the number of failures. 198 | * @return this {@link CircuitBreakerOptions} 199 | */ 200 | public CircuitBreakerOptions setMaxFailures(int maxFailures) { 201 | this.maxFailures = maxFailures; 202 | return this; 203 | } 204 | 205 | /** 206 | * @return the configured timeout in milliseconds 207 | */ 208 | public long getTimeout() { 209 | return timeout; 210 | } 211 | 212 | /** 213 | * Sets the timeout in milliseconds. If an action does not complete before this timeout, the action is considered as 214 | * a failure. 215 | * 216 | * @param timeoutInMs the timeout, -1 to disable the timeout 217 | * @return this {@link CircuitBreakerOptions} 218 | */ 219 | public CircuitBreakerOptions setTimeout(long timeoutInMs) { 220 | this.timeout = timeoutInMs; 221 | return this; 222 | } 223 | 224 | /** 225 | * @return whether the fallback is executed on failures, even when the circuit breaker is closed 226 | */ 227 | public boolean isFallbackOnFailure() { 228 | return fallbackOnFailure; 229 | } 230 | 231 | /** 232 | * Sets whether the fallback is executed on failure, even when the circuit breaker is closed. 233 | * 234 | * @param fallbackOnFailure {@code true} to enable it. 235 | * @return this {@link CircuitBreakerOptions} 236 | */ 237 | public CircuitBreakerOptions setFallbackOnFailure(boolean fallbackOnFailure) { 238 | this.fallbackOnFailure = fallbackOnFailure; 239 | return this; 240 | } 241 | 242 | /** 243 | * @return the time in milliseconds before an open circuit breaker moves to half-open (in an attempt to re-close) 244 | */ 245 | public long getResetTimeout() { 246 | return resetTimeout; 247 | } 248 | 249 | /** 250 | * Sets the time in milliseconds before an open circuit breaker moves to half-open (in an attempt to re-close). 251 | * If the circuit breaker is closed when the timeout is reached, nothing happens. {@code -1} disables this feature. 252 | * 253 | * @param resetTimeout the time in ms 254 | * @return this {@link CircuitBreakerOptions} 255 | */ 256 | public CircuitBreakerOptions setResetTimeout(long resetTimeout) { 257 | this.resetTimeout = resetTimeout; 258 | return this; 259 | } 260 | 261 | /** 262 | * @return {@code true} if circuit breaker state events should be delivered only to local consumers, 263 | * {@code false} otherwise 264 | */ 265 | public boolean isNotificationLocalOnly() { 266 | return notificationLocalOnly; 267 | } 268 | 269 | /** 270 | * Sets whether circuit breaker state events should be delivered only to local consumers. 271 | * 272 | * @param notificationLocalOnly {@code true} if circuit breaker state events should be delivered only to local consumers, {@code false} otherwise 273 | * @return this {@link CircuitBreakerOptions} 274 | */ 275 | public CircuitBreakerOptions setNotificationLocalOnly(boolean notificationLocalOnly) { 276 | this.notificationLocalOnly = notificationLocalOnly; 277 | return this; 278 | } 279 | 280 | /** 281 | * @return the eventbus address on which the circuit breaker events are published, or {@code null} if this feature has 282 | * been disabled 283 | */ 284 | public String getNotificationAddress() { 285 | return notificationAddress; 286 | } 287 | 288 | /** 289 | * Sets the event bus address on which the circuit breaker publishes its state changes. 290 | * 291 | * @param notificationAddress the address, {@code null} to disable this feature 292 | * @return this {@link CircuitBreakerOptions} 293 | */ 294 | public CircuitBreakerOptions setNotificationAddress(String notificationAddress) { 295 | this.notificationAddress = notificationAddress; 296 | return this; 297 | } 298 | 299 | /** 300 | * @return the period in milliseconds in which the circuit breaker sends notifications about its state 301 | */ 302 | public long getNotificationPeriod() { 303 | return notificationPeriod; 304 | } 305 | 306 | /** 307 | * Sets the period in milliseconds in which the circuit breaker sends notifications on the event bus with its 308 | * current state. 309 | * 310 | * @param notificationPeriod the period, 0 to disable this feature. 311 | * @return this {@link CircuitBreakerOptions} 312 | */ 313 | public CircuitBreakerOptions setNotificationPeriod(long notificationPeriod) { 314 | this.notificationPeriod = notificationPeriod; 315 | return this; 316 | } 317 | 318 | /** 319 | * @return the configured length of rolling window for metrics 320 | */ 321 | public long getMetricsRollingWindow() { 322 | return metricsRollingWindow; 323 | } 324 | 325 | /** 326 | * Sets the rolling window length used for metrics. 327 | * 328 | * @param metricsRollingWindow the period in milliseconds 329 | * @return this {@link CircuitBreakerOptions} 330 | */ 331 | public CircuitBreakerOptions setMetricsRollingWindow(long metricsRollingWindow) { 332 | this.metricsRollingWindow = metricsRollingWindow; 333 | return this; 334 | } 335 | 336 | /** 337 | * @return the configured length of rolling window for failures 338 | */ 339 | public long getFailuresRollingWindow() { 340 | return failuresRollingWindow; 341 | } 342 | 343 | /** 344 | * Sets the rolling window length used for failures. 345 | * 346 | * @param failureRollingWindow the period in milliseconds 347 | * @return this {@link CircuitBreakerOptions} 348 | */ 349 | public CircuitBreakerOptions setFailuresRollingWindow(long failureRollingWindow) { 350 | this.failuresRollingWindow = failureRollingWindow; 351 | return this; 352 | } 353 | 354 | /** 355 | * @return the configured number of buckets the metrics rolling window is divided into 356 | */ 357 | public int getMetricsRollingBuckets() { 358 | return metricsRollingBuckets; 359 | } 360 | 361 | /** 362 | * Sets the number of buckets the metrics rolling window is divided into. 363 | *

364 | * The following must be true: {@code metricsRollingWindow % metricsRollingBuckets == 0}, 365 | * otherwise an exception will be thrown. 366 | * For example, 10000/10 is okay, so is 10000/20, but 10000/7 is not. 367 | * 368 | * @param metricsRollingBuckets the number of buckets 369 | * @return this {@link CircuitBreakerOptions} 370 | */ 371 | public CircuitBreakerOptions setMetricsRollingBuckets(int metricsRollingBuckets) { 372 | this.metricsRollingBuckets = metricsRollingBuckets; 373 | return this; 374 | } 375 | 376 | /** 377 | * @return the number of times the circuit breaker retries an operation before failing 378 | */ 379 | public int getMaxRetries() { 380 | return maxRetries; 381 | } 382 | 383 | /** 384 | * Sets the number of times the circuit breaker retries an operation before failing. 385 | * 386 | * @param maxRetries the number of retries, 0 to disable retrying 387 | * @return this {@link CircuitBreakerOptions} 388 | */ 389 | public CircuitBreakerOptions setMaxRetries(int maxRetries) { 390 | this.maxRetries = maxRetries; 391 | return this; 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/CircuitBreakerState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.vertx.circuitbreaker; 18 | 19 | import io.vertx.codegen.annotations.VertxGen; 20 | 21 | /** 22 | * Circuit breaker states. 23 | * 24 | * @author Clement Escoffier 25 | */ 26 | @VertxGen 27 | public enum CircuitBreakerState { 28 | /** 29 | * The {@code OPEN} state. The circuit breaker is executing the fallback, and switches to the {@link #HALF_OPEN} 30 | * state after the specified time. 31 | */ 32 | OPEN, 33 | /** 34 | * The {@code CLOSED} state. The circuit breaker lets invocations pass and collects the failures. If the number of 35 | * failures reach the specified threshold, the circuit breaker switches to the {@link #OPEN} state. 36 | */ 37 | CLOSED, 38 | /** 39 | * The {@code HALF_OPEN} state. The circuit breaker has been opened, and is now checking the current situation. It 40 | * lets the next invocation pass and determines from the result (failure or success) if the circuit breaker can 41 | * be switched to the {@link #CLOSED} state again. 42 | */ 43 | HALF_OPEN 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/FailurePolicy.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker; 2 | 3 | import io.vertx.codegen.annotations.VertxGen; 4 | import io.vertx.core.AsyncResult; 5 | import io.vertx.core.Future; 6 | 7 | import java.util.function.Predicate; 8 | 9 | /** 10 | * A failure policy for the {@link CircuitBreaker}. 11 | *

12 | * The default policy is to consider an asynchronous result as a failure if {@link AsyncResult#failed()} returns {@code true}. 13 | * Nevertheless, sometimes this is not good enough. For example, an HTTP Client could return a response, but with an unexpected status code. 14 | *

15 | * In this case, a custom failure policy can be configured with {@link CircuitBreaker#failurePolicy(FailurePolicy)}. 16 | */ 17 | @VertxGen 18 | public interface FailurePolicy extends Predicate> { 19 | 20 | /** 21 | * The default policy, which considers an asynchronous result as a failure if {@link AsyncResult#failed()} returns {@code true}. 22 | */ 23 | static FailurePolicy defaultPolicy() { 24 | return AsyncResult::failed; 25 | } 26 | 27 | /** 28 | * Invoked by the {@link CircuitBreaker} when an operation completes. 29 | * 30 | * @param future a completed future 31 | * @return {@code true} if the asynchronous result should be considered as a failure, {@code false} otherwise 32 | */ 33 | @Override 34 | boolean test(Future future); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/OpenCircuitException.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker; 2 | 3 | /** 4 | * Exception reported when the circuit breaker is open. 5 | *

6 | * For performance reason, this exception does not carry a stack trace. You are not allowed to set a stack trace or a 7 | * cause to this exception. This immutability allows using a singleton instance. 8 | * 9 | * @author Clement Escoffier 10 | */ 11 | public class OpenCircuitException extends RuntimeException { 12 | 13 | public static OpenCircuitException INSTANCE = new OpenCircuitException(); 14 | 15 | private OpenCircuitException() { 16 | super("open circuit", null, false, false); 17 | } 18 | 19 | @Override 20 | public void setStackTrace(StackTraceElement[] stackTrace) { 21 | throw new UnsupportedOperationException(); 22 | } 23 | 24 | @Override 25 | public synchronized Throwable initCause(Throwable cause) { 26 | throw new UnsupportedOperationException(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/RetryPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | * which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | * 9 | * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | */ 11 | 12 | package io.vertx.circuitbreaker; 13 | 14 | import io.vertx.codegen.annotations.VertxGen; 15 | 16 | import java.util.concurrent.ThreadLocalRandom; 17 | 18 | import static java.lang.Math.*; 19 | 20 | /** 21 | * A policy for retry execution. 22 | */ 23 | @VertxGen 24 | @FunctionalInterface 25 | public interface RetryPolicy { 26 | 27 | /** 28 | * Create a constant delay retry policy. 29 | * 30 | * @param delay the constant delay in milliseconds 31 | */ 32 | static RetryPolicy constantDelay(long delay) { 33 | if (delay <= 0) { 34 | throw new IllegalArgumentException("delay must be strictly positive"); 35 | } 36 | return (failure, retryCount) -> delay; 37 | } 38 | 39 | /** 40 | * Create a linear delay retry policy. 41 | * 42 | * @param initialDelay the initial delay in milliseconds 43 | * @param maxDelay maximum delay in milliseconds 44 | */ 45 | static RetryPolicy linearDelay(long initialDelay, long maxDelay) { 46 | if (initialDelay <= 0) { 47 | throw new IllegalArgumentException("initialDelay must be strictly positive"); 48 | } 49 | if (maxDelay < initialDelay) { 50 | throw new IllegalArgumentException("maxDelay must be greater than initialDelay"); 51 | } 52 | return (failure, retryCount) -> min(maxDelay, initialDelay * retryCount); 53 | } 54 | 55 | /** 56 | * Create an exponential delay with jitter retry policy. 57 | *

58 | * Based on the Full Jitter approach described in 59 | * Exponential Backoff And Jitter. 60 | * 61 | * @param initialDelay the initial delay in milliseconds 62 | * @param maxDelay maximum delay in milliseconds 63 | */ 64 | static RetryPolicy exponentialDelayWithJitter(long initialDelay, long maxDelay) { 65 | if (initialDelay <= 0) { 66 | throw new IllegalArgumentException("initialDelay must be strictly positive"); 67 | } 68 | if (maxDelay < initialDelay) { 69 | throw new IllegalArgumentException("maxDelay must be greater than initialDelay"); 70 | } 71 | return (failure, retryCount) -> { 72 | ThreadLocalRandom random = ThreadLocalRandom.current(); 73 | long delay = initialDelay * (1L << retryCount); 74 | return random.nextLong(0, delay < 0 ? maxDelay : min(maxDelay, delay)); 75 | }; 76 | } 77 | 78 | /** 79 | * Compute a delay in milliseconds before retry is executed. 80 | * 81 | * @param failure the failure of the previous execution attempt 82 | * @param retryCount the number of times operation has been retried already 83 | * @return a delay in milliseconds before retry is executed 84 | */ 85 | long delay(Throwable failure, int retryCount); 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/TimeoutException.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker; 2 | 3 | /** 4 | * Exception reported when the monitored operation timed out. 5 | *

6 | * For performance reason, this exception does not carry a stack trace. You are not allowed to set a stack trace or a 7 | * cause to this exception. This immutability allows using a singleton instance. 8 | * 9 | * @author Clement Escoffier 10 | */ 11 | public class TimeoutException extends RuntimeException { 12 | 13 | public static TimeoutException INSTANCE = new TimeoutException(); 14 | 15 | private TimeoutException() { 16 | super("operation timeout", null, false, false); 17 | } 18 | 19 | @Override 20 | public void setStackTrace(StackTraceElement[] stackTrace) { 21 | throw new UnsupportedOperationException(); 22 | } 23 | 24 | @Override 25 | public synchronized Throwable initCause(Throwable cause) { 26 | throw new UnsupportedOperationException(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.vertx.circuitbreaker.impl; 18 | 19 | import io.vertx.circuitbreaker.*; 20 | import io.vertx.core.*; 21 | import io.vertx.core.eventbus.DeliveryOptions; 22 | import io.vertx.core.internal.ContextInternal; 23 | import io.vertx.core.internal.PromiseInternal; 24 | import io.vertx.core.json.JsonObject; 25 | 26 | import java.util.LinkedHashMap; 27 | import java.util.Map; 28 | import java.util.Objects; 29 | import java.util.concurrent.TimeUnit; 30 | import java.util.concurrent.atomic.AtomicInteger; 31 | import java.util.function.Function; 32 | import java.util.function.Supplier; 33 | 34 | /** 35 | * @author Clement Escoffier 36 | */ 37 | public class CircuitBreakerImpl implements CircuitBreaker { 38 | 39 | private static final Handler NOOP = (v) -> { 40 | // Nothing... 41 | }; 42 | 43 | private final Vertx vertx; 44 | private final CircuitBreakerOptions options; 45 | private final String name; 46 | private final long periodicUpdateTask; 47 | 48 | private Handler openHandler = NOOP; 49 | private Handler halfOpenHandler = NOOP; 50 | private Handler closeHandler = NOOP; 51 | private Function fallback = null; 52 | private FailurePolicy failurePolicy = FailurePolicy.defaultPolicy(); 53 | 54 | private CircuitBreakerState state = CircuitBreakerState.CLOSED; 55 | private RollingCounter rollingFailures; 56 | 57 | private final AtomicInteger passed = new AtomicInteger(); 58 | 59 | private final CircuitBreakerMetrics metrics; 60 | private RetryPolicy retryPolicy = (failure, retryCount) -> 0L; 61 | 62 | public CircuitBreakerImpl(String name, Vertx vertx, CircuitBreakerOptions options) { 63 | Objects.requireNonNull(name); 64 | Objects.requireNonNull(vertx); 65 | this.vertx = vertx; 66 | this.name = name; 67 | 68 | if (options == null) { 69 | this.options = new CircuitBreakerOptions(); 70 | } else { 71 | this.options = new CircuitBreakerOptions(options); 72 | } 73 | 74 | this.rollingFailures = new RollingCounter(this.options.getFailuresRollingWindow() / 1000, TimeUnit.SECONDS); 75 | 76 | if (this.options.getNotificationAddress() != null) { 77 | this.metrics = new CircuitBreakerMetrics(vertx, this, this.options); 78 | sendUpdateOnEventBus(); 79 | if (this.options.getNotificationPeriod() > 0) { 80 | this.periodicUpdateTask = vertx.setPeriodic(this.options.getNotificationPeriod(), l -> sendUpdateOnEventBus()); 81 | } else { 82 | this.periodicUpdateTask = -1; 83 | } 84 | } else { 85 | this.metrics = null; 86 | this.periodicUpdateTask = -1; 87 | } 88 | } 89 | 90 | @Override 91 | public CircuitBreaker close() { 92 | if (metrics != null) { 93 | if (periodicUpdateTask != -1) { 94 | vertx.cancelTimer(periodicUpdateTask); 95 | } 96 | metrics.close(); 97 | } 98 | return this; 99 | } 100 | 101 | @Override 102 | public synchronized CircuitBreaker openHandler(Handler handler) { 103 | Objects.requireNonNull(handler); 104 | openHandler = handler; 105 | return this; 106 | } 107 | 108 | @Override 109 | public synchronized CircuitBreaker halfOpenHandler(Handler handler) { 110 | Objects.requireNonNull(handler); 111 | halfOpenHandler = handler; 112 | return this; 113 | } 114 | 115 | @Override 116 | public synchronized CircuitBreaker closeHandler(Handler handler) { 117 | Objects.requireNonNull(handler); 118 | closeHandler = handler; 119 | return this; 120 | } 121 | 122 | @Override 123 | public CircuitBreaker fallback(Function handler) { 124 | Objects.requireNonNull(handler); 125 | fallback = handler; 126 | return this; 127 | } 128 | 129 | @Override 130 | public CircuitBreaker failurePolicy(FailurePolicy failurePolicy) { 131 | Objects.requireNonNull(failurePolicy); 132 | this.failurePolicy = failurePolicy; 133 | return this; 134 | } 135 | 136 | /** 137 | * A version of {@link #reset()} that can forcefully change the state to closed even if the circuit breaker is open. 138 | *

139 | * This is an internal API. 140 | * 141 | * @param force whether we force the state change and allow an illegal transition 142 | * @return this circuit breaker 143 | */ 144 | public synchronized CircuitBreaker reset(boolean force) { 145 | rollingFailures.reset(); 146 | 147 | if (state == CircuitBreakerState.CLOSED) { 148 | // Do nothing else. 149 | return this; 150 | } 151 | 152 | if (!force && state == CircuitBreakerState.OPEN) { 153 | // Resetting the circuit breaker while we are in the open state is an illegal transition 154 | return this; 155 | } 156 | 157 | state = CircuitBreakerState.CLOSED; 158 | closeHandler.handle(null); 159 | sendUpdateOnEventBus(); 160 | return this; 161 | } 162 | 163 | @Override 164 | public synchronized CircuitBreaker reset() { 165 | return reset(false); 166 | } 167 | 168 | private synchronized void sendUpdateOnEventBus() { 169 | if (metrics != null) { 170 | DeliveryOptions deliveryOptions = new DeliveryOptions() 171 | .setLocalOnly(options.isNotificationLocalOnly()); 172 | vertx.eventBus().publish(options.getNotificationAddress(), metrics.toJson(), deliveryOptions); 173 | } 174 | } 175 | 176 | @Override 177 | public synchronized CircuitBreaker open() { 178 | state = CircuitBreakerState.OPEN; 179 | openHandler.handle(null); 180 | sendUpdateOnEventBus(); 181 | 182 | // Set up the attempt reset timer 183 | long period = options.getResetTimeout(); 184 | if (period != -1) { 185 | vertx.setTimer(period, l -> attemptReset()); 186 | } 187 | 188 | return this; 189 | } 190 | 191 | @Override 192 | public synchronized long failureCount() { 193 | return rollingFailures.count(); 194 | } 195 | 196 | @Override 197 | public synchronized CircuitBreakerState state() { 198 | return state; 199 | } 200 | 201 | private synchronized CircuitBreaker attemptReset() { 202 | if (state == CircuitBreakerState.OPEN) { 203 | passed.set(0); 204 | state = CircuitBreakerState.HALF_OPEN; 205 | halfOpenHandler.handle(null); 206 | sendUpdateOnEventBus(); 207 | } 208 | return this; 209 | } 210 | 211 | @Override 212 | public CircuitBreaker executeAndReportWithFallback(Promise resultPromise, 213 | Handler> command, 214 | Function fallback) { 215 | ContextInternal context = (ContextInternal) vertx.getOrCreateContext(); 216 | executeAndReportWithFallback(context, resultPromise, convert(context, command), fallback); 217 | return this; 218 | } 219 | 220 | public void executeAndReportWithFallback( 221 | ContextInternal context, 222 | Promise resultPromise, 223 | Supplier> command, 224 | Function fallback) { 225 | 226 | CircuitBreakerState currentState; 227 | synchronized (this) { 228 | currentState = state; 229 | } 230 | 231 | CircuitBreakerMetrics.Operation operationMetrics = metrics != null ? metrics.enqueue() : null; 232 | 233 | // this future object tracks the completion of the operation 234 | // This future is marked as failed on operation failures and timeout. 235 | Promise operationResult = context.promise(); 236 | 237 | if (currentState == CircuitBreakerState.CLOSED) { 238 | Future opFuture = operationResult.future(); 239 | opFuture.onComplete(new ClosedCircuitCompletion<>(context, resultPromise, fallback, operationMetrics)); 240 | if (options.getMaxRetries() > 0) { 241 | executeOperation(command, retryPromise(context, 0, command, operationResult, operationMetrics), operationMetrics); 242 | } else { 243 | executeOperation(command, operationResult, operationMetrics); 244 | } 245 | } else if (currentState == CircuitBreakerState.OPEN) { 246 | // Fallback immediately 247 | if (operationMetrics != null) { 248 | operationMetrics.shortCircuited(); 249 | } 250 | invokeFallback(OpenCircuitException.INSTANCE, resultPromise, fallback, operationMetrics); 251 | } else if (currentState == CircuitBreakerState.HALF_OPEN) { 252 | if (passed.incrementAndGet() == 1) { 253 | Future opFuture = operationResult.future(); 254 | opFuture.onComplete(new HalfOpenedCircuitCompletion<>(context, resultPromise, fallback, operationMetrics)); 255 | // Execute the operation 256 | executeOperation(command, operationResult, operationMetrics); 257 | } else { 258 | // Not selected, fallback. 259 | if (operationMetrics != null) { 260 | operationMetrics.shortCircuited(); 261 | } 262 | invokeFallback(OpenCircuitException.INSTANCE, resultPromise, fallback, operationMetrics); 263 | } 264 | } 265 | } 266 | 267 | private Promise retryPromise(ContextInternal context, int retryCount, Supplier> command, 268 | Promise operationResult, CircuitBreakerMetrics.Operation operationMetrics) { 269 | 270 | Promise promise = context.promise(); 271 | promise.future().onComplete(event -> { 272 | if (event.succeeded()) { 273 | reset(); 274 | operationResult.complete(event.result()); 275 | return; 276 | } 277 | 278 | CircuitBreakerState currentState; 279 | synchronized (this) { 280 | currentState = state; 281 | } 282 | 283 | if (currentState == CircuitBreakerState.CLOSED) { 284 | if (retryCount < options.getMaxRetries() - 1) { 285 | executeRetryWithDelay(event.cause(), retryCount, l -> { 286 | // Don't report timeout or error in the retry attempt, only the last one. 287 | executeOperation(command, retryPromise(context, retryCount + 1, command, operationResult, null), 288 | operationMetrics); 289 | }); 290 | } else { 291 | executeRetryWithDelay(event.cause(), retryCount, l -> { 292 | executeOperation(command, operationResult, operationMetrics); 293 | }); 294 | } 295 | } else { 296 | operationResult.fail(OpenCircuitException.INSTANCE); 297 | } 298 | }); 299 | return promise; 300 | } 301 | 302 | private void executeRetryWithDelay(Throwable failure, int retryCount, Handler action) { 303 | long retryDelay = retryPolicy.delay(failure, retryCount + 1); 304 | 305 | if (retryDelay > 0) { 306 | vertx.setTimer(retryDelay, l -> { 307 | action.handle(null); 308 | }); 309 | } else { 310 | action.handle(null); 311 | } 312 | } 313 | 314 | private void invokeFallback(Throwable reason, Promise resultPromise, 315 | Function fallback, CircuitBreakerMetrics.Operation operationMetrics) { 316 | if (fallback == null) { 317 | // No fallback, mark the user future as failed. 318 | resultPromise.fail(reason); 319 | return; 320 | } 321 | 322 | try { 323 | T apply = fallback.apply(reason); 324 | if (operationMetrics != null) { 325 | operationMetrics.fallbackSucceed(); 326 | } 327 | resultPromise.complete(apply); 328 | } catch (Exception e) { 329 | resultPromise.fail(e); 330 | if (operationMetrics != null) { 331 | operationMetrics.fallbackFailed(); 332 | } 333 | } 334 | } 335 | 336 | private Supplier> convert(ContextInternal context, Handler> handler) { 337 | // We use an intermediate future to avoid the passed future to complete or fail after a timeout. 338 | return () -> { 339 | Promise passedFuture = context.promise(); 340 | handler.handle(passedFuture); 341 | return passedFuture.future(); 342 | }; 343 | } 344 | 345 | private void executeOperation(Supplier> supplier, Promise operationResult, 346 | CircuitBreakerMetrics.Operation operationMetrics) { 347 | // Execute the operation 348 | Future fut; 349 | try { 350 | fut = supplier.get(); 351 | } catch (Throwable e) { 352 | if (!operationResult.future().isComplete()) { 353 | if (operationMetrics != null) { 354 | operationMetrics.error(); 355 | } 356 | operationResult.fail(e); 357 | } 358 | return; 359 | } 360 | 361 | if (options.getTimeout() != -1) { 362 | long timerId = vertx.setTimer(options.getTimeout(), (l) -> { 363 | // Check if the operation has not already been completed 364 | if (!operationResult.future().isComplete()) { 365 | if (operationMetrics != null) { 366 | operationMetrics.timeout(); 367 | } 368 | operationResult.fail(TimeoutException.INSTANCE); 369 | } 370 | // Else Operation has completed 371 | }); 372 | fut.onComplete(v -> vertx.cancelTimer(timerId)); 373 | } 374 | 375 | fut.onComplete(ar -> { 376 | if (ar.failed()) { 377 | if (!operationResult.future().isComplete()) { 378 | operationResult.fail(ar.cause()); 379 | } 380 | } else { 381 | if (!operationResult.future().isComplete()) { 382 | operationResult.complete(ar.result()); 383 | } 384 | } 385 | }); 386 | } 387 | 388 | @Override 389 | public Future executeWithFallback(Handler> operation, Function fallback) { 390 | // be careful to not create a new context, to preserve existing (sometimes synchronous) behavior 391 | ContextInternal context = ContextInternal.current(); 392 | Promise promise = context != null ? context.promise() : Promise.promise(); 393 | executeAndReportWithFallback(promise, operation, fallback); 394 | return promise.future(); 395 | } 396 | 397 | @Override 398 | public Future executeWithFallback(Supplier> command, Function fallback) { 399 | ContextInternal context = (ContextInternal) vertx.getOrCreateContext(); 400 | Promise resultPromise = context.promise(); 401 | executeAndReportWithFallback(context, resultPromise, command, fallback); 402 | return resultPromise.future(); 403 | } 404 | 405 | public Future execute(Handler> operation) { 406 | return executeWithFallback(operation, fallback); 407 | } 408 | 409 | @Override 410 | public Future execute(Supplier> command) { 411 | return executeWithFallback(command, fallback); 412 | } 413 | 414 | @Override 415 | public CircuitBreaker executeAndReport(Promise resultPromise, Handler> operation) { 416 | return executeAndReportWithFallback(resultPromise, operation, fallback); 417 | } 418 | 419 | @Override 420 | public String name() { 421 | return name; 422 | } 423 | 424 | private synchronized void incrementFailures() { 425 | rollingFailures.increment(); 426 | if (rollingFailures.count() >= options.getMaxFailures()) { 427 | if (state != CircuitBreakerState.OPEN) { 428 | open(); 429 | } else { 430 | // `open()` calls `sendUpdateOnEventBus()`, so no need to repeat it in the previous case 431 | sendUpdateOnEventBus(); 432 | } 433 | } else { 434 | // Number of failure has changed, send update. 435 | sendUpdateOnEventBus(); 436 | } 437 | } 438 | 439 | /** 440 | * For testing purpose only. 441 | * 442 | * @return retrieve the metrics. 443 | */ 444 | public JsonObject getMetrics() { 445 | return metrics.toJson(); 446 | } 447 | 448 | public CircuitBreakerOptions options() { 449 | return options; 450 | } 451 | 452 | @Override 453 | public CircuitBreaker retryPolicy(RetryPolicy retryPolicy) { 454 | this.retryPolicy = retryPolicy; 455 | return this; 456 | } 457 | 458 | static class RollingCounter { 459 | // all `RollingCounter` methods are called in a `synchronized (CircuitBreakerImpl.this)` block, 460 | // which therefore guards access to these fields 461 | 462 | private Map window; 463 | private long timeUnitsInWindow; 464 | private TimeUnit windowTimeUnit; 465 | 466 | public RollingCounter(long timeUnitsInWindow, TimeUnit windowTimeUnit) { 467 | this.windowTimeUnit = windowTimeUnit; 468 | this.window = new LinkedHashMap((int) timeUnitsInWindow + 1) { 469 | @Override 470 | protected boolean removeEldestEntry(Map.Entry eldest) { 471 | return size() > timeUnitsInWindow; 472 | } 473 | }; 474 | this.timeUnitsInWindow = timeUnitsInWindow; 475 | } 476 | 477 | public void increment() { 478 | long timeSlot = windowTimeUnit.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS); 479 | Long current = window.getOrDefault(timeSlot, 0L); 480 | window.put(timeSlot, ++current); 481 | } 482 | 483 | public long count() { 484 | long windowStartTime = windowTimeUnit.convert(System.currentTimeMillis() - windowTimeUnit.toMillis(timeUnitsInWindow), TimeUnit.MILLISECONDS); 485 | 486 | long result = 0; 487 | for (Map.Entry entry : window.entrySet()) { 488 | if (entry.getKey() >= windowStartTime) { 489 | result += entry.getValue(); 490 | } 491 | } 492 | return result; 493 | } 494 | 495 | public void reset() { 496 | window.clear(); 497 | } 498 | } 499 | 500 | @SuppressWarnings("unchecked") 501 | private abstract class Completion implements Handler> { 502 | 503 | final Context context; 504 | final Promise resultFuture; 505 | final Function fallback; 506 | final CircuitBreakerMetrics.Operation operationMetrics; 507 | 508 | protected Completion(Context context, Promise resultFuture, Function fallback, CircuitBreakerMetrics.Operation operationMetrics) { 509 | this.context = context; 510 | this.resultFuture = resultFuture; 511 | this.fallback = fallback; 512 | this.operationMetrics = operationMetrics; 513 | } 514 | 515 | @Override 516 | public void handle(AsyncResult ar) { 517 | context.runOnContext(v -> { 518 | if (failurePolicy.test(asFuture(ar))) { 519 | failureAction(); 520 | if (operationMetrics != null) { 521 | operationMetrics.failed(); 522 | } 523 | if (options.isFallbackOnFailure()) { 524 | invokeFallback(ar.cause(), resultFuture, fallback, operationMetrics); 525 | } else { 526 | resultFuture.fail(ar.cause()); 527 | } 528 | } else { 529 | if (operationMetrics != null) { 530 | operationMetrics.complete(); 531 | } 532 | reset(); 533 | //The event may pass due to a user given predicate. We still want to push up the failure for the user 534 | //to do any work 535 | resultFuture.handle(ar); 536 | } 537 | }); 538 | } 539 | 540 | private Future asFuture(AsyncResult ar) { 541 | Future result; 542 | if (ar instanceof Future) { 543 | result = (Future) ar; 544 | } else if (ar.succeeded()) { 545 | result = Future.succeededFuture(ar.result()); 546 | } else { 547 | result = Future.failedFuture(ar.cause()); 548 | } 549 | return result; 550 | } 551 | 552 | protected abstract void failureAction(); 553 | } 554 | 555 | private class ClosedCircuitCompletion extends Completion { 556 | 557 | ClosedCircuitCompletion(Context context, Promise userFuture, Function fallback, CircuitBreakerMetrics.Operation call) { 558 | super(context, userFuture, fallback, call); 559 | } 560 | 561 | @Override 562 | protected void failureAction() { 563 | incrementFailures(); 564 | } 565 | } 566 | 567 | private class HalfOpenedCircuitCompletion extends Completion { 568 | 569 | HalfOpenedCircuitCompletion(Context context, Promise userFuture, Function fallback, CircuitBreakerMetrics.Operation call) { 570 | super(context, userFuture, fallback, call); 571 | } 572 | 573 | @Override 574 | protected void failureAction() { 575 | open(); 576 | } 577 | } 578 | } 579 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/impl/CircuitBreakerMetrics.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker.impl; 2 | 3 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.internal.VertxInternal; 6 | import io.vertx.core.json.JsonObject; 7 | import org.HdrHistogram.Histogram; 8 | 9 | /** 10 | * Circuit breaker metrics. 11 | * 12 | * @author Clement Escoffier 13 | */ 14 | public class CircuitBreakerMetrics { 15 | private final CircuitBreakerImpl circuitBreaker; 16 | private final String node; 17 | 18 | private final long circuitBreakerResetTimeout; 19 | private final long circuitBreakerTimeout; 20 | 21 | // Global statistics 22 | 23 | private final RollingWindow rollingWindow; 24 | 25 | CircuitBreakerMetrics(Vertx vertx, CircuitBreakerImpl circuitBreaker, CircuitBreakerOptions options) { 26 | this.circuitBreaker = circuitBreaker; 27 | this.circuitBreakerTimeout = circuitBreaker.options().getTimeout(); 28 | this.circuitBreakerResetTimeout = circuitBreaker.options().getResetTimeout(); 29 | this.node = vertx.isClustered() ? ((VertxInternal) vertx).clusterManager().getNodeId() : "local"; 30 | this.rollingWindow = new RollingWindow(options.getMetricsRollingWindow(), options.getMetricsRollingBuckets()); 31 | } 32 | 33 | private synchronized void evictOutdatedOperations() { 34 | rollingWindow.updateTime(); 35 | } 36 | 37 | public void close() { 38 | // do nothing by default. 39 | } 40 | 41 | class Operation { 42 | final long begin; 43 | private volatile long end; 44 | private boolean complete; 45 | private boolean failed; 46 | private boolean timeout; 47 | private boolean exception; 48 | private boolean fallbackFailed; 49 | private boolean fallbackSucceed; 50 | private boolean shortCircuited; 51 | 52 | Operation() { 53 | begin = System.nanoTime(); 54 | } 55 | 56 | synchronized void complete() { 57 | end = System.nanoTime(); 58 | complete = true; 59 | CircuitBreakerMetrics.this.complete(this); 60 | } 61 | 62 | synchronized void failed() { 63 | if (timeout || exception) { 64 | // Already completed. 65 | return; 66 | } 67 | end = System.nanoTime(); 68 | failed = true; 69 | CircuitBreakerMetrics.this.complete(this); 70 | } 71 | 72 | synchronized void timeout() { 73 | end = System.nanoTime(); 74 | failed = false; 75 | timeout = true; 76 | CircuitBreakerMetrics.this.complete(this); 77 | } 78 | 79 | synchronized void error() { 80 | end = System.nanoTime(); 81 | failed = false; 82 | exception = true; 83 | CircuitBreakerMetrics.this.complete(this); 84 | } 85 | 86 | synchronized void fallbackFailed() { 87 | fallbackFailed = true; 88 | } 89 | 90 | synchronized void fallbackSucceed() { 91 | fallbackSucceed = true; 92 | } 93 | 94 | synchronized void shortCircuited() { 95 | end = System.nanoTime(); 96 | shortCircuited = true; 97 | CircuitBreakerMetrics.this.complete(this); 98 | } 99 | 100 | long durationInMs() { 101 | return (end - begin) / 1_000_000; 102 | } 103 | } 104 | 105 | Operation enqueue() { 106 | return new Operation(); 107 | } 108 | 109 | public synchronized void complete(Operation operation) { 110 | rollingWindow.add(operation); 111 | } 112 | 113 | public synchronized JsonObject toJson() { 114 | JsonObject json = new JsonObject(); 115 | 116 | // Configuration 117 | json.put("resetTimeout", circuitBreakerResetTimeout); 118 | json.put("timeout", circuitBreakerTimeout); 119 | json.put("metricRollingWindow", rollingWindow.getMetricRollingWindowSizeInMs()); 120 | json.put("name", circuitBreaker.name()); 121 | json.put("node", node); 122 | 123 | // Current state 124 | json.put("state", circuitBreaker.state()); 125 | json.put("failures", circuitBreaker.failureCount()); 126 | 127 | // Global metrics 128 | addSummary(json, rollingWindow.totalSummary(), MetricNames.TOTAL); 129 | 130 | // Window metrics 131 | evictOutdatedOperations(); 132 | addSummary(json, rollingWindow.windowSummary(), MetricNames.ROLLING); 133 | 134 | return json; 135 | } 136 | 137 | private void addSummary(JsonObject json, RollingWindow.Summary summary, MetricNames names) { 138 | long calls = summary.count(); 139 | int errorCount = summary.failures + summary.exceptions + summary.timeouts; 140 | 141 | json.put(names.operationCountName, calls - summary.shortCircuited); 142 | json.put(names.errorCountName, errorCount); 143 | json.put(names.successCountName, summary.successes); 144 | json.put(names.timeoutCountName, summary.timeouts); 145 | json.put(names.exceptionCountName, summary.exceptions); 146 | json.put(names.failureCountName, summary.failures); 147 | 148 | if (calls == 0) { 149 | json.put(names.successPercentageName, 0); 150 | json.put(names.errorPercentageName, 0); 151 | } else { 152 | json.put(names.successPercentageName, ((double) summary.successes / calls) * 100); 153 | json.put(names.errorPercentageName, ((double) (errorCount) / calls) * 100); 154 | } 155 | 156 | json.put(names.fallbackSuccessCountName, summary.fallbackSuccess); 157 | json.put(names.fallbackFailureCountName, summary.fallbackFailure); 158 | json.put(names.shortCircuitedCountName, summary.shortCircuited); 159 | 160 | addLatency(json, summary.statistics, names); 161 | } 162 | 163 | 164 | private void addLatency(JsonObject json, Histogram histogram, MetricNames names) { 165 | json.put(names.latencyMeanName, histogram.getMean()); 166 | json.put(names.latencyName, new JsonObject() 167 | .put("0", histogram.getValueAtPercentile(0)) 168 | .put("25", histogram.getValueAtPercentile(25)) 169 | .put("50", histogram.getValueAtPercentile(50)) 170 | .put("75", histogram.getValueAtPercentile(75)) 171 | .put("90", histogram.getValueAtPercentile(90)) 172 | .put("95", histogram.getValueAtPercentile(95)) 173 | .put("99", histogram.getValueAtPercentile(99)) 174 | .put("99.5", histogram.getValueAtPercentile(99.5)) 175 | .put("100", histogram.getValueAtPercentile(100))); 176 | } 177 | 178 | private enum MetricNames { 179 | ROLLING("rolling"), TOTAL("total"); 180 | 181 | private final String operationCountName; 182 | private final String errorCountName; 183 | private final String successCountName; 184 | private final String timeoutCountName; 185 | private final String exceptionCountName; 186 | private final String failureCountName; 187 | private final String successPercentageName; 188 | private final String errorPercentageName; 189 | private final String fallbackSuccessCountName; 190 | private final String fallbackFailureCountName; 191 | private final String shortCircuitedCountName; 192 | 193 | private final String latencyMeanName; 194 | private final String latencyName; 195 | 196 | MetricNames(String prefix){ 197 | operationCountName = prefix + "OperationCount"; 198 | errorCountName = prefix + "ErrorCount"; 199 | successCountName = prefix + "SuccessCount"; 200 | timeoutCountName = prefix + "TimeoutCount"; 201 | exceptionCountName = prefix + "ExceptionCount"; 202 | failureCountName = prefix + "FailureCount"; 203 | successPercentageName = prefix + "SuccessPercentage"; 204 | errorPercentageName = prefix + "ErrorPercentage"; 205 | fallbackSuccessCountName = prefix + "FallbackSuccessCount"; 206 | fallbackFailureCountName = prefix + "FallbackFailureCount"; 207 | shortCircuitedCountName = prefix + "ShortCircuitedCount"; 208 | 209 | latencyName = prefix + "Latency"; 210 | latencyMeanName = prefix + "LatencyMean"; 211 | } 212 | } 213 | 214 | private static class RollingWindow { 215 | private final Summary history; 216 | private final Summary[] buckets; 217 | private final long bucketSizeInNs; 218 | 219 | RollingWindow(long windowSizeInMs, int numberOfBuckets) { 220 | if (windowSizeInMs % numberOfBuckets != 0) { 221 | throw new IllegalArgumentException("Window size should be divisible by number of buckets."); 222 | } 223 | this.buckets = new Summary[numberOfBuckets]; 224 | for (int i = 0; i < buckets.length; i++) { 225 | this.buckets[i] = new Summary(); 226 | } 227 | this.bucketSizeInNs = 1_000_000 * windowSizeInMs / numberOfBuckets; 228 | this.history = new Summary(); 229 | } 230 | 231 | public void add(Operation operation) { 232 | getBucket(operation.end).add(operation); 233 | } 234 | 235 | public Summary totalSummary() { 236 | Summary total = new Summary(); 237 | 238 | total.add(history); 239 | total.add(windowSummary()); 240 | 241 | return total; 242 | } 243 | 244 | public Summary windowSummary() { 245 | Summary window = new Summary(buckets[0].bucketIndex); 246 | for (Summary bucket : buckets) { 247 | window.add(bucket); 248 | } 249 | 250 | return window; 251 | } 252 | 253 | public void updateTime() { 254 | getBucket(System.nanoTime()); 255 | } 256 | 257 | private Summary getBucket(long timeInNs) { 258 | long bucketIndex = timeInNs / bucketSizeInNs; 259 | 260 | //sample too old: 261 | if (bucketIndex < buckets[0].bucketIndex) { 262 | return history; 263 | } 264 | 265 | shiftIfNecessary(bucketIndex); 266 | 267 | return buckets[(int) (bucketIndex - buckets[0].bucketIndex)]; 268 | } 269 | 270 | private void shiftIfNecessary(long bucketIndex) { 271 | long shiftUnlimited = bucketIndex - buckets[buckets.length - 1].bucketIndex; 272 | if (shiftUnlimited <= 0) { 273 | return; 274 | } 275 | int shift = (int) Long.min(buckets.length, shiftUnlimited); 276 | 277 | // Add old buckets to history 278 | for(int i = 0; i < shift; i++) { 279 | history.add(buckets[i]); 280 | } 281 | 282 | System.arraycopy(buckets, shift, buckets, 0, buckets.length - shift); 283 | 284 | for(int i = buckets.length - shift; i < buckets.length; i++) { 285 | buckets[i] = new Summary(bucketIndex + i + 1 - buckets.length); 286 | } 287 | } 288 | 289 | public long getMetricRollingWindowSizeInMs() { 290 | return bucketSizeInNs * buckets.length / 1_000_000; 291 | } 292 | 293 | private static class Summary { 294 | final long bucketIndex; 295 | final Histogram statistics; 296 | 297 | private int successes; 298 | private int failures; 299 | private int exceptions; 300 | private int timeouts; 301 | private int fallbackSuccess; 302 | private int fallbackFailure; 303 | private int shortCircuited; 304 | 305 | private Summary() { 306 | this(-1); 307 | } 308 | 309 | private Summary(long bucketIndex) { 310 | this.bucketIndex = bucketIndex; 311 | statistics = new Histogram(2); 312 | } 313 | 314 | public void add(Summary other) { 315 | statistics.add(other.statistics); 316 | 317 | successes += other.successes; 318 | failures += other.failures; 319 | exceptions += other.exceptions; 320 | timeouts += other.timeouts; 321 | fallbackSuccess += other.fallbackSuccess; 322 | fallbackFailure += other.fallbackFailure; 323 | shortCircuited += other.shortCircuited; 324 | } 325 | 326 | public void add(Operation operation) { 327 | statistics.recordValue(operation.durationInMs()); 328 | if (operation.complete) { 329 | successes++; 330 | } else if (operation.failed) { 331 | failures++; 332 | } else if (operation.exception) { 333 | exceptions++; 334 | } else if (operation.timeout) { 335 | timeouts++; 336 | } 337 | 338 | if (operation.fallbackSucceed) { 339 | fallbackSuccess++; 340 | } else if (operation.fallbackFailed) { 341 | fallbackFailure++; 342 | } 343 | 344 | if (operation.shortCircuited) { 345 | shortCircuited++; 346 | } 347 | } 348 | 349 | public long count() { 350 | return statistics.getTotalCount(); 351 | } 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/circuitbreaker/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | @ModuleGen(name = "vertx-circuit-breaker", groupPackage = "io.vertx") 17 | package io.vertx.circuitbreaker; 18 | 19 | import io.vertx.codegen.annotations.ModuleGen; 20 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2024 Contributors to the Eclipse Foundation 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | * which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | * 9 | * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | */ 11 | module io.vertx.circuitbreaker { 12 | 13 | requires static io.vertx.docgen; 14 | requires static io.vertx.codegen.api; 15 | requires static io.vertx.codegen.json; 16 | 17 | requires static HdrHistogram; 18 | requires io.vertx.core; 19 | requires io.vertx.core.logging; 20 | 21 | exports io.vertx.circuitbreaker; 22 | exports io.vertx.circuitbreaker.impl to io.vertx.circuitbreaker.tests; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/JsonFactory.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker.tests; 2 | 3 | import io.vertx.core.json.jackson.JacksonCodec; 4 | import io.vertx.core.spi.json.JsonCodec; 5 | 6 | public class JsonFactory implements io.vertx.core.spi.JsonFactory { 7 | 8 | @Override 9 | public JsonCodec codec() { 10 | return new JacksonCodec(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/PredefinedRetryPolicyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2022 Contributors to the Eclipse Foundation 3 | * 4 | * This program and the accompanying materials are made available under the 5 | * terms of the Eclipse Public License 2.0 which is available at 6 | * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | * which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | * 9 | * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | */ 11 | 12 | package io.vertx.circuitbreaker.tests; 13 | 14 | import io.vertx.circuitbreaker.RetryPolicy; 15 | import org.junit.Test; 16 | 17 | import static org.junit.Assert.*; 18 | 19 | public class PredefinedRetryPolicyTest { 20 | 21 | @Test(expected = IllegalArgumentException.class) 22 | public void testConstantDelayNegative() { 23 | RetryPolicy.constantDelay(-1); 24 | } 25 | 26 | @Test 27 | public void testConstantDelay() { 28 | RetryPolicy retryPolicy = RetryPolicy.constantDelay(10); 29 | for (int i = 1; i <= 50; i++) { 30 | assertEquals(10, retryPolicy.delay(null, i)); 31 | } 32 | } 33 | 34 | @Test(expected = IllegalArgumentException.class) 35 | public void testLinearDelayNegative() { 36 | RetryPolicy.linearDelay(-1, 30000); 37 | } 38 | 39 | @Test(expected = IllegalArgumentException.class) 40 | public void testLinearDelayGreatherThanMax() { 41 | RetryPolicy.linearDelay(50000, 30000); 42 | } 43 | 44 | @Test 45 | public void testLinearDelay() { 46 | RetryPolicy retryPolicy = RetryPolicy.linearDelay(10, 250); 47 | for (int i = 1; i <= 50; i++) { 48 | long delay = retryPolicy.delay(null, i); 49 | if (i <= 25) { 50 | assertEquals(10 * i, delay); 51 | } else { 52 | assertEquals(250, delay); 53 | } 54 | } 55 | } 56 | 57 | @Test(expected = IllegalArgumentException.class) 58 | public void testExponentialDelayNegative() { 59 | RetryPolicy.exponentialDelayWithJitter(-1, 30000); 60 | } 61 | 62 | @Test(expected = IllegalArgumentException.class) 63 | public void testExponentialDelayGreatherThanMax() { 64 | RetryPolicy.exponentialDelayWithJitter(50000, 30000); 65 | } 66 | 67 | @Test 68 | public void testExponentialDelayWithJitter() { 69 | int maxDelay = 30000; 70 | RetryPolicy retryPolicy = RetryPolicy.exponentialDelayWithJitter(3, maxDelay); 71 | for (int i = 1; i <= 50; i++) { 72 | long delay = retryPolicy.delay(null, i); 73 | assertTrue(0 <= delay && delay <= maxDelay); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/APITest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.vertx.circuitbreaker.tests.impl; 18 | 19 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 20 | import io.vertx.circuitbreaker.CircuitBreakerState; 21 | import io.vertx.core.Promise; 22 | import io.vertx.core.Vertx; 23 | import io.vertx.circuitbreaker.CircuitBreaker; 24 | import org.junit.After; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | 28 | import java.util.concurrent.atomic.AtomicBoolean; 29 | import java.util.concurrent.atomic.AtomicInteger; 30 | 31 | import static org.awaitility.Awaitility.await; 32 | import static org.hamcrest.core.Is.is; 33 | import static org.junit.Assert.assertEquals; 34 | import static org.junit.Assert.assertNotNull; 35 | 36 | /** 37 | * @author Clement Escoffier 38 | */ 39 | public class APITest { 40 | 41 | private CircuitBreaker breaker; 42 | private Vertx vertx; 43 | 44 | @Before 45 | public void setUp() { 46 | vertx = Vertx.vertx(); 47 | } 48 | 49 | @After 50 | public void tearDown() { 51 | if (breaker != null) { 52 | breaker.close(); 53 | } 54 | AtomicBoolean completed = new AtomicBoolean(); 55 | vertx.close().onComplete(ar -> completed.set(ar.succeeded())); 56 | await().untilAtomic(completed, is(true)); 57 | } 58 | 59 | /** 60 | * Reproducer of https://github.com/vert-x3/vertx-circuit-breaker/issues/9 61 | */ 62 | @Test 63 | public void testWhenOptionsAreNull() { 64 | CircuitBreaker cb = CircuitBreaker.create("name", vertx, null); 65 | assertNotNull(cb); 66 | assertEquals("name", cb.name()); 67 | assertEquals(CircuitBreakerState.CLOSED, cb.state()); 68 | } 69 | 70 | 71 | @Test 72 | public void testWithOperationWithHandler() { 73 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()); 74 | 75 | AtomicInteger result = new AtomicInteger(); 76 | 77 | breaker.executeWithFallback(fut -> { 78 | MyAsyncOperations.operation(1, 1, fut); 79 | }, v -> 0) 80 | .onComplete(ar -> result.set(ar.result())); 81 | 82 | await().untilAtomic(result, is(2)); 83 | } 84 | 85 | @Test 86 | public void testWithOperationWithCompletionHandler() { 87 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()); 88 | 89 | AtomicInteger result = new AtomicInteger(); 90 | 91 | breaker.executeWithFallback(fut -> { 92 | MyAsyncOperations.operation(1, 1, fut); 93 | }, v -> 0).onComplete(ar -> result.set(ar.result())); 94 | 95 | await().untilAtomic(result, is(2)); 96 | } 97 | 98 | @Test 99 | public void testWithFailingOperationWithHandler() { 100 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions() 101 | .setFallbackOnFailure(true)); 102 | 103 | AtomicInteger result = new AtomicInteger(); 104 | 105 | breaker.executeWithFallback(fut -> { 106 | MyAsyncOperations.fail(fut); 107 | }, v -> -1) 108 | .onComplete(ar -> result.set(ar.result())); 109 | 110 | await().untilAtomic(result, is(-1)); 111 | } 112 | 113 | @Test 114 | public void testWithFailingOperationWithCompletionHandler() { 115 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions() 116 | .setFallbackOnFailure(true)); 117 | 118 | AtomicInteger result = new AtomicInteger(); 119 | 120 | breaker.executeWithFallback(fut -> { 121 | MyAsyncOperations.fail(fut); 122 | }, v -> -1).onComplete(ar -> result.set(ar.result())); 123 | 124 | await().untilAtomic(result, is(-1)); 125 | } 126 | 127 | 128 | @Test 129 | public void testWithOperationWithFuture() { 130 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions() 131 | .setFallbackOnFailure(true)); 132 | 133 | AtomicInteger result = new AtomicInteger(); 134 | Promise operationResult = Promise.promise(); 135 | operationResult.future().onComplete(ar -> { 136 | result.set(ar.result()); 137 | }); 138 | 139 | breaker.executeAndReport(operationResult, future -> MyAsyncOperations.operation(future, 1, 1)); 140 | 141 | await().untilAtomic(result, is(2)); 142 | } 143 | 144 | @Test 145 | public void testWithFailingOperationWithFuture() { 146 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions() 147 | .setFallbackOnFailure(true)); 148 | 149 | AtomicInteger result = new AtomicInteger(); 150 | 151 | Promise operationResult = Promise.promise(); 152 | operationResult.future().onComplete(ar -> result.set(ar.result())); 153 | 154 | breaker.executeAndReportWithFallback(operationResult, MyAsyncOperations::fail, t -> -1); 155 | 156 | await().untilAtomic(result, is(-1)); 157 | } 158 | 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/AsyncBreakerTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker.tests.impl; 2 | 3 | import io.vertx.circuitbreaker.CircuitBreaker; 4 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 5 | import io.vertx.core.Vertx; 6 | import io.vertx.core.internal.logging.Logger; 7 | import io.vertx.core.internal.logging.LoggerFactory; 8 | import io.vertx.ext.unit.Async; 9 | import io.vertx.ext.unit.TestContext; 10 | import io.vertx.ext.unit.junit.VertxUnitRunner; 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | 16 | /** 17 | * Reproducer for https://github.com/vert-x3/issues/issues/294 (copied to 18 | * https://github.com/vert-x3/vertx-circuit-breaker/issues/14). 19 | */ 20 | @RunWith(VertxUnitRunner.class) 21 | public class AsyncBreakerTest { 22 | 23 | private Vertx vertx; 24 | private CircuitBreaker breaker; 25 | private int count; 26 | 27 | private static Logger LOG = LoggerFactory.getLogger(AsyncBreakerTest.class); 28 | 29 | @Before 30 | public void setUp() { 31 | vertx = Vertx.vertx(); 32 | 33 | breaker = CircuitBreaker.create("collector-circuit-breaker", vertx, 34 | new CircuitBreakerOptions() 35 | .setMaxFailures(2) 36 | .setTimeout(1_000) 37 | .setFallbackOnFailure(false) 38 | .setResetTimeout(2_000) 39 | .setNotificationPeriod(0)); 40 | 41 | count = 0; 42 | } 43 | 44 | @After 45 | public void tearDown(TestContext tc) { 46 | vertx.close().onComplete(tc.asyncAssertSuccess()); 47 | } 48 | 49 | private void x(TestContext tc, int id) { 50 | Async async = tc.async(10); 51 | breaker.executeWithFallback(future -> { 52 | vertx.setTimer(100 + (id * 10), handler -> { 53 | synchronized (this) { 54 | count++; 55 | if (count < 5 || count > 12) { 56 | future.complete("OK"); 57 | async.complete(); 58 | } else { 59 | future.fail("kapot"); 60 | async.complete(); 61 | } 62 | } 63 | }); 64 | 65 | }, fallback -> { 66 | LOG.info("OPEN " + id); 67 | async.complete(); 68 | return "OPEN"; 69 | }); 70 | } 71 | 72 | @Test 73 | public void test1(TestContext tc) throws InterruptedException { 74 | 75 | for (int i = 0; i < 20; ++i) { 76 | x(tc, i); 77 | } 78 | 79 | breaker.openHandler(h -> LOG.info("Breaker open")); 80 | breaker.closeHandler(h -> tc.fail("should not close")); 81 | breaker.halfOpenHandler(h -> LOG.info("Breaker half open")); 82 | } 83 | 84 | @Test 85 | public void test2(TestContext tc) throws InterruptedException { 86 | Async async = tc.async(); 87 | 88 | for (int i = 0; i < 20; ++i) { 89 | x(tc, i); 90 | } 91 | 92 | breaker.openHandler(h -> LOG.info("Breaker open")); 93 | breaker.closeHandler(h -> { 94 | LOG.info("Breaker closed"); 95 | async.complete(); 96 | }); 97 | breaker.halfOpenHandler(h -> LOG.info("Breaker half open")); 98 | 99 | LOG.info("Waiting for test to complete"); 100 | 101 | Thread.sleep(3000); 102 | LOG.info("Sleep done"); 103 | for (int i = 0; i < 5; ++i) { 104 | x(tc, i); 105 | } 106 | } 107 | 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/CircuitBreakerImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.vertx.circuitbreaker.tests.impl; 18 | 19 | import io.vertx.circuitbreaker.*; 20 | import io.vertx.circuitbreaker.impl.CircuitBreakerImpl; 21 | import io.vertx.core.*; 22 | import io.vertx.ext.unit.Async; 23 | import io.vertx.ext.unit.TestContext; 24 | import io.vertx.ext.unit.junit.Repeat; 25 | import io.vertx.ext.unit.junit.RepeatRule; 26 | import io.vertx.ext.unit.junit.VertxUnitRunner; 27 | import org.junit.After; 28 | import org.junit.Before; 29 | import org.junit.Rule; 30 | import org.junit.Test; 31 | import org.junit.runner.RunWith; 32 | 33 | import java.util.ArrayList; 34 | import java.util.List; 35 | import java.util.concurrent.TimeUnit; 36 | import java.util.concurrent.atomic.AtomicBoolean; 37 | import java.util.concurrent.atomic.AtomicInteger; 38 | import java.util.concurrent.atomic.AtomicReference; 39 | import java.util.stream.IntStream; 40 | 41 | import static org.awaitility.Awaitility.await; 42 | import static org.hamcrest.core.Is.is; 43 | import static org.junit.Assert.*; 44 | 45 | /** 46 | * Test the basic behavior of the circuit breaker. 47 | * 48 | * @author Clement Escoffier 49 | */ 50 | @RunWith(VertxUnitRunner.class) 51 | public class CircuitBreakerImplTest { 52 | private Vertx vertx; 53 | private CircuitBreaker breaker; 54 | 55 | @Rule 56 | public RepeatRule rule = new RepeatRule(); 57 | 58 | @Before 59 | public void setUp() { 60 | vertx = Vertx.vertx(); 61 | } 62 | 63 | @After 64 | public void tearDown() { 65 | if (breaker != null) { 66 | breaker.close(); 67 | } 68 | AtomicBoolean completed = new AtomicBoolean(); 69 | vertx.close().onComplete(ar -> completed.set(ar.succeeded())); 70 | await().untilAtomic(completed, is(true)); 71 | } 72 | 73 | @Test 74 | public void testCreationWithDefault() { 75 | breaker = CircuitBreaker.create("name", vertx); 76 | assertEquals("name", breaker.name()); 77 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 78 | } 79 | 80 | @Test 81 | @Repeat(5) 82 | public void testOk() { 83 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()); 84 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 85 | 86 | AtomicBoolean operationCalled = new AtomicBoolean(); 87 | AtomicReference completionCalled = new AtomicReference<>(); 88 | breaker.execute(fut -> { 89 | operationCalled.set(true); 90 | fut.complete("hello"); 91 | }).onComplete(ar -> completionCalled.set(ar.result())); 92 | 93 | await().until(operationCalled::get); 94 | await().until(() -> completionCalled.get().equalsIgnoreCase("hello")); 95 | } 96 | 97 | @Test 98 | @Repeat(5) 99 | public void testWithCustomPredicateOk() { 100 | breaker = CircuitBreaker.create("test", vertx).failurePolicy(ar -> { 101 | return ar.failed() && ar.cause().getStackTrace().length > 0; 102 | }); 103 | 104 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 105 | 106 | AtomicBoolean operationCalled = new AtomicBoolean(); 107 | AtomicReference completionCalled = new AtomicReference<>(); 108 | breaker.execute(fut -> { 109 | operationCalled.set(true); 110 | fut.fail("some fake exception"); 111 | }).onComplete(ar -> { 112 | completionCalled.set(ar.cause().getMessage()); 113 | assertTrue(ar.failed()); 114 | }); 115 | 116 | await().until(operationCalled::get); 117 | await().until(() -> completionCalled.get().equalsIgnoreCase("some fake exception")); 118 | } 119 | 120 | @Test 121 | @Repeat(5) 122 | public void testWithUserFutureOk() { 123 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()); 124 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 125 | 126 | AtomicBoolean operationCalled = new AtomicBoolean(); 127 | AtomicReference completionCalled = new AtomicReference<>(); 128 | 129 | Promise userFuture = Promise.promise(); 130 | userFuture.future().onComplete(ar -> 131 | completionCalled.set(ar.result())); 132 | 133 | breaker.executeAndReport(userFuture, fut -> { 134 | operationCalled.set(true); 135 | fut.complete("hello"); 136 | }); 137 | 138 | await().until(operationCalled::get); 139 | await().until(() -> completionCalled.get().equalsIgnoreCase("hello")); 140 | } 141 | 142 | @Test 143 | @Repeat(5) 144 | public void testWithUserFutureWithCustomPredicateOk() { 145 | breaker = CircuitBreaker.create("test", vertx).failurePolicy(ar -> { 146 | return ar.failed() && ar.cause().getStackTrace().length > 0; 147 | }); 148 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 149 | 150 | AtomicBoolean operationCalled = new AtomicBoolean(); 151 | AtomicReference completionCalled = new AtomicReference<>(); 152 | 153 | Promise userFuture = Promise.promise(); 154 | userFuture.future().onComplete(ar -> { 155 | completionCalled.set(ar.cause().getMessage()); 156 | assertTrue(ar.failed()); 157 | }); 158 | 159 | breaker.executeAndReport(userFuture, fut -> { 160 | operationCalled.set(true); 161 | fut.fail("some custom exception"); 162 | }); 163 | 164 | await().until(operationCalled::get); 165 | await().until(() -> completionCalled.get().equalsIgnoreCase("some custom exception")); 166 | } 167 | 168 | @Test 169 | public void testAsynchronousOk() { 170 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()); 171 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 172 | 173 | AtomicBoolean called = new AtomicBoolean(); 174 | AtomicReference result = new AtomicReference<>(); 175 | breaker.execute(future -> 176 | vertx.setTimer(100, l -> { 177 | called.set(true); 178 | future.complete("hello"); 179 | }) 180 | ).onComplete(ar -> result.set(ar.result())); 181 | 182 | await().until(called::get); 183 | await().untilAtomic(result, is("hello")); 184 | } 185 | 186 | @Test 187 | public void testAsynchronousWithCustomPredicateOk() { 188 | breaker = CircuitBreaker.create("test", vertx).failurePolicy(ar -> { 189 | return ar.failed() && ar.cause().getStackTrace().length > 0; 190 | }); 191 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 192 | 193 | AtomicBoolean called = new AtomicBoolean(); 194 | AtomicReference result = new AtomicReference<>(); 195 | breaker.execute(future -> 196 | vertx.setTimer(100, l -> { 197 | called.set(true); 198 | future.fail("some custom exception"); 199 | }) 200 | ).onComplete(ar -> { 201 | result.set(ar.cause().getMessage()); 202 | assertTrue(ar.failed()); 203 | }); 204 | ; 205 | 206 | await().until(called::get); 207 | await().untilAtomic(result, is("some custom exception")); 208 | } 209 | 210 | @Test 211 | public void testAsynchronousWithUserFutureOk() { 212 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()); 213 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 214 | 215 | AtomicBoolean called = new AtomicBoolean(); 216 | AtomicReference result = new AtomicReference<>(); 217 | 218 | Promise userFuture = Promise.promise(); 219 | userFuture.future().onComplete(ar -> result.set(ar.result())); 220 | 221 | breaker.executeAndReport(userFuture, future -> 222 | vertx.setTimer(100, l -> { 223 | called.set(true); 224 | future.complete("hello"); 225 | }) 226 | ); 227 | 228 | await().until(called::get); 229 | await().untilAtomic(result, is("hello")); 230 | } 231 | 232 | @Test 233 | public void testAsynchronousWithUserFutureAndWithCustomPredicateOk() { 234 | breaker = CircuitBreaker.create("test", vertx).failurePolicy(ar -> { 235 | return ar.failed() && ar.cause() instanceof ClassNotFoundException; 236 | }); 237 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 238 | 239 | AtomicBoolean called = new AtomicBoolean(); 240 | AtomicReference result = new AtomicReference<>(); 241 | 242 | Promise userFuture = Promise.promise(); 243 | userFuture.future().onComplete(ar -> { 244 | result.set(ar.cause().getMessage()); 245 | assertTrue(ar.failed()); 246 | }); 247 | ; 248 | 249 | breaker.executeAndReport(userFuture, future -> 250 | vertx.setTimer(100, l -> { 251 | called.set(true); 252 | future.fail(new NullPointerException("some custom exception")); 253 | }) 254 | ); 255 | 256 | await().until(called::get); 257 | await().untilAtomic(result, is("some custom exception")); 258 | } 259 | 260 | @Test 261 | public void testRollingWindowFailuresAreDecreased() { 262 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions() 263 | .setMaxFailures(10) 264 | .setFailuresRollingWindow(10000)); 265 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 266 | 267 | IntStream.range(0, 9).forEach(i -> breaker.execute(v -> v.fail(new RuntimeException("oh no, but this is expected")))); 268 | await().until(() -> breaker.failureCount() == 9); 269 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 270 | 271 | await().atMost(11, TimeUnit.SECONDS).until(() -> breaker.failureCount() < 9); 272 | 273 | assertTrue(breaker.failureCount() < 9); 274 | } 275 | 276 | @Test 277 | @Repeat(5) 278 | public void testOpenAndCloseHandler() { 279 | AtomicInteger spyOpen = new AtomicInteger(); 280 | AtomicInteger spyClosed = new AtomicInteger(); 281 | 282 | AtomicReference lastException = new AtomicReference<>(); 283 | 284 | breaker = CircuitBreaker.create("name", vertx, new CircuitBreakerOptions().setResetTimeout(-1)) 285 | .openHandler((v) -> spyOpen.incrementAndGet()) 286 | .closeHandler((v) -> spyClosed.incrementAndGet()); 287 | 288 | assertEquals(0, spyOpen.get()); 289 | assertEquals(0, spyClosed.get()); 290 | 291 | // First failure 292 | breaker.execute(v -> { 293 | throw new RuntimeException("oh no, but this is expected"); 294 | }) 295 | .onComplete(ar -> lastException.set(ar.cause())); 296 | 297 | assertEquals(0, spyOpen.get()); 298 | assertEquals(0, spyClosed.get()); 299 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED); 300 | assertNotNull(lastException.get()); 301 | lastException.set(null); 302 | 303 | for (int i = 1; i < CircuitBreakerOptions.DEFAULT_MAX_FAILURES; i++) { 304 | breaker.execute(v -> { 305 | throw new RuntimeException("oh no, but this is expected"); 306 | }) 307 | .onComplete(ar -> lastException.set(ar.cause())); 308 | } 309 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN); 310 | assertEquals(1, spyOpen.get()); 311 | assertNotNull(lastException.get()); 312 | 313 | ((CircuitBreakerImpl) breaker).reset(true); 314 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 315 | assertEquals(1, spyOpen.get()); 316 | assertEquals(1, spyClosed.get()); 317 | } 318 | 319 | @Test 320 | @Repeat(5) 321 | public void testHalfOpen() { 322 | AtomicBoolean thrown = new AtomicBoolean(false); 323 | Context ctx = vertx.getOrCreateContext() 324 | .exceptionHandler(ex -> // intercept exceptions 325 | thrown.set(true)); 326 | 327 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions() 328 | .setResetTimeout(200) 329 | .setMaxFailures(1)); 330 | 331 | Handler> fail = p -> p.fail("fail"); 332 | Handler> success = Promise::complete; 333 | 334 | ctx.runOnContext(v -> { 335 | breaker.execute(fail); 336 | breaker.execute(fail); 337 | }); 338 | 339 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN); 340 | 341 | ctx.runOnContext(v -> { 342 | breaker.execute(fail); 343 | }); 344 | 345 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN); 346 | 347 | ctx.runOnContext(v -> { 348 | breaker.execute(success); 349 | }); 350 | 351 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED); 352 | 353 | assertFalse(thrown.get()); 354 | } 355 | 356 | @Test 357 | @Repeat(5) 358 | public void testExceptionOnSynchronousCode() { 359 | AtomicBoolean called = new AtomicBoolean(false); 360 | CircuitBreakerOptions options = new CircuitBreakerOptions() 361 | .setFallbackOnFailure(false) 362 | .setResetTimeout(-1); 363 | breaker = CircuitBreaker.create("test", vertx, options) 364 | .fallback(t -> { 365 | called.set(true); 366 | return "fallback"; 367 | }); 368 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 369 | 370 | for (int i = 0; i < options.getMaxFailures(); i++) { 371 | breaker.execute(v -> { 372 | throw new RuntimeException("oh no, but this is expected"); 373 | }); 374 | } 375 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || 376 | breaker.state() == CircuitBreakerState.HALF_OPEN); 377 | assertFalse(called.get()); 378 | 379 | AtomicBoolean spy = new AtomicBoolean(); 380 | breaker.execute(v -> spy.set(true)); 381 | assertFalse(spy.get()); 382 | assertTrue(called.get()); 383 | } 384 | 385 | @Test 386 | @Repeat(5) 387 | public void testExceptionOnSynchronousCodeWithExecute() { 388 | CircuitBreakerOptions options = new CircuitBreakerOptions() 389 | .setFallbackOnFailure(false) 390 | .setResetTimeout(-1); 391 | breaker = CircuitBreaker.create("test", vertx, options) 392 | .fallback(t -> "fallback"); 393 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 394 | 395 | for (int i = 0; i < options.getMaxFailures(); i++) { 396 | Promise future = Promise.promise(); 397 | AtomicReference result = new AtomicReference<>(); 398 | breaker.executeAndReport(future, v -> { 399 | throw new RuntimeException("oh no, but this is expected"); 400 | }); 401 | future.future().onComplete(ar -> result.set(ar.result())); 402 | assertNull(result.get()); 403 | } 404 | 405 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN); 406 | assertEquals(CircuitBreakerState.OPEN, breaker.state()); 407 | 408 | AtomicBoolean spy = new AtomicBoolean(); 409 | AtomicReference result = new AtomicReference<>(); 410 | Promise fut = Promise.promise(); 411 | fut.future().onComplete(ar -> 412 | result.set(ar.result()) 413 | ); 414 | breaker.executeAndReport(fut, v -> spy.set(true)); 415 | assertFalse(spy.get()); 416 | assertEquals("fallback", result.get()); 417 | } 418 | 419 | @Test 420 | public void testFailureOnAsynchronousCode() { 421 | AtomicBoolean called = new AtomicBoolean(false); 422 | AtomicReference result = new AtomicReference<>(); 423 | CircuitBreakerOptions options = new CircuitBreakerOptions().setResetTimeout(-1); 424 | breaker = CircuitBreaker.create("test", vertx, options) 425 | .fallback(v -> { 426 | called.set(true); 427 | return "fallback"; 428 | }); 429 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 430 | 431 | for (int i = 0; i < options.getMaxFailures(); i++) { 432 | breaker.execute( 433 | future -> vertx.setTimer(100, l -> future.fail("expected failure")) 434 | ).onComplete(ar -> result.set(ar.result())); 435 | } 436 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN); 437 | assertFalse(called.get()); 438 | 439 | AtomicBoolean spy = new AtomicBoolean(); 440 | breaker.execute( 441 | future -> vertx.setTimer(100, l -> { 442 | future.fail("expected failure"); 443 | spy.set(true); 444 | })) 445 | .onComplete(ar -> result.set(ar.result())); 446 | await().untilAtomic(called, is(true)); 447 | assertFalse(spy.get()); 448 | assertEquals("fallback", result.get()); 449 | } 450 | 451 | @Test 452 | public void testFailureOnAsynchronousCodeWithCustomPredicate() { 453 | AtomicBoolean called = new AtomicBoolean(false); 454 | AtomicReference result = new AtomicReference<>(); 455 | CircuitBreakerOptions options = new CircuitBreakerOptions().setResetTimeout(-1); 456 | breaker = CircuitBreaker.create("test", vertx, options) 457 | .fallback(v -> { 458 | called.set(true); 459 | return "fallback"; 460 | }) 461 | .failurePolicy(ar -> { 462 | return ar.failed() && ar.cause().getStackTrace().length == 0; 463 | }); 464 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 465 | 466 | for (int i = 0; i < options.getMaxFailures(); i++) { 467 | breaker.execute( 468 | future -> vertx.setTimer(100, l -> future.fail("expected failure")) 469 | ).onComplete(ar -> result.set(ar.result())); 470 | } 471 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN); 472 | assertFalse(called.get()); 473 | 474 | AtomicBoolean spy = new AtomicBoolean(); 475 | breaker.execute( 476 | future -> vertx.setTimer(100, l -> { 477 | future.fail("expected failure"); 478 | spy.set(true); 479 | })) 480 | .onComplete(ar -> { 481 | result.set(ar.result()); 482 | }); 483 | ; 484 | await().untilAtomic(called, is(true)); 485 | assertFalse(spy.get()); 486 | assertEquals("fallback", result.get()); 487 | } 488 | 489 | @Test 490 | @Repeat(5) 491 | public void testResetAttempt() { 492 | AtomicBoolean called = new AtomicBoolean(false); 493 | CircuitBreakerOptions options = new CircuitBreakerOptions().setResetTimeout(100); 494 | breaker = CircuitBreaker.create("test", vertx, options) 495 | .fallback(v -> { 496 | called.set(true); 497 | return "fallback"; 498 | }); 499 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 500 | 501 | for (int i = 0; i < options.getMaxFailures(); i++) { 502 | breaker.execute(v -> { 503 | throw new RuntimeException("oh no, but this is expected"); 504 | }); 505 | } 506 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN); 507 | assertFalse(called.get()); 508 | 509 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN); 510 | 511 | AtomicBoolean spy = new AtomicBoolean(); 512 | breaker.execute(v -> { 513 | spy.set(true); 514 | v.complete(); 515 | }); 516 | assertTrue(spy.get()); 517 | assertFalse(called.get()); 518 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED); 519 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 520 | } 521 | 522 | @Test 523 | @Repeat(5) 524 | public void testResetAttemptThatFails() { 525 | AtomicBoolean called = new AtomicBoolean(false); 526 | CircuitBreakerOptions options = new CircuitBreakerOptions() 527 | .setResetTimeout(100) 528 | .setFallbackOnFailure(true); 529 | breaker = CircuitBreaker.create("test", vertx, options) 530 | .fallback(v -> { 531 | called.set(true); 532 | return "fallback"; 533 | }); 534 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED); 535 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 536 | 537 | for (int i = 0; i < options.getMaxFailures(); i++) { 538 | breaker.execute(v -> { 539 | throw new RuntimeException("oh no, but this is expected"); 540 | }); 541 | } 542 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN); 543 | assertTrue(called.get()); 544 | 545 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN); 546 | called.set(false); 547 | 548 | AtomicReference result = new AtomicReference<>(); 549 | breaker.execute(v -> { 550 | throw new RuntimeException("oh no, but this is expected"); 551 | }).onComplete(ar -> result.set(ar.result())); 552 | 553 | await().until(called::get); 554 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN); 555 | assertEquals("fallback", result.get()); 556 | } 557 | 558 | @Test 559 | public void testTimeout() { 560 | AtomicBoolean called = new AtomicBoolean(false); 561 | CircuitBreakerOptions options = new CircuitBreakerOptions().setTimeout(100); 562 | breaker = CircuitBreaker.create("test", vertx, options) 563 | .fallback(v -> { 564 | called.set(true); 565 | return "fallback"; 566 | }); 567 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 568 | 569 | AtomicInteger failureCount = new AtomicInteger(); 570 | for (int i = 0; i < options.getMaxFailures(); i++) { 571 | try { 572 | breaker.execute(v -> { 573 | vertx.setTimer(500, id -> { 574 | v.complete("done"); 575 | }); 576 | }).onComplete(ar -> { 577 | if (ar.failed()) failureCount.incrementAndGet(); 578 | }).await(); 579 | } catch (TimeoutException e) { 580 | // Timeout 581 | } 582 | } 583 | 584 | assertEquals(CircuitBreakerState.OPEN, breaker.state()); 585 | assertFalse(called.get()); 586 | assertEquals(options.getMaxFailures(), failureCount.get()); 587 | 588 | AtomicBoolean spy = new AtomicBoolean(); 589 | AtomicReference result = new AtomicReference<>(); 590 | breaker.execute(v -> { 591 | spy.set(true); 592 | v.complete(); 593 | }) 594 | .onComplete(ar -> result.set(ar.result())); 595 | assertFalse(spy.get()); 596 | assertTrue(called.get()); 597 | assertEquals("fallback", result.get()); 598 | } 599 | 600 | @Test 601 | public void testTimeoutWithFallbackCalled() { 602 | AtomicBoolean called = new AtomicBoolean(false); 603 | CircuitBreakerOptions options = new CircuitBreakerOptions().setTimeout(100) 604 | .setResetTimeout(5000) 605 | .setFallbackOnFailure(true); 606 | breaker = CircuitBreaker.create("test", vertx, options) 607 | .fallback(v -> { 608 | called.set(true); 609 | return "fallback"; 610 | }); 611 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 612 | 613 | AtomicInteger count = new AtomicInteger(); 614 | for (int i = 0; i < options.getMaxFailures() + 3; i++) { 615 | breaker.execute(v -> { 616 | vertx.setTimer(500, id -> { 617 | v.complete("done"); 618 | }); 619 | }).onComplete(ar -> { 620 | if (ar.result().equals("fallback")) { 621 | count.incrementAndGet(); 622 | } 623 | }).await(); 624 | } 625 | 626 | assertEquals(CircuitBreakerState.OPEN, breaker.state()); 627 | assertTrue(called.get()); 628 | assertEquals(options.getMaxFailures() + 3, count.get()); 629 | } 630 | 631 | @Test 632 | public void testResetAttemptOnTimeout() { 633 | AtomicBoolean called = new AtomicBoolean(false); 634 | AtomicBoolean hasBeenOpened = new AtomicBoolean(false); 635 | CircuitBreakerOptions options = new CircuitBreakerOptions() 636 | .setResetTimeout(100) 637 | .setTimeout(10) 638 | .setFallbackOnFailure(true); 639 | breaker = CircuitBreaker.create("test", vertx, options) 640 | .fallback(v -> { 641 | called.set(true); 642 | return "fallback"; 643 | }) 644 | .openHandler(v -> hasBeenOpened.set(true)); 645 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 646 | 647 | for (int i = 0; i < options.getMaxFailures(); i++) { 648 | breaker.execute(future -> { 649 | // Do nothing with the future, this is a very bad thing. 650 | }); 651 | } 652 | await().untilAtomic(hasBeenOpened, is(true)); 653 | assertTrue(called.get()); 654 | 655 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN); 656 | called.set(false); 657 | 658 | breaker.execute(Promise::complete); 659 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED); 660 | await().untilAtomic(called, is(false)); 661 | } 662 | 663 | @Test 664 | @Repeat(10) 665 | public void testResetAttemptThatFailsOnTimeout() { 666 | AtomicBoolean called = new AtomicBoolean(false); 667 | AtomicBoolean hasBeenOpened = new AtomicBoolean(false); 668 | CircuitBreakerOptions options = new CircuitBreakerOptions() 669 | .setResetTimeout(100) 670 | .setTimeout(10) 671 | .setFallbackOnFailure(true); 672 | breaker = CircuitBreaker.create("test", vertx, options) 673 | .fallback(v -> { 674 | called.set(true); 675 | return "fallback"; 676 | }) 677 | .openHandler(v -> hasBeenOpened.set(true)); 678 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 679 | 680 | for (int i = 0; i < options.getMaxFailures(); i++) { 681 | breaker.execute(future -> { 682 | // Do nothing with the future, this is a very bad thing. 683 | }); 684 | } 685 | await().untilAtomic(hasBeenOpened, is(true)); 686 | assertTrue(called.get()); 687 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN); 688 | hasBeenOpened.set(false); 689 | called.set(false); 690 | 691 | breaker.execute(future -> { 692 | // Do nothing with the future, this is a very bad thing. 693 | }); 694 | // Failed again, open circuit 695 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN); 696 | await().untilAtomic(called, is(true)); 697 | await().untilAtomic(hasBeenOpened, is(true)); 698 | 699 | hasBeenOpened.set(false); 700 | called.set(false); 701 | 702 | breaker.execute(future -> { 703 | // Do nothing with the future, this is a very bad thing. 704 | }); 705 | // Failed again, open circuit 706 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN || breaker.state() == CircuitBreakerState.HALF_OPEN); 707 | await().untilAtomic(called, is(true)); 708 | await().untilAtomic(hasBeenOpened, is(true)); 709 | 710 | hasBeenOpened.set(false); 711 | called.set(false); 712 | 713 | hasBeenOpened.set(false); 714 | called.set(false); 715 | 716 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED || breaker.state() == CircuitBreakerState.HALF_OPEN); 717 | 718 | // If HO - need to get next request executed and wait until we are closed 719 | breaker.execute(Promise::complete); 720 | await().until(() -> { 721 | if (breaker.state() == CircuitBreakerState.CLOSED) { 722 | return true; 723 | } else { 724 | breaker.execute(Promise::complete); 725 | return false; 726 | } 727 | }); 728 | called.set(false); 729 | for (int i = 0; i < options.getMaxFailures(); i++) { 730 | breaker.execute(f -> f.complete(null)); 731 | } 732 | 733 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED); 734 | await().untilAtomic(hasBeenOpened, is(false)); 735 | } 736 | 737 | @Test 738 | public void testThatOnlyOneRequestIsCheckedInHalfOpen() { 739 | AtomicBoolean called = new AtomicBoolean(false); 740 | AtomicBoolean hasBeenOpened = new AtomicBoolean(false); 741 | CircuitBreakerOptions options = new CircuitBreakerOptions() 742 | .setResetTimeout(1000) 743 | .setFallbackOnFailure(true); 744 | breaker = CircuitBreaker.create("test", vertx, options) 745 | .fallback(v -> { 746 | called.set(true); 747 | return "fallback"; 748 | }) 749 | .openHandler(v -> hasBeenOpened.set(true)); 750 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 751 | 752 | for (int i = 0; i < options.getMaxFailures(); i++) { 753 | breaker.execute(future -> future.fail("expected failure")); 754 | } 755 | await().untilAtomic(hasBeenOpened, is(true)); 756 | assertTrue(called.get()); 757 | 758 | await().until(() -> breaker.state() == CircuitBreakerState.HALF_OPEN); 759 | called.set(false); 760 | 761 | AtomicInteger fallbackCalled = new AtomicInteger(); 762 | for (int i = 0; i < options.getMaxFailures(); i++) { 763 | breaker.executeWithFallback( 764 | future -> vertx.setTimer(500, l -> future.complete()), 765 | v -> { 766 | fallbackCalled.incrementAndGet(); 767 | return "fallback"; 768 | }); 769 | } 770 | 771 | await().until(() -> breaker.state() == CircuitBreakerState.CLOSED); 772 | assertEquals(options.getMaxFailures() - 1, fallbackCalled.get()); 773 | } 774 | 775 | @Test 776 | public void testFailureWhenThereIsNoFallback() { 777 | CircuitBreakerOptions options = new CircuitBreakerOptions() 778 | .setResetTimeout(50000) 779 | .setTimeout(300) 780 | .setFallbackOnFailure(true); 781 | breaker = CircuitBreaker.create("test", vertx, options); 782 | 783 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 784 | 785 | List> results = new ArrayList<>(); 786 | for (int i = 0; i < options.getMaxFailures(); i++) { 787 | breaker.execute(future -> future.fail("expected failure")) 788 | .onComplete(ar -> results.add(ar)); 789 | } 790 | await().until(() -> results.size() == options.getMaxFailures()); 791 | results.forEach(ar -> { 792 | assertTrue(ar.failed()); 793 | assertNotNull(ar.cause()); 794 | assertEquals("expected failure", ar.cause().getMessage()); 795 | }); 796 | 797 | results.clear(); 798 | 799 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN); 800 | breaker.execute(future -> future.fail("expected failure")) 801 | .onComplete(ar -> results.add(ar)); 802 | await().until(() -> results.size() == 1); 803 | results.forEach(ar -> { 804 | assertTrue(ar.failed()); 805 | assertNotNull(ar.cause()); 806 | assertTrue(ar.cause() instanceof OpenCircuitException); 807 | assertEquals("open circuit", ar.cause().getMessage()); 808 | }); 809 | 810 | ((CircuitBreakerImpl) breaker).reset(true); 811 | 812 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 813 | results.clear(); 814 | 815 | breaker.execute(future -> { 816 | try { 817 | Thread.sleep(500); 818 | } catch (InterruptedException e) { 819 | // Ignored. 820 | } 821 | }) 822 | .onComplete(ar -> results.add(ar)); 823 | await().until(() -> results.size() == 1); 824 | results.forEach(ar -> { 825 | assertTrue(ar.failed()); 826 | assertNotNull(ar.cause()); 827 | assertTrue(ar.cause() instanceof TimeoutException); 828 | assertEquals("operation timeout", ar.cause().getMessage()); 829 | }); 830 | } 831 | 832 | @Test 833 | public void testWhenFallbackThrowsAnException() { 834 | CircuitBreakerOptions options = new CircuitBreakerOptions() 835 | .setResetTimeout(5000) 836 | .setFallbackOnFailure(true); 837 | breaker = CircuitBreaker.create("test", vertx, options); 838 | 839 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 840 | 841 | List> results = new ArrayList<>(); 842 | for (int i = 0; i < options.getMaxFailures(); i++) { 843 | breaker.executeWithFallback( 844 | future -> future.fail("expected failure"), 845 | t -> { 846 | throw new RuntimeException("boom"); 847 | }) 848 | .onComplete(ar -> results.add(ar)); 849 | } 850 | await().until(() -> results.size() == options.getMaxFailures()); 851 | results.forEach(ar -> { 852 | assertTrue(ar.failed()); 853 | assertNotNull(ar.cause()); 854 | assertEquals("boom", ar.cause().getMessage()); 855 | }); 856 | 857 | results.clear(); 858 | 859 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN); 860 | breaker.executeWithFallback( 861 | future -> future.fail("expected failure"), 862 | t -> { 863 | throw new RuntimeException("boom"); 864 | }) 865 | .onComplete(ar -> results.add(ar)); 866 | await().until(() -> results.size() == 1); 867 | results.forEach(ar -> { 868 | assertTrue(ar.failed()); 869 | assertNotNull(ar.cause()); 870 | assertEquals("boom", ar.cause().getMessage()); 871 | }); 872 | } 873 | 874 | 875 | @Test 876 | public void testTheExceptionReceivedByFallback() { 877 | CircuitBreakerOptions options = new CircuitBreakerOptions() 878 | .setResetTimeout(50000) 879 | .setTimeout(300) 880 | .setFallbackOnFailure(true); 881 | List failures = new ArrayList<>(); 882 | 883 | breaker = CircuitBreaker.create("test", vertx, options) 884 | .fallback(failures::add); 885 | 886 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 887 | 888 | for (int i = 0; i < options.getMaxFailures(); i++) { 889 | breaker.execute(future -> future.fail("expected failure")); 890 | } 891 | await().until(() -> failures.size() == options.getMaxFailures()); 892 | failures.forEach(ar -> { 893 | assertNotNull(ar); 894 | assertEquals("expected failure", ar.getMessage()); 895 | }); 896 | 897 | failures.clear(); 898 | 899 | ((CircuitBreakerImpl) breaker).reset(true); 900 | 901 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 902 | failures.clear(); 903 | 904 | breaker.execute(future -> { 905 | try { 906 | Thread.sleep(500); 907 | } catch (InterruptedException e) { 908 | // Ignored. 909 | } 910 | }); 911 | await().until(() -> failures.size() == 1); 912 | failures.forEach(ar -> { 913 | assertNotNull(ar); 914 | assertTrue(ar instanceof TimeoutException); 915 | assertEquals("operation timeout", ar.getMessage()); 916 | }); 917 | 918 | ((CircuitBreakerImpl) breaker).reset(true); 919 | 920 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 921 | failures.clear(); 922 | 923 | breaker.execute(future -> { 924 | throw new RuntimeException("boom"); 925 | }); 926 | await().until(() -> failures.size() == 1); 927 | failures.forEach(ar -> { 928 | assertNotNull(ar); 929 | assertEquals("boom", ar.getMessage()); 930 | }); 931 | } 932 | 933 | @Test 934 | @Repeat(5) 935 | public void testRetries() { 936 | CircuitBreakerOptions options = new CircuitBreakerOptions().setMaxRetries(5).setMaxFailures(4).setTimeout(100) 937 | .setFallbackOnFailure(true); 938 | List failures = new ArrayList<>(); 939 | 940 | AtomicInteger calls = new AtomicInteger(); 941 | breaker = CircuitBreaker.create("test", vertx, options); 942 | 943 | 944 | final AtomicReference result = new AtomicReference<>(); 945 | vertx.runOnContext(v -> { 946 | result.set(breaker.execute(future -> { 947 | calls.incrementAndGet(); 948 | future.fail("boom"); 949 | })); 950 | }); 951 | 952 | await().untilAtomic(calls, is(6)); 953 | assertTrue(result.get().failed()); 954 | assertEquals(1, breaker.failureCount()); 955 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 956 | 957 | ((CircuitBreakerImpl) breaker).reset(true); 958 | calls.set(0); 959 | result.set(null); 960 | 961 | vertx.runOnContext(v -> { 962 | result.set(breaker.execute(future -> { 963 | if (calls.incrementAndGet() >= 4) { 964 | future.complete(); 965 | } else { 966 | future.fail("boom"); 967 | } 968 | })); 969 | }); 970 | 971 | 972 | await().untilAtomic(calls, is(4)); 973 | assertTrue(result.get().succeeded()); 974 | assertEquals(0, breaker.failureCount()); 975 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 976 | 977 | ((CircuitBreakerImpl) breaker).reset(true); 978 | calls.set(0); 979 | 980 | vertx.runOnContext(v -> { 981 | for (int i = 0; i < options.getMaxFailures() + 1; i++) { 982 | breaker.execute(future -> { 983 | calls.incrementAndGet(); 984 | }); 985 | } 986 | }); 987 | 988 | await().until(() -> breaker.state() == CircuitBreakerState.OPEN); 989 | 990 | calls.set(0); 991 | ((CircuitBreakerImpl) breaker).reset(true); 992 | AtomicReference result2 = new AtomicReference<>(); 993 | vertx.runOnContext(v -> { 994 | result2.set(breaker.execute(future -> { 995 | if (calls.incrementAndGet() == 4) { 996 | future.complete(); 997 | } else { 998 | } 999 | })); 1000 | for (int i = 0; i < options.getMaxFailures(); i++) { 1001 | breaker.execute(future -> { 1002 | future.fail("boom"); 1003 | }); 1004 | } 1005 | }); 1006 | 1007 | 1008 | await().until(() -> result2.get() != null && result2.get().failed()); 1009 | assertEquals(options.getMaxFailures() + 1, breaker.failureCount()); 1010 | assertEquals(CircuitBreakerState.OPEN, breaker.state()); 1011 | 1012 | 1013 | ((CircuitBreakerImpl) breaker).reset(true); 1014 | breaker.fallback(failures::add); 1015 | calls.set(0); 1016 | result.set(null); 1017 | 1018 | vertx.runOnContext(v -> { 1019 | result.set(breaker.execute(future -> { 1020 | try { 1021 | Thread.sleep(150); 1022 | } catch (InterruptedException e) { 1023 | Thread.currentThread().interrupt(); 1024 | } 1025 | })); 1026 | }); 1027 | 1028 | 1029 | await().until(() -> failures.size() == 1); 1030 | failures.forEach(ar -> { 1031 | assertNotNull(ar); 1032 | assertTrue(ar instanceof TimeoutException); 1033 | assertEquals("operation timeout", ar.getMessage()); 1034 | }); 1035 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 1036 | 1037 | ((CircuitBreakerImpl) breaker).reset(true); 1038 | calls.set(0); 1039 | result.set(null); 1040 | 1041 | 1042 | vertx.runOnContext(v -> { 1043 | result.set(breaker.execute(future -> { 1044 | if (calls.incrementAndGet() == 4) { 1045 | future.complete(); 1046 | } else { 1047 | try { 1048 | Thread.sleep(150); 1049 | } catch (InterruptedException e) { 1050 | Thread.currentThread().interrupt(); 1051 | } 1052 | } 1053 | })); 1054 | }); 1055 | 1056 | 1057 | await().untilAtomic(calls, is(4)); 1058 | assertTrue(result.get().succeeded()); 1059 | assertEquals(0, breaker.failureCount()); 1060 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 1061 | 1062 | } 1063 | 1064 | @Test(expected = IllegalArgumentException.class) 1065 | public void testInvalidBucketSize() { 1066 | CircuitBreakerOptions options = new CircuitBreakerOptions() 1067 | .setNotificationAddress(CircuitBreakerOptions.DEFAULT_NOTIFICATION_ADDRESS) 1068 | .setMetricsRollingBuckets(7); 1069 | CircuitBreaker.create("test", vertx, options); 1070 | } 1071 | 1072 | @Test 1073 | public void operationTimersShouldBeRemovedToAvoidOOM(TestContext ctx) { 1074 | breaker = CircuitBreaker.create("cb", vertx, new CircuitBreakerOptions().setTimeout(600_000)); 1075 | Async async = ctx.async(3000); 1076 | long id = vertx.setPeriodic(1, l -> { 1077 | breaker.execute(prom -> prom.complete(new byte[10 * 1024 * 1024])).onSuccess(v -> async.countDown()).onFailure(ctx::fail); 1078 | }); 1079 | async.await(); 1080 | // Test will throw OOM if operation timers are not removed 1081 | } 1082 | } 1083 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/CircuitBreakerMetricsTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker.tests.impl; 2 | 3 | import io.vertx.circuitbreaker.CircuitBreaker; 4 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 5 | import io.vertx.circuitbreaker.CircuitBreakerState; 6 | import io.vertx.circuitbreaker.impl.CircuitBreakerImpl; 7 | import io.vertx.core.*; 8 | import io.vertx.core.json.JsonObject; 9 | import io.vertx.ext.unit.Async; 10 | import io.vertx.ext.unit.TestContext; 11 | import io.vertx.ext.unit.junit.Repeat; 12 | import io.vertx.ext.unit.junit.RepeatRule; 13 | import io.vertx.ext.unit.junit.VertxUnitRunner; 14 | import org.junit.After; 15 | import org.junit.Before; 16 | import org.junit.Rule; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.concurrent.atomic.AtomicBoolean; 23 | import java.util.stream.IntStream; 24 | 25 | import static org.awaitility.Awaitility.await; 26 | import static java.util.stream.Collectors.collectingAndThen; 27 | import static java.util.stream.Collectors.toList; 28 | import static org.hamcrest.core.Is.is; 29 | import static org.junit.Assert.assertEquals; 30 | import static org.junit.Assert.assertTrue; 31 | 32 | /** 33 | * @author Clement Escoffier 34 | */ 35 | @RunWith(VertxUnitRunner.class) 36 | public class CircuitBreakerMetricsTest { 37 | 38 | 39 | private Vertx vertx; 40 | private CircuitBreaker breaker; 41 | 42 | @Rule 43 | public RepeatRule rule = new RepeatRule(); 44 | 45 | 46 | @Before 47 | public void setUp(TestContext tc) { 48 | vertx = Vertx.vertx(); 49 | vertx.exceptionHandler(tc.exceptionHandler()); 50 | } 51 | 52 | @After 53 | public void tearDown() { 54 | vertx.exceptionHandler(null); 55 | if (breaker != null) { 56 | breaker.close(); 57 | } 58 | AtomicBoolean completed = new AtomicBoolean(); 59 | vertx.close().onComplete(ar -> completed.set(ar.succeeded())); 60 | await().untilAtomic(completed, is(true)); 61 | } 62 | 63 | 64 | @Test 65 | @Repeat(10) 66 | public void testWithSuccessfulCommands(TestContext tc) { 67 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions()); 68 | Async async = tc.async(); 69 | 70 | 71 | Future command1 = breaker.execute(commandThatWorks()); 72 | Future command2 = breaker.execute(commandThatWorks()); 73 | Future command3 = breaker.execute(commandThatWorks()); 74 | 75 | Future.all(command1, command2, command3) 76 | .onComplete(ar -> { 77 | assertTrue(ar.succeeded()); 78 | assertEquals("some-circuit-breaker", metrics().getString("name")); 79 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state")); 80 | assertEquals(0, (int)metrics().getInteger("failures")); 81 | assertEquals(0, (int)metrics().getInteger("totalErrorCount")); 82 | assertEquals(3, (int)metrics().getInteger("totalSuccessCount")); 83 | assertEquals(0, (int)metrics().getInteger("totalTimeoutCount")); 84 | assertEquals(0, (int)metrics().getInteger("totalExceptionCount")); 85 | assertEquals(0, (int)metrics().getInteger("totalFailureCount")); 86 | assertEquals(100, (int)metrics().getInteger("totalSuccessPercentage")); 87 | assertEquals(0, (int)metrics().getInteger("totalErrorPercentage")); 88 | async.complete(); 89 | }); 90 | } 91 | 92 | private CircuitBreakerOptions getOptions() { 93 | return new CircuitBreakerOptions() 94 | .setNotificationAddress(CircuitBreakerOptions.DEFAULT_NOTIFICATION_ADDRESS); 95 | } 96 | 97 | @Test 98 | @Repeat(10) 99 | public void testWithFailedCommands(TestContext tc) { 100 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions()); 101 | Async async = tc.async(); 102 | 103 | Future command1 = breaker.execute(commandThatFails()); 104 | Future command2 = breaker.execute(commandThatWorks()); 105 | Future command3 = breaker.execute(commandThatWorks()); 106 | Future command4 = breaker.execute(commandThatFails()); 107 | 108 | Future.join(command1, command2, command3, command4) 109 | .onComplete(ar -> { 110 | assertEquals("some-circuit-breaker", metrics().getString("name")); 111 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state")); 112 | assertEquals(2, (int)metrics().getInteger("totalErrorCount")); 113 | assertEquals(2, (int)metrics().getInteger("totalSuccessCount")); 114 | assertEquals(0, (int)metrics().getInteger("totalTimeoutCount")); 115 | assertEquals(0, (int)metrics().getInteger("totalExceptionCount")); 116 | assertEquals(2, (int)metrics().getInteger("totalFailureCount")); 117 | assertEquals(4, (int)metrics().getInteger("totalOperationCount")); 118 | assertEquals(50, (int)metrics().getInteger("totalSuccessPercentage")); 119 | assertEquals(50, (int)metrics().getInteger("totalErrorPercentage")); 120 | async.complete(); 121 | }); 122 | } 123 | 124 | @Test 125 | @Repeat(10) 126 | public void testWithCrashingCommands(TestContext tc) { 127 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions()); 128 | Async async = tc.async(); 129 | 130 | Future command1 = breaker.execute(commandThatFails()); 131 | Future command2 = breaker.execute(commandThatWorks()); 132 | Future command3 = breaker.execute(commandThatWorks()); 133 | Future command4 = breaker.execute(commandThatFails()); 134 | Future command5 = breaker.execute(commandThatCrashes()); 135 | 136 | Future.join(command1, command2, command3, command4, command5) 137 | .onComplete(ar -> { 138 | assertEquals("some-circuit-breaker", metrics().getString("name")); 139 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state")); 140 | assertEquals(3, (int)metrics().getInteger("totalErrorCount")); 141 | assertEquals(2, (int)metrics().getInteger("totalSuccessCount")); 142 | assertEquals(0, (int)metrics().getInteger("totalTimeoutCount")); 143 | assertEquals(1, (int)metrics().getInteger("totalExceptionCount")); 144 | assertEquals(2, (int)metrics().getInteger("totalFailureCount")); 145 | assertEquals(5, (int)metrics().getInteger("totalOperationCount")); 146 | assertEquals((2.0 / 5 * 100), (float)metrics().getFloat("totalSuccessPercentage"), 0.1); 147 | assertEquals((3.0 / 5 * 100), (float)metrics().getFloat("totalErrorPercentage"), 0.1); 148 | async.complete(); 149 | }); 150 | } 151 | 152 | @Test 153 | @Repeat(10) 154 | public void testWithTimeoutCommands(TestContext tc) { 155 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions().setTimeout(100)); 156 | Async async = tc.async(); 157 | 158 | Future command1 = breaker.execute(commandThatFails()); 159 | Future command2 = breaker.execute(commandThatWorks()); 160 | Future command3 = breaker.execute(commandThatWorks()); 161 | Future command4 = breaker.execute(commandThatFails()); 162 | Future command5 = breaker.execute(commandThatTimeout(100)); 163 | 164 | Future.join(command1, command2, command3, command4, command5) 165 | .onComplete(ar -> { 166 | assertEquals("some-circuit-breaker", metrics().getString("name")); 167 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state")); 168 | assertEquals(3, (int)metrics().getInteger("totalErrorCount")); 169 | assertEquals(2, (int)metrics().getInteger("totalSuccessCount")); 170 | assertEquals(1, (int)metrics().getInteger("totalTimeoutCount")); 171 | assertEquals(0, (int)metrics().getInteger("totalExceptionCount")); 172 | assertEquals(2, (int)metrics().getInteger("totalFailureCount")); 173 | assertEquals(5, (int)metrics().getInteger("totalOperationCount")); 174 | assertEquals((2.0 / 5 * 100), (float)metrics().getFloat("totalSuccessPercentage"), 0.1); 175 | assertEquals((3.0 / 5 * 100), (float)metrics().getFloat("totalErrorPercentage"), 0.1); 176 | async.complete(); 177 | }); 178 | } 179 | 180 | 181 | @Test 182 | @Repeat(10) 183 | public void testLatencyComputation(TestContext tc) { 184 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions()); 185 | Async async = tc.async(); 186 | 187 | 188 | int count = 1000; 189 | 190 | IntStream.range(0, count) 191 | .mapToObj(i -> breaker.execute(commandThatWorks())) 192 | .collect(collectingAndThen(toList(), Future::all)) 193 | .onComplete(ar -> { 194 | assertTrue(ar.succeeded()); 195 | assertEquals("some-circuit-breaker", metrics().getString("name")); 196 | assertEquals(CircuitBreakerState.CLOSED.name(), metrics().getString("state")); 197 | assertEquals(0, (int)metrics().getInteger("failures")); 198 | assertEquals(0, (int)metrics().getInteger("totalErrorCount")); 199 | assertEquals(count, (int)metrics().getInteger("totalSuccessCount")); 200 | assertEquals(0, (int)metrics().getInteger("totalTimeoutCount")); 201 | assertEquals(0, (int)metrics().getInteger("totalExceptionCount")); 202 | assertEquals(0, (int)metrics().getInteger("totalFailureCount")); 203 | assertEquals(count, (int)metrics().getInteger("totalOperationCount")); 204 | assertEquals(100, metrics().getFloat("totalSuccessPercentage"), 0.1); 205 | assertEquals(0, metrics().getFloat("totalErrorPercentage"), 0.1); 206 | async.complete(); 207 | }); 208 | } 209 | 210 | @Test 211 | @Repeat(100) 212 | public void testEviction(TestContext tc) { 213 | breaker = CircuitBreaker.create("some-circuit-breaker", vertx, getOptions().setMetricsRollingWindow(10)); 214 | Async async = tc.async(); 215 | 216 | 217 | int count = 1000; 218 | 219 | List> list = new ArrayList<>(); 220 | for (int i = 0; i < count; i++) { 221 | list.add(breaker.execute(commandThatWorks())); 222 | } 223 | 224 | Future.all(list) 225 | .onComplete(ar -> { 226 | assertTrue(ar.succeeded()); 227 | assertEquals(1000, (int)metrics().getInteger("totalOperationCount")); 228 | assertTrue(metrics().getInteger("rollingOperationCount") <= 1000); 229 | async.complete(); 230 | }); 231 | } 232 | 233 | 234 | private Handler> commandThatWorks() { 235 | return (future -> vertx.setTimer(5, l -> future.complete(null))); 236 | } 237 | 238 | private Handler> commandThatFails() { 239 | return (future -> vertx.setTimer(5, l -> future.fail("expected failure"))); 240 | } 241 | 242 | private Handler> commandThatCrashes() { 243 | return (future -> { 244 | throw new RuntimeException("Expected error"); 245 | }); 246 | } 247 | 248 | private Handler> commandThatTimeout(int timeout) { 249 | return (future -> vertx.setTimer(timeout + 500, l -> future.complete(null))); 250 | } 251 | 252 | private JsonObject metrics() { 253 | return ((CircuitBreakerImpl) breaker).getMetrics(); 254 | } 255 | 256 | } 257 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/CircuitBreakerWithHTTPTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.vertx.circuitbreaker.tests.impl; 18 | 19 | import io.vertx.circuitbreaker.CircuitBreaker; 20 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 21 | import io.vertx.circuitbreaker.CircuitBreakerState; 22 | import io.vertx.core.Future; 23 | import io.vertx.core.Promise; 24 | import io.vertx.core.Vertx; 25 | import io.vertx.core.VertxException; 26 | import io.vertx.core.buffer.Buffer; 27 | import io.vertx.core.http.HttpClient; 28 | import io.vertx.core.http.HttpClientRequest; 29 | import io.vertx.core.http.HttpClientResponse; 30 | import io.vertx.core.http.HttpMethod; 31 | import io.vertx.core.http.HttpServer; 32 | import org.junit.After; 33 | import org.junit.Before; 34 | import org.junit.Ignore; 35 | import org.junit.Test; 36 | 37 | import java.time.Duration; 38 | import java.time.LocalDateTime; 39 | import java.util.ArrayList; 40 | import java.util.Collections; 41 | import java.util.List; 42 | import java.util.concurrent.atomic.AtomicBoolean; 43 | import java.util.concurrent.atomic.AtomicInteger; 44 | 45 | import static org.awaitility.Awaitility.*; 46 | import static io.vertx.core.http.HttpHeaders.*; 47 | import static java.util.concurrent.TimeUnit.*; 48 | import static org.hamcrest.core.Is.*; 49 | import static org.junit.Assert.*; 50 | 51 | /** 52 | * Test the circuit breaker when doing HTTP calls. 53 | * 54 | * @author Clement Escoffier 55 | */ 56 | public class CircuitBreakerWithHTTPTest { 57 | private Vertx vertx; 58 | private HttpServer http; 59 | private HttpClient client; 60 | private CircuitBreaker breaker; 61 | 62 | @Before 63 | public void setUp() throws Exception { 64 | vertx = Vertx.vertx(); 65 | AtomicBoolean invoked = new AtomicBoolean(); 66 | http = vertx 67 | .createHttpServer() 68 | .requestHandler(request -> { 69 | 70 | switch (request.path()) { 71 | case "/": 72 | request.response().end("hello"); 73 | break; 74 | case "/error": 75 | request.response().setStatusCode(500).end("failed !"); 76 | break; 77 | case "/long": 78 | try { 79 | Thread.sleep(2000); 80 | } catch (Exception e) { 81 | // Ignored. 82 | } 83 | request.response().end("hello"); 84 | break; 85 | case "/flaky": 86 | if (invoked.compareAndSet(false, true)) { 87 | request.response().setStatusCode(503).putHeader(RETRY_AFTER, "2").end(); 88 | } else { 89 | request.response().setStatusCode(200).end(); 90 | } 91 | break; 92 | } 93 | }) 94 | .listen(8080) 95 | .await(20, SECONDS); 96 | client = vertx.createHttpClient(); 97 | } 98 | 99 | @After 100 | public void tearDown() throws Exception { 101 | if (breaker != null) { 102 | breaker.close(); 103 | } 104 | try { 105 | vertx 106 | .close() 107 | .await(20, SECONDS); 108 | } finally { 109 | vertx = null; 110 | http = null; 111 | client = null; 112 | } 113 | } 114 | 115 | @Test 116 | public void testOk() { 117 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions()); 118 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 119 | 120 | Promise result = Promise.promise(); 121 | breaker.executeAndReport(result, v -> client.request(HttpMethod.GET, 8080, "localhost", "/") 122 | .compose(req -> req 123 | .send() 124 | .compose(resp -> resp 125 | .body() 126 | .map(Buffer::toString))) 127 | .onComplete(v)); 128 | 129 | await().until(() -> result.future().result() != null); 130 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 131 | } 132 | 133 | @Test 134 | public void testFailure() { 135 | CircuitBreakerOptions options = new CircuitBreakerOptions(); 136 | breaker = CircuitBreaker.create("test", vertx, options); 137 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 138 | 139 | AtomicInteger count = new AtomicInteger(); 140 | 141 | for (int i = 0; i < options.getMaxFailures(); i++) { 142 | Promise result = Promise.promise(); 143 | breaker.executeAndReport(result, future -> 144 | client.request(HttpMethod.GET, 8080, "localhost", "/error").compose(req -> 145 | req.send().compose(resp -> Future.succeededFuture(resp.statusCode())) 146 | ).onSuccess(sc -> { 147 | if (sc != 200) { 148 | future.fail("http error"); 149 | } else { 150 | future.complete(); 151 | } 152 | count.incrementAndGet(); 153 | }) 154 | ); 155 | } 156 | 157 | await().untilAtomic(count, is(options.getMaxFailures())); 158 | assertEquals(CircuitBreakerState.OPEN, breaker.state()); 159 | 160 | Promise result = Promise.promise(); 161 | breaker.executeAndReportWithFallback(result, future -> 162 | client.request(HttpMethod.GET, 8080, "localhost", "/error") 163 | .compose(req -> req.send().compose(resp -> Future.succeededFuture(resp.statusCode()))) 164 | .onSuccess(sc -> { 165 | if (sc != 200) { 166 | future.fail("http error"); 167 | } else { 168 | future.complete(); 169 | } 170 | }), v -> "fallback"); 171 | 172 | await().until(() -> result.future().result().equals("fallback")); 173 | assertEquals(CircuitBreakerState.OPEN, breaker.state()); 174 | 175 | } 176 | 177 | @Test 178 | public void testTimeout() { 179 | CircuitBreakerOptions options = new CircuitBreakerOptions().setTimeout(100).setMaxFailures(2); 180 | breaker = CircuitBreaker.create("test", vertx, options); 181 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 182 | 183 | AtomicInteger count = new AtomicInteger(); 184 | 185 | for (int i = 0; i < options.getMaxFailures(); i++) { 186 | breaker.execute(future -> 187 | client.request(HttpMethod.GET,8080, "localhost", "/long").compose(req -> 188 | req.send().compose(HttpClientResponse::body).onSuccess(body -> { 189 | count.incrementAndGet(); 190 | future.complete(); 191 | }))); 192 | } 193 | 194 | await().untilAtomic(count, is(options.getMaxFailures())); 195 | assertEquals(CircuitBreakerState.OPEN, breaker.state()); 196 | 197 | Promise result = Promise.promise(); 198 | breaker.executeAndReportWithFallback(result, future -> 199 | client.request(HttpMethod.GET, 8080, "localhost", "/long") 200 | .compose(HttpClientRequest::send).onSuccess(response -> { 201 | System.out.println("Got response"); 202 | future.complete(); 203 | }), v -> "fallback"); 204 | 205 | await().until(() -> result.future().result().equals("fallback")); 206 | assertEquals(CircuitBreakerState.OPEN, breaker.state()); 207 | } 208 | 209 | private static class ServiceUnavailableException extends VertxException { 210 | final int delay; 211 | 212 | ServiceUnavailableException(int delay) { 213 | super("unavailable", true); 214 | this.delay = delay; 215 | } 216 | } 217 | 218 | @Test 219 | public void testUseRetryAfterHeaderValue() { 220 | breaker = CircuitBreaker.create("test", vertx, new CircuitBreakerOptions().setMaxRetries(1)) 221 | .retryPolicy((failure, retryCount) -> { 222 | if (failure instanceof ServiceUnavailableException) { 223 | ServiceUnavailableException sue = (ServiceUnavailableException) failure; 224 | return MILLISECONDS.convert(sue.delay, SECONDS); 225 | } 226 | return 0; 227 | }); 228 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 229 | 230 | List requestLocalDateTimes = Collections.synchronizedList(new ArrayList<>()); 231 | Promise result = Promise.promise(); 232 | breaker.executeAndReport(result, v -> { 233 | requestLocalDateTimes.add(LocalDateTime.now()); 234 | client.request(HttpMethod.GET, 8080, "localhost", "/flaky") 235 | .compose(req -> req 236 | .send() 237 | .compose(resp -> { 238 | if (resp.statusCode() == 503) { 239 | ServiceUnavailableException sue = new ServiceUnavailableException(Integer.parseInt(resp.getHeader(RETRY_AFTER))); 240 | return Future.failedFuture(sue); 241 | } else { 242 | return resp.body().map(Buffer::toString); 243 | } 244 | }) 245 | ) 246 | .onComplete(v); 247 | }); 248 | 249 | await().until(() -> result.future().result() != null); 250 | assertEquals(CircuitBreakerState.CLOSED, breaker.state()); 251 | 252 | assertEquals(2, requestLocalDateTimes.size()); 253 | assertTrue(Duration.between(requestLocalDateTimes.get(0), requestLocalDateTimes.get(1)).toMillis() >= 2000); 254 | } 255 | 256 | } 257 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/MyAsyncOperations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.vertx.circuitbreaker.tests.impl; 18 | 19 | import io.vertx.core.*; 20 | 21 | /** 22 | * Some methods using asynchronous patterns. 23 | * @author Clement Escoffier 24 | */ 25 | public class MyAsyncOperations { 26 | 27 | public static void operation(int a, int b, Completable handler) { 28 | handler.succeed(a + b); 29 | } 30 | 31 | public static void fail(Handler> handler) { 32 | handler.handle(Future.failedFuture("boom")); 33 | } 34 | 35 | public static void operation(Promise future, int a, int b) { 36 | future.complete(a + b); 37 | } 38 | 39 | public static void fail(Promise future) { 40 | future.fail("boom"); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/NumberOfRetryTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker.tests.impl; 2 | 3 | import io.vertx.circuitbreaker.CircuitBreaker; 4 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 5 | import io.vertx.core.Vertx; 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | import static org.awaitility.Awaitility.await; 13 | import static org.hamcrest.Matchers.is; 14 | 15 | /** 16 | * Checks the number of retries. 17 | * 18 | * @author Clement Escoffier 19 | */ 20 | public class NumberOfRetryTest { 21 | 22 | private Vertx vertx; 23 | 24 | @Before 25 | public void setup() { 26 | vertx = Vertx.vertx(); 27 | } 28 | 29 | @After 30 | public void tearDown() { 31 | vertx.close(); 32 | } 33 | 34 | 35 | @Test 36 | public void testWithoutRetry() { 37 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 38 | new CircuitBreakerOptions().setMaxFailures(5)); 39 | AtomicInteger counter = new AtomicInteger(); 40 | 41 | breaker.execute(future -> { 42 | counter.incrementAndGet(); 43 | future.fail("FAILED"); 44 | }).onComplete(ar -> { 45 | 46 | }); 47 | 48 | await().untilAtomic(counter, is(1)); 49 | } 50 | 51 | @Test 52 | public void testWithRetrySetToZero() { 53 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 54 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(0)); 55 | AtomicInteger counter = new AtomicInteger(); 56 | 57 | breaker.execute(future -> { 58 | counter.incrementAndGet(); 59 | future.fail("FAILED"); 60 | }).onComplete(ar -> { 61 | 62 | }); 63 | 64 | await().untilAtomic(counter, is(1)); 65 | } 66 | 67 | @Test 68 | public void testWithRetrySetToOne() { 69 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 70 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(1)); 71 | AtomicInteger counter = new AtomicInteger(); 72 | 73 | breaker.execute(future -> { 74 | counter.incrementAndGet(); 75 | future.fail("FAILED"); 76 | }).onComplete(ar -> { 77 | 78 | }); 79 | 80 | await().untilAtomic(counter, is(2)); 81 | } 82 | 83 | @Test 84 | public void testWithRetrySetToFive() { 85 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 86 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(5)); 87 | AtomicInteger counter = new AtomicInteger(); 88 | 89 | breaker.execute(future -> { 90 | counter.incrementAndGet(); 91 | future.fail("FAILED"); 92 | }).onComplete(ar -> { 93 | 94 | }); 95 | 96 | await().untilAtomic(counter, is(6)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/RetryPolicyTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker.tests.impl; 2 | 3 | import io.vertx.circuitbreaker.CircuitBreaker; 4 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 5 | import io.vertx.circuitbreaker.RetryPolicy; 6 | import io.vertx.core.Vertx; 7 | import org.junit.After; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | import static org.awaitility.Awaitility.*; 14 | import static org.hamcrest.Matchers.*; 15 | 16 | /** 17 | * Checks that retry policy is being applied 18 | */ 19 | public class RetryPolicyTest { 20 | private Vertx vertx; 21 | 22 | @Before 23 | public void setup() { 24 | vertx = Vertx.vertx(); 25 | } 26 | 27 | @After 28 | public void tearDown() { 29 | vertx.close(); 30 | } 31 | 32 | @Test 33 | public void testWithRetryPolicy() { 34 | runRetryPolicyTest(RetryPolicy.linearDelay(100, 10000)); 35 | } 36 | 37 | @Test 38 | public void testWithZeroRetryPolicy() { 39 | runRetryPolicyTest((failure, retryCount) -> 0); 40 | } 41 | 42 | @Test 43 | public void testWithNegativeRetryPolicy() { 44 | runRetryPolicyTest((failure, retryCount) -> -1); 45 | } 46 | 47 | /** 48 | * Helper method to run retry policy tests 49 | */ 50 | private void runRetryPolicyTest(RetryPolicy retryPolicy) { 51 | CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx, 52 | new CircuitBreakerOptions().setMaxFailures(5).setMaxRetries(5)); 53 | AtomicInteger counter = new AtomicInteger(); 54 | AtomicInteger retryPolicyCounter = new AtomicInteger(); 55 | 56 | breaker.retryPolicy((failure, retryCount) -> { 57 | retryPolicyCounter.incrementAndGet(); 58 | return retryPolicy.delay(null, retryCount); 59 | }); 60 | 61 | breaker.execute(future -> { 62 | counter.incrementAndGet(); 63 | future.fail("FAILED"); 64 | }).onComplete(ar -> { 65 | 66 | }); 67 | 68 | await().untilAtomic(counter, is(6)); 69 | await().untilAtomic(retryPolicyCounter, is(5)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/circuitbreaker/tests/impl/UsageTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.circuitbreaker.tests.impl; 2 | 3 | import io.vertx.circuitbreaker.CircuitBreaker; 4 | import io.vertx.circuitbreaker.CircuitBreakerOptions; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Promise; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.buffer.Buffer; 9 | import io.vertx.core.eventbus.Message; 10 | import io.vertx.core.http.HttpClient; 11 | import io.vertx.core.http.HttpHeaders; 12 | import io.vertx.core.http.HttpMethod; 13 | import io.vertx.core.http.HttpServer; 14 | import io.vertx.core.json.JsonObject; 15 | import io.vertx.ext.unit.junit.Repeat; 16 | import io.vertx.ext.unit.junit.RepeatRule; 17 | import io.vertx.ext.unit.junit.VertxUnitRunner; 18 | import org.junit.After; 19 | import org.junit.Before; 20 | import org.junit.Rule; 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | 24 | import java.util.concurrent.ThreadLocalRandom; 25 | import java.util.concurrent.TimeUnit; 26 | import java.util.concurrent.atomic.AtomicReference; 27 | 28 | import static org.awaitility.Awaitility.await; 29 | import static org.hamcrest.CoreMatchers.*; 30 | import static org.junit.Assert.assertEquals; 31 | 32 | /** 33 | * @author Clement Escoffier 34 | */ 35 | @RunWith(VertxUnitRunner.class) 36 | public class UsageTest { 37 | 38 | @Rule 39 | public RepeatRule repeatRule = new RepeatRule(); 40 | 41 | private Vertx vertx; 42 | private CircuitBreaker cb; 43 | private HttpServer server; 44 | 45 | @Before 46 | public void setUp() { 47 | vertx = Vertx.vertx(); 48 | cb = CircuitBreaker.create("circuit-breaker", vertx, new CircuitBreakerOptions() 49 | .setFallbackOnFailure(true) 50 | .setTimeout(500) 51 | .setResetTimeout(1000)); 52 | 53 | vertx.eventBus().consumer("ok", message -> message.reply("OK")); 54 | 55 | vertx.eventBus().consumer("fail", message -> message.fail(100, "Bad bad bad")); 56 | 57 | vertx.eventBus().consumer("exception", message -> { 58 | throw new RuntimeException("RT - Bad bad bad"); 59 | }); 60 | 61 | vertx.eventBus().consumer("timeout", message -> vertx.setTimer(2000, x -> message.reply("Too late"))); 62 | } 63 | 64 | @After 65 | public void tearDown() { 66 | if (server != null) { 67 | server.close().await(); 68 | } 69 | cb.close(); 70 | vertx.close().await(); 71 | } 72 | 73 | @Test 74 | @Repeat(10) 75 | public void testCBWithReadOperation() throws Exception { 76 | server = vertx.createHttpServer().requestHandler(req -> { 77 | switch (req.path()) { 78 | case "/resource": 79 | req.response() 80 | .putHeader(HttpHeaders.CONTENT_TYPE, "application/json") 81 | .end(new JsonObject().put("status", "OK").encode()); 82 | break; 83 | case "/delayed": 84 | vertx.setTimer(2000, id -> { 85 | req.response().end(); 86 | }); 87 | break; 88 | case "/error": 89 | req.response() 90 | .setStatusCode(500) 91 | .end("This is an error"); 92 | break; 93 | } 94 | }).listen(8089) 95 | .await(20, TimeUnit.SECONDS); 96 | 97 | 98 | HttpClient client = vertx.createHttpClient(); 99 | 100 | AtomicReference json = new AtomicReference<>(); 101 | cb.executeWithFallback( 102 | promise -> { 103 | client.request(HttpMethod.GET, 8089, "localhost", "/resource") 104 | .compose(req -> req 105 | .putHeader("Accept", "application/json") 106 | .send().compose(resp -> resp 107 | .body() 108 | .map(Buffer::toJsonObject)) 109 | ).onComplete(promise); 110 | }, 111 | t -> null 112 | ).onComplete(ar -> json.set(ar.result())); 113 | await().atMost(1, TimeUnit.MINUTES).untilAtomic(json, is(notNullValue())); 114 | assertEquals("OK", json.get().getString("status")); 115 | 116 | json.set(null); 117 | cb.executeWithFallback( 118 | promise -> { 119 | client.request(HttpMethod.GET, 8089, "localhost", "/error") 120 | .compose(req -> req 121 | .putHeader("Accept", "application/json") 122 | .send().compose(resp -> { 123 | if (resp.statusCode() != 200) { 124 | return Future.failedFuture("Invalid response"); 125 | } else { 126 | return resp.body().map(Buffer::toJsonObject); 127 | } 128 | }) 129 | ).onComplete(promise); 130 | }, 131 | t -> new JsonObject().put("status", "KO") 132 | ).onComplete(ar -> json.set(ar.result())); 133 | await().untilAtomic(json, is(notNullValue())); 134 | assertEquals("KO", json.get().getString("status")); 135 | 136 | json.set(null); 137 | cb.executeWithFallback( 138 | promise -> { 139 | client.request(HttpMethod.GET, 8089, "localhost", "/delayed") 140 | .compose(req -> req 141 | .putHeader("Accept", "application/json") 142 | .send().compose(resp -> { 143 | if (resp.statusCode() != 200) { 144 | return Future.failedFuture("Invalid response"); 145 | } else { 146 | return resp.body().map(Buffer::toJsonObject); 147 | } 148 | }) 149 | ).onComplete(promise); 150 | }, 151 | t -> new JsonObject().put("status", "KO") 152 | ).onComplete(ar -> json.set(ar.result())); 153 | await().untilAtomic(json, is(notNullValue())); 154 | assertEquals("KO", json.get().getString("status")); 155 | } 156 | 157 | private void asyncWrite(Scenario scenario, Promise promise) { 158 | long delay; 159 | switch (scenario) { 160 | case RUNTIME_EXCEPTION: 161 | throw new RuntimeException("Bad bad bad"); 162 | case TIMEOUT: 163 | delay = 2000; 164 | break; 165 | default: 166 | delay = ThreadLocalRandom.current().nextLong(1, 250); // Must be less than CB timeout 167 | break; 168 | } 169 | 170 | vertx.setTimer(delay, l -> { 171 | if (scenario == Scenario.FAILURE) { 172 | promise.fail("Bad Bad Bad"); 173 | } else { 174 | promise.succeed("foo"); 175 | } 176 | }); 177 | } 178 | 179 | private enum Scenario { 180 | OK, 181 | FAILURE, 182 | RUNTIME_EXCEPTION, 183 | TIMEOUT 184 | } 185 | 186 | @Test 187 | @Repeat(10) 188 | public void testCBWithWriteOperation() { 189 | AtomicReference str = new AtomicReference<>(); 190 | cb.executeWithFallback( 191 | promise -> asyncWrite(Scenario.OK, promise), 192 | t -> "bar" 193 | ).onComplete(ar -> str.set(ar.result())); 194 | await().untilAtomic(str, is(equalTo("foo"))); 195 | 196 | str.set(null); 197 | cb.executeWithFallback( 198 | promise -> asyncWrite(Scenario.FAILURE, promise), 199 | t -> "bar" 200 | ).onComplete(ar -> str.set(ar.result())); 201 | await().untilAtomic(str, is(equalTo("bar"))); 202 | 203 | str.set(null); 204 | cb.executeWithFallback( 205 | promise -> asyncWrite(Scenario.TIMEOUT, promise), 206 | t -> "bar" 207 | ).onComplete(ar -> str.set(ar.result())); 208 | await().untilAtomic(str, is(equalTo("bar"))); 209 | 210 | str.set(null); 211 | cb.executeWithFallback( 212 | promise -> asyncWrite(Scenario.RUNTIME_EXCEPTION, promise), 213 | t -> "bar" 214 | ).onComplete(ar -> str.set(ar.result())); 215 | await().untilAtomic(str, is(equalTo("bar"))); 216 | } 217 | 218 | 219 | @Test 220 | public void testCBWithEventBus() { 221 | AtomicReference str = new AtomicReference<>(); 222 | cb.executeWithFallback( 223 | promise -> vertx.eventBus().request("ok", "").map(Message::body).onComplete(promise), 224 | t -> "KO" 225 | ).onComplete(ar -> str.set(ar.result())); 226 | await().untilAtomic(str, is(equalTo("OK"))); 227 | 228 | str.set(null); 229 | cb.executeWithFallback( 230 | promise -> vertx.eventBus().request("timeout", "").map(Message::body).onComplete(promise), 231 | t -> "KO" 232 | ).onComplete(ar -> str.set(ar.result())); 233 | await().untilAtomic(str, is(equalTo("KO"))); 234 | 235 | str.set(null); 236 | cb.executeWithFallback( 237 | promise -> vertx.eventBus().request("fail", "").map(Message::body).onComplete(promise), 238 | t -> "KO" 239 | ).onComplete(ar -> str.set(ar.result())); 240 | await().untilAtomic(str, is(equalTo("KO"))); 241 | 242 | str.set(null); 243 | cb.executeWithFallback( 244 | promise -> vertx.eventBus().request("exception", "").map(Message::body).onComplete(promise), 245 | t -> "KO" 246 | ).onComplete(ar -> str.set(ar.result())); 247 | await().untilAtomic(str, is(equalTo("KO"))); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/test/java/module-info.java: -------------------------------------------------------------------------------- 1 | open module io.vertx.circuitbreaker.tests { 2 | requires io.vertx.circuitbreaker; 3 | requires io.vertx.core; 4 | requires io.vertx.core.logging; 5 | requires io.vertx.testing.unit; 6 | requires junit; 7 | requires awaitility; 8 | requires org.hamcrest; 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/services/io.vertx.core.spi.JsonFactory: -------------------------------------------------------------------------------- 1 | io.vertx.circuitbreaker.tests.JsonFactory 2 | --------------------------------------------------------------------------------