├── .circleci └── config.yml ├── .github └── release-drafter.yml ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── circuitbreaker.md ├── index.md ├── logo.png ├── ratelimiter.md ├── retry.md ├── template.html └── timeout.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── org │ └── rnorth │ └── ducttape │ ├── Preconditions.java │ ├── RetryCountExceededException.java │ ├── TimeoutException.java │ ├── circuitbreakers │ ├── Breaker.java │ ├── BreakerBuilder.java │ ├── DefaultBreaker.java │ ├── MapBackedStateStore.java │ ├── SimpleStateStore.java │ ├── State.java │ ├── StateStore.java │ └── TimeSource.java │ ├── inconsistents │ ├── InconsistentResultsException.java │ ├── Inconsistents.java │ └── ResultsNeverConsistentException.java │ ├── ratelimits │ ├── ConstantThroughputRateLimiter.java │ ├── RateLimiter.java │ └── RateLimiterBuilder.java │ ├── timeouts │ └── Timeouts.java │ └── unreliables │ └── Unreliables.java └── test └── java └── org └── rnorth └── ducttape ├── CompositeTest.java ├── circuitbreakers ├── AutoResetTest.java ├── ExampleService.java ├── ExampleTest.java ├── ExternalStateTest.java └── SimpleBreakerTest.java ├── inconsistents └── InconsistentsTest.java ├── ratelimits └── RateLimiterTest.java ├── timeouts └── TimeoutsTest.java └── unreliables └── UnreliablesTest.java /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Java Maven CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-java/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: adoptopenjdk/openjdk8 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/postgres:9.4 16 | 17 | working_directory: ~/repo 18 | 19 | environment: 20 | # Customize the JVM maximum heap limit 21 | MAVEN_OPTS: -Xmx3200m 22 | 23 | steps: 24 | - checkout 25 | 26 | # Download and cache dependencies 27 | - restore_cache: 28 | keys: 29 | - v1-dependencies-{{ checksum "pom.xml" }} 30 | # fallback to using the latest cache if no exact match is found 31 | - v1-dependencies- 32 | 33 | - run: ./mvnw dependency:go-offline 34 | 35 | - save_cache: 36 | paths: 37 | - ~/.m2 38 | key: v1-dependencies-{{ checksum "pom.xml" }} 39 | 40 | # run tests! 41 | - run: ./mvnw integration-test 42 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: $NEXT_PATCH_VERSION 2 | tag-template: $NEXT_PATCH_VERSION 3 | template: | 4 | # What's Changed 5 | 6 | $CHANGES 7 | categories: 8 | - title: 🚀 Features 9 | label: type/feature 10 | - title: 🐛 Bug Fixes 11 | label: type/bug 12 | - title: 📖 Documentation 13 | label: type/docs 14 | - title: 📦 Dependency updates 15 | label: dependencies 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .gitignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 4 | 5 | *.iml 6 | 7 | ## Directory-based project format: 8 | .idea/ 9 | # if you remove the above rule, at least ignore the following: 10 | 11 | # User-specific stuff: 12 | # .idea/workspace.xml 13 | # .idea/tasks.xml 14 | # .idea/dictionaries 15 | 16 | # Sensitive or high-churn files: 17 | # .idea/dataSources.ids 18 | # .idea/dataSources.xml 19 | # .idea/sqlDataSources.xml 20 | # .idea/dynamic.xml 21 | # .idea/uiDesigner.xml 22 | 23 | # Gradle: 24 | # .idea/gradle.xml 25 | # .idea/libraries 26 | 27 | # Mongo Explorer plugin: 28 | # .idea/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.ipr 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | 50 | 51 | ### Gradle template 52 | .gradle 53 | build/ 54 | 55 | # Ignore Gradle GUI config 56 | gradle-app.setting 57 | 58 | # VS Code 59 | target/ 60 | .classpath 61 | .vscode/ 62 | .settings/ 63 | .project 64 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.5"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnorth/duct-tape/2a1c5be9f2ef3f16bf036cec8752a170d130b61e/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.1/apache-maven-3.6.1-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Richard North 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See [Releases](https://github.com/rnorth/duct-tape/releases) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Richard North 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Duct Tape 2 | 3 | > Duct Tape is a little Java 8 library providing fault tolerance support for code that calls external APIs and 4 | unreliable components, including those that may fail, time out, or return inconsistent results. 5 | 6 | ![Logo](https://raw.githubusercontent.com/rnorth/duct-tape/master/docs/logo.png) 7 | 8 | This library was formerly named `circuitbreakers`, but given the inclusion of more general purpose fault tolerance 9 | features, it was renamed. 10 | 11 | [![Circle CI](https://circleci.com/gh/rnorth/duct-tape.svg?style=svg)](https://circleci.com/gh/rnorth/duct-tape) 12 | 13 | # [Read the documentation here](http://rnorth.viewdocs.io/duct-tape/) 14 | 15 | ## License 16 | 17 | See [LICENSE](LICENSE). 18 | 19 | ## Copyright 20 | 21 | Copyright (c) 2014-2015 Richard North. 22 | 23 | See [AUTHORS](AUTHORS) for contributors. 24 | -------------------------------------------------------------------------------- /docs/circuitbreaker.md: -------------------------------------------------------------------------------- 1 | # Circuit breakers 2 | 3 | From [Martin Fowler](http://martinfowler.com/bliki/CircuitBreaker.html): 4 | 5 | > The basic idea behind the circuit breaker is very simple. You wrap a protected function call in a circuit breaker 6 | object, which monitors for failures. Once the failures reach a certain threshold, the circuit breaker trips, and all 7 | further calls to the circuit breaker return with an error, without the protected call being made at all. Usually you'll 8 | also want some kind of monitor alert if the circuit breaker trips. 9 | 10 | ## Usage summary 11 | 12 | In this library a breaker exists in one of two states: 13 | 14 | * `OK`: the primary action will be invoked. If it fails by throwing an exception, a one-time failure 15 | action can be called, and the breaker will 'trip' into the `BROKEN` state. 16 | * `BROKEN`: if provided, a fallback action will be invoked instead of the primary action. 17 | 18 | ## Example 19 | 20 | // Creating a new breaker instance 21 | breaker = BreakerBuilder.newBuilder() 22 | .build(); 23 | 24 | // Using the breaker instance to perform a task which could potentially fail 25 | breaker.tryDo(() -> { 26 | someUnreliableService.sendMessage(message); // <1> 27 | }, () -> { 28 | LOGGER.error("Service failed!"); // <2> 29 | }, () -> { 30 | fallbackQueue.add(message); // <3> 31 | }); 32 | 33 | In this example, **1** will definitely be called the first time this piece of code is hit, and will 34 | be called on subsequent invocations as long as the breaker remains in the `OK` state. 35 | 36 | If **1** throws an exception, **2** and **3** will be called immediately, and the breaker will change into 37 | the `BROKEN` state. Thereafter, every time this code is hit **3** will be called. 38 | 39 | ## Key Javadocs 40 | 41 | * **[BreakerBuilder](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/circuitbreakers/BreakerBuilder.html)** 42 | * **[Breaker](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/circuitbreakers/Breaker.html)** 43 | 44 | ## Features/TODOs 45 | 46 | * Support for calling a void-return `Runnable`, or getting a value from a `Callable` (see `tryDo` and `tryGet` methods) 47 | * Optional automatic reset a given time after the last failure ([javadocs](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/circuitbreakers/BreakerBuilder.html#autoResetAfter-long-java.util.concurrent.TimeUnit-)) 48 | * Optional holding of state in an external object or Map (aimed at allowing breaker state to be shared across a cluster - [javadocs](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/circuitbreakers/BreakerBuilder.html#storeStateIn-java.util.concurrent.ConcurrentMap-java.lang.String-)) 49 | * TODO: Configurable trip after _n_ consecutive failures 50 | * TODO: Configurable trip at _x_% failure rate 51 | * TODO: Configurable logging of breaker state transitions 52 | * TODO: Support passing exceptions to `runOnFirstFailure` for handling 53 | * TODO: Allow configuration of exceptions that should trip the breaker vs exceptions that should be re-thrown -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Duct Tape 2 | 3 | > Duct Tape is a little Java 8 library providing fault tolerance support for code that calls external APIs and 4 | unreliable components, including those that may fail, time out, or return inconsistent results. 5 | 6 | ![Logo](https://raw.githubusercontent.com/rnorth/duct-tape/master/docs/logo.png) 7 | 8 | This library was formerly named `circuitbreakers`, but given the inclusion of more general purpose fault tolerance 9 | features, it was renamed. 10 | 11 | [![Circle CI](https://circleci.com/gh/rnorth/duct-tape.svg?style=svg)](https://circleci.com/gh/rnorth/duct-tape) 12 | 13 | [View on Github](https://github.com/rnorth/duct-tape) 14 | 15 | ## Features 16 | 17 | * **[Circuit breakers](circuitbreaker.md):** graceful isolation of an external component after a 18 | failure occurs 19 | * **[Automatic retry functions](retry.md):** try/retry a block of code until it succeeds, returns true, or returns a 20 | consistent result for long enough. 21 | * **[Timeout functions](timeout.md):** quick and easy wrappers for code to automatically limit execution time 22 | * **[Rate limiter](ratelimiter.md):** limit how often a block of code can be called 23 | 24 | ## Javadocs 25 | 26 | See [here](http://rnorth.github.io/duct-tape/index.html) 27 | 28 | ## Example 29 | 30 | This example combines several of the above features: 31 | 32 | // Shared instances (e.g. fields on a singleton object) 33 | circuitBreaker = BreakerBuilder.newBuilder().build(); 34 | rateLimiter = RateLimiterBuilder.newBuilder() 35 | .withRate(20, TimeUnit.SECONDS) 36 | .withConstantThroughput() 37 | .build(); 38 | 39 | // ... 40 | 41 | // Try and get the result but trip a circuit breaker if the component is dead: 42 | Result result = circuitBreaker.tryGet(() -> { 43 | // Retry the call for up to 2s if an exception is thrown 44 | return Unreliables.retryUntilSuccess(2, TimeUnit.SECONDS, () -> { 45 | // Limit calls to a max rate of 20 per second 46 | return rateLimiter.getWhenReady(() -> { 47 | // Limit each call to 100ms 48 | return Timeouts.getWithTimeout(100, TimeUnit.MILLISECONDS, () -> { 49 | // Actually call the external service/API/unreliable component 50 | return exampleService.getValue("Hello World"); 51 | }); 52 | }); 53 | }); 54 | }, () -> { 55 | // Report the first time a failure occurs (probably fire off a monitoring alert IRL) 56 | LOGGER.error("Circuit breaker was tripped"); 57 | }, () -> { 58 | // Provide a default value if the circuit breaker is tripped 59 | return DEFAULT_VALUE; 60 | }); 61 | 62 | 63 | ## Why not Hystrix? 64 | 65 | [Hystrix](https://github.com/Netflix/Hystrix) is a leading Java fault tolerance library that is undoubtedly more mature, 66 | more battle-tested and probably more robust. 67 | However, it is fairly prescriptive, and imposes a structure that might not fit with existing codebases. Duct Tape 68 | is intended to be very easy to understand and easy to integrate, with least disruption: integration should not be much 69 | harder than wrapping integration points. 70 | 71 | If you need the strength of Hystrix or pre JDK-1.8 support then absolutely go for Hystrix. Otherwise, please at least 72 | consider Duct Tape :) 73 | 74 | ## Maven dependency 75 | 76 | 77 | 78 | org.rnorth.duct-tape 79 | duct-tape 80 | 1.0.4 81 | 82 | 83 | 84 | ## License 85 | 86 | See [LICENSE](LICENSE). 87 | 88 | ## Contributing 89 | 90 | * Star the project on Github and help spread the word :) 91 | * [Post an issue](https://github.com/rnorth/duct-tape/issues) if you find any bugs 92 | * Contribute improvements or fixes using a [Pull Request](https://github.com/rnorth/duct-tape/pulls). If you're going to contribute, thank you! Please just be sure to: 93 | * discuss with the authors on an issue ticket prior to doing anything big 94 | * follow the style, naming and structure conventions of the rest of the project 95 | * make commits atomic and easy to merge 96 | * verify all tests are passing. Build the project with `mvn clean install` to do this. 97 | 98 | ## Copyright 99 | 100 | Copyright (c) 2014-2015 Richard North. 101 | 102 | See [AUTHORS](AUTHORS) for contributors. 103 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnorth/duct-tape/2a1c5be9f2ef3f16bf036cec8752a170d130b61e/docs/logo.png -------------------------------------------------------------------------------- /docs/ratelimiter.md: -------------------------------------------------------------------------------- 1 | # Rate limiter 2 | 3 | Rate limiters allow control over the maximum frequency a block of code will be called. 4 | 5 | At present this is simply done by performing a `Thread.sleep()` to achieve a given constant throughput rate, but could 6 | be expanded to other timing techniques in the future. 7 | 8 | ## Examples 9 | 10 | Limit calls to a third party service to a maximum of 2 per second. 11 | 12 | // Create a shared rate limiter object somewhere. The same rate limiter object should be reused at all call sites 13 | RateLimiter sharedRateLimiter = RateLimiterBuilder.newBuilder() 14 | .withRate(2, TimeUnit.SECONDS) 15 | .withConstantThroughput() 16 | .build(); 17 | 18 | // .... 19 | 20 | // Somewhere else (the call site), wrap the call to the third party service in the rate limiter 21 | result = sharedRateLimiter.getWhenReady(() -> { 22 | return externalApi.fetchSomethingById(id) 23 | }); 24 | 25 | ## Key Javadocs 26 | 27 | * **[RateLimiterBuilder](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/ratelimits/RateLimiterBuilder.html)** 28 | * **[RateLimiter](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/ratelimits/RateLimiter.html)** -------------------------------------------------------------------------------- /docs/retry.md: -------------------------------------------------------------------------------- 1 | # Automatic retry functions 2 | 3 | This library also includes utility classes to help with automatic retry where there's an element of unpredictability. 4 | 5 | For example: 6 | 7 | * connection attempts to networked resources, 8 | * waiting for a file or resource managed by another process to become available 9 | * many kinds of automated testing tasks 10 | 11 | It's often best to use events/callbacks to manage these situations, but when these are not available as options we need 12 | a polling approach. These retry functions try to reduce the amount of boilerplate code needed. 13 | 14 | ## Calling *unreliable* code 15 | 16 | The [Unreliables](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/unreliables/Unreliables.html) class caters for 17 | calls that may be unreliable: 18 | * [retryUntilSuccess](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/unreliables/Unreliables.html#retryUntilSuccess-int-java.util.concurrent.TimeUnit-java.util.concurrent.Callable-) retries a call that might throw exceptions. The call will be repeated (up to a time limit) 19 | until it returns a result. 20 | * [retryUntilTrue](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/unreliables/Unreliables.html#retryUntilTrue-int-java.util.concurrent.TimeUnit-java.util.concurrent.Callable-) retries a call until it returns `true` 21 | 22 | ## Calling *inconsistent* code 23 | 24 | The [Inconsistents](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/inconsistents/Inconsistents.html) 25 | class deals with calls that might return a result quickly, but that take some time to stabilize 26 | on a consistent result. A call wrapped with [retryUntilConsistent](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/inconsistents/Inconsistents.html#retryUntilConsistent-int-int-java.util.concurrent.TimeUnit-java.util.concurrent.Callable-) 27 | will be called until the result is stable (equal) for a minimum period of time, or until a time limit is hit. 28 | 29 | ## A note about retry frequency 30 | 31 | _These methods do not automatically pause between retries - they'll retry a call as quickly as possible. You'll 32 | perhaps want to use a [rate limiter](ratelimiter.md) to reduce polling frequency. Also, these functions are fairly simple 33 | and not optimized for performance or resource usage yet, so please test before using these under heavy load._ 34 | 35 | ## Examples 36 | 37 | Try to obtain a database connection: 38 | 39 | Connection connection = Unreliables.retryUntilSuccess(30, TimeUnit.SECONDS, () -> { 40 | return driver.connect(databaseUrl, databaseInfo); 41 | }); 42 | 43 | Make sure a Selenium UI element has been updated before continuing: 44 | 45 | Unreliables.retryUntilTrue(3, TimeUnit.SECONDS, () -> { 46 | return statusLabel.getText().equals("Finished"); 47 | }); 48 | 49 | Wait until a collection of UI elements has been fully updated by JavaScript code before continuing: 50 | 51 | List listItems = Inconsistents.retryUntilConsistent(300, 1000, TimeUnit.MILLISECONDS, () -> { 52 | return driver.findElements(By.cssSelector("li.product")); 53 | }); 54 | 55 | ## Key Javadocs 56 | 57 | * **[Unreliables](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/unreliables/Unreliables.html)** 58 | * **[Inconsistents](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/inconsistents/Inconsistents.html)** -------------------------------------------------------------------------------- /docs/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{NAME}} :: viewdocs.io 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 25 | 26 | 27 |
28 |
29 |

{{NAME}} :: index

30 |
31 |
32 | {{CONTENT}} 33 |
34 |
35 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/timeout.md: -------------------------------------------------------------------------------- 1 | # Timeout functions 2 | 3 | The timeout functions provide a very simple way to ensure that a given block of code does not exceed a given execution 4 | time. 5 | 6 | The [Timeouts](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/timeouts/Timeouts.html) 7 | class allows dispatching a synchronous call that does not return a result ([doWithTimeout](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/timeouts/Timeouts.html#doWithTimeout-int-java.util.concurrent.TimeUnit-java.lang.Runnable-)) 8 | or a synchronous call that returns a result ([getWithTimeout](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/timeouts/Timeouts.html#getWithTimeout-int-java.util.concurrent.TimeUnit-java.util.concurrent.Callable-)). 9 | 10 | ## Examples 11 | 12 | Try to invoke an external service and return a result, but time out if it takes more than 5 seconds. 13 | 14 | try { 15 | result = Timeouts.getWithTimeout(5, TimeUnit.SECONDS, () -> { 16 | return myExternalService.doSomething(); 17 | }); 18 | } catch (TimeoutException e) { 19 | // handle failure - e.g. use a cached value, report an error, etc 20 | } 21 | 22 | Try to invoke an external service without returning a result, but time out if it takes more than 30 seconds. 23 | 24 | Timeouts.doWithTimeout(30, TimeUnit.SECONDS, () -> { 25 | myExternalService.doSomething(); 26 | }); 27 | 28 | ## Key Javadocs 29 | 30 | * **[Timeouts](http://rnorth.github.io/duct-tape/org/rnorth/ducttape/timeouts/Timeouts.html)** -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.rnorth.duct-tape 6 | duct-tape 7 | 1.0.8-SNAPSHOT 8 | 9 | 10 | 11 | Duct Tape 12 | 13 | General purpose resilience utilities for Java 8 (circuit breakers, timeouts, rate limiters, and handlers for unreliable or inconsistent results) 14 | 15 | https://github.com/rnorth/${project.artifactId} 16 | 17 | 18 | MIT 19 | http://opensource.org/licenses/MIT 20 | 21 | 22 | 23 | 24 | rnorth 25 | Richard North 26 | rich.north@gmail.com 27 | 28 | 29 | 30 | 31 | 32 | org.slf4j 33 | slf4j-api 34 | 1.7.7 35 | provided 36 | 37 | 38 | junit 39 | junit 40 | 4.12 41 | test 42 | 43 | 44 | org.mockito 45 | mockito-all 46 | 1.9.5 47 | test 48 | 49 | 50 | org.slf4j 51 | slf4j-simple 52 | 1.7.7 53 | test 54 | 55 | 56 | org.rnorth.visible-assertions 57 | visible-assertions 58 | 1.0.5 59 | test 60 | 61 | 62 | org.jetbrains 63 | annotations 64 | 17.0.0 65 | 66 | 67 | 68 | 69 | 70 | 71 | maven-compiler-plugin 72 | 3.1 73 | 74 | 1.8 75 | 1.8 76 | 77 | 78 | 79 | maven-scm-plugin 80 | 1.9.4 81 | 82 | ${project.version} 83 | 84 | 85 | 86 | maven-release-plugin 87 | 2.5.1 88 | 89 | false 90 | true 91 | 92 | 93 | 94 | maven-source-plugin 95 | 2.4 96 | 97 | 98 | attach-sources 99 | 100 | jar 101 | 102 | 103 | 104 | 105 | 106 | maven-javadoc-plugin 107 | 2.10.3 108 | 109 | 110 | attach-javadocs 111 | 112 | jar 113 | 114 | 115 | 116 | 117 | true 118 | public 119 | true 120 |
${project.name}, ${project.version}
121 |
${project.name}, ${project.version}
122 | ${project.name}, ${project.version} 123 |
124 |
125 | 126 | org.apache.maven.plugins 127 | maven-scm-publish-plugin 128 | 1.0-beta-2 129 | 130 | 131 | publish-javadocs 132 | site-deploy 133 | 134 | publish-scm 135 | 136 | 137 | 138 | 139 | ${project.build.directory}/scmpublish 140 | Publishing javadoc for ${project.artifactId}:${project.version} 141 | ${project.reporting.outputDirectory}/apidocs 142 | true 143 | scm:git:git@github.com:rnorth/${project.artifactId}.git 144 | gh-pages 145 | 146 | 147 |
148 |
149 | 150 | 151 | scm:git:https://github.com/rnorth/${project.artifactId}.git 152 | scm:git:git@github.com:rnorth/${project.artifactId}.git 153 | https://github.com/rnorth/${project.artifactId} 154 | HEAD 155 | 156 | 157 | 158 | 159 | bintray 160 | https://api.bintray.com/maven/richnorth/maven/${project.artifactId} 161 | 162 | 163 |
164 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/Preconditions.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape; 2 | 3 | /** 4 | * Simple Preconditions check implementation. 5 | */ 6 | public class Preconditions { 7 | 8 | /** 9 | * Check that a given condition is true. Will throw an IllegalArgumentException otherwise. 10 | * @param message message to display if the precondition check fails 11 | * @param condition the result of evaluating the condition 12 | */ 13 | public static void check(String message, boolean condition) { 14 | if (!condition) { 15 | throw new IllegalArgumentException("Precondition failed: " + message); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/RetryCountExceededException.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * Indicates repeated failure of an UnreliableSupplier 7 | */ 8 | public class RetryCountExceededException extends RuntimeException { 9 | 10 | public RetryCountExceededException(@NotNull String message, @NotNull Exception exception) { 11 | super(message, exception); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/TimeoutException.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | /** 7 | * Indicates timeout of an UnreliableSupplier 8 | */ 9 | public class TimeoutException extends RuntimeException { 10 | 11 | public TimeoutException(@NotNull String message, @Nullable Exception exception) { 12 | super(message, exception); 13 | } 14 | 15 | public TimeoutException(@NotNull Exception e) { 16 | super(e); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/circuitbreakers/Breaker.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import java.util.Optional; 4 | import java.util.concurrent.Callable; 5 | import java.util.function.Supplier; 6 | 7 | /** 8 | * A Circuit Breaker, per http://martinfowler.com/bliki/CircuitBreaker.html[the pattern of the same name]. 9 | * 10 | * @author richardnorth 11 | */ 12 | public interface Breaker { 13 | 14 | /** 15 | * Do something, unless the breaker is in a broken state, in which case perform a fallback action. 16 | * 17 | * @param tryIfAlive A runnable, to be invoked unless the breaker is in `BROKEN` state 18 | * @param runOnFirstFailure A runnable, to be invoked immediately if the first runnable fails 19 | * @param runIfBroken A runnable, to be invoked if the breaker is in `BROKEN` state 20 | */ 21 | void tryDo(Runnable tryIfAlive, Runnable runOnFirstFailure, Runnable runIfBroken); 22 | 23 | /** 24 | * Do something, unless the breaker is in a broken state, in which case perform a fallback action. 25 | * 26 | * @param tryIfAlive A runnable, to be invoked unless the breaker is in `BROKEN` state 27 | * @param runIfBroken A runnable, to be invoked if the breaker is in `BROKEN` state 28 | */ 29 | void tryDo(Runnable tryIfAlive, Runnable runIfBroken); 30 | 31 | /** 32 | * Do something, unless the breaker is in a broken state, in which case do nothing. 33 | * 34 | * @param tryIfAlive A runnable, to be invoked unless the breaker is in `BROKEN` state 35 | */ 36 | void tryDo(Runnable tryIfAlive); 37 | 38 | /** 39 | * Get the result of something, unless the breaker is in a broken state, in which case get a fallback value. 40 | * 41 | * @param tryIfAlive A callable, to be called unless the breaker is in `BROKEN` state. May throw a checked exception. 42 | * @param runOnFirstFailure A runnable, to be invoked immediately if the first callable fails 43 | * @param getIfBroken A supplier, to be invoked if the breaker is in `BROKEN` state. May not throw a checked exception. 44 | * @param The type to be returned by the `tryIfAlive` callable and `getIfBroken` supplier 45 | * @return The value returned by either `tryIfAlive` or `getIfBroken` 46 | */ 47 | T tryGet(Callable tryIfAlive, Runnable runOnFirstFailure, Supplier getIfBroken); 48 | 49 | /** 50 | * Get the result of something, unless the breaker is in a broken state, in which case get a fallback value. 51 | * 52 | * @param tryIfAlive A callable, to be called unless the breaker is in `BROKEN` state. May throw a checked exception. 53 | * @param getIfBroken A supplier, to be invoked if the breaker is in `BROKEN` state. May not throw a checked exception. 54 | * @param The type to be returned by the `tryIfAlive` callable and `getIfBroken` supplier 55 | * @return The value returned by either `tryIfAlive` or `getIfBroken` 56 | */ 57 | T tryGet(Callable tryIfAlive, Supplier getIfBroken); 58 | 59 | /** 60 | * Get the result of something, unless the breaker is in a broken state, in which case get a fallback value. 61 | * 62 | * @param tryIfAlive A callable, to be called unless the breaker is in `BROKEN` state. May throw a checked exception. 63 | * @param The type to be returned by the `tryIfAlive` callable and `getIfBroken` supplier 64 | * @return An `Optional` wrapping either the value returned by `tryIfAlive`, or empty if the breaker is in 65 | * `BROKEN` state or `tryIfAlive` failed 66 | */ 67 | Optional tryGet(Callable tryIfAlive); 68 | 69 | /** 70 | * @return The current state of the breaker 71 | */ 72 | State getState(); 73 | 74 | static void NoOp() { 75 | // No-op 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/circuitbreakers/BreakerBuilder.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.concurrent.ConcurrentMap; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import static org.rnorth.ducttape.Preconditions.check; 9 | 10 | /** 11 | * BreakerBuilder 12 | * 13 | * A builder of {@link Breaker} instances having any combination of the following features: 14 | *
    15 | *
  • automatic reset after a configured period of time has passed since the last failure
  • 16 | *
  • configurable state storage, for example for use with a distributed map implementation
  • 17 | *
18 | * These features are optional; by default instances will be created with none of them. 19 | * 20 | * @author richardnorth 21 | */ 22 | public class BreakerBuilder { 23 | 24 | private TimeSource timeSource = new TimeSource(); 25 | private long autoResetInterval = Long.MAX_VALUE; 26 | private TimeUnit autoResetUnit = TimeUnit.DAYS; 27 | private StateStore stateStore = new SimpleStateStore(); 28 | 29 | private BreakerBuilder() { 30 | } 31 | 32 | /** 33 | * @return a new `BreakerBuilder` instance. 34 | */ 35 | public static BreakerBuilder newBuilder() { 36 | return new BreakerBuilder(); 37 | } 38 | 39 | /** 40 | * @return a {@link Breaker} instance configured using settings passed in to this 41 | * `BreakerBuilder` 42 | */ 43 | public Breaker build() { 44 | return new DefaultBreaker(timeSource, autoResetInterval, autoResetUnit, stateStore); 45 | } 46 | 47 | /** 48 | * Use a {@link TimeSource} instance to track time elapsed since last failure. 49 | * Mainly intended for use in testing. 50 | * 51 | * @param timeSource a time source instance to track time elapsed since last failure. 52 | * 53 | * @return this 54 | */ 55 | public BreakerBuilder timeSource(@NotNull TimeSource timeSource) { 56 | this.timeSource = timeSource; 57 | return this; 58 | } 59 | 60 | /** 61 | * Configure the breaker to automatically reset a given time after a failure has occurred. Use this for 62 | * unattended retry behaviour. 63 | * 64 | * If this method is not used, the default is for the breaker to wait `Long.MAX_VALUE` days before it 65 | * resets automatically, i.e. effectively forever. 66 | * 67 | * @param autoResetInterval the interval length 68 | * @param autoResetUnit the units of the interval 69 | * 70 | * @return this 71 | */ 72 | public BreakerBuilder autoResetAfter(long autoResetInterval, @NotNull final TimeUnit autoResetUnit) { 73 | 74 | check("reset interval must be greater than zero", autoResetInterval > 0); 75 | 76 | this.autoResetInterval = autoResetInterval; 77 | this.autoResetUnit = autoResetUnit; 78 | return this; 79 | } 80 | 81 | /** 82 | * Configure the breaker to use the provided {@link java.util.Map} to store its state. The
keyPrefix
is used to 83 | * uniquely identify the breaker's entries in the map. 84 | * 85 | *
keyPrefix
should be unique; behaviour is undefined if it is not. Additionally, behaviour is undefined if entries 86 | * are directly modified. 87 | * 88 | * @param map the map to use for storage 89 | * @param keyPrefix a unique prefix for the +Breaker+'s entries in the map 90 | * 91 | * @return this 92 | */ 93 | public BreakerBuilder storeStateIn(@NotNull final ConcurrentMap map, @NotNull final String keyPrefix) { 94 | this.stateStore = new MapBackedStateStore(map, keyPrefix); 95 | return this; 96 | } 97 | 98 | /** 99 | * Configure the breaker to use the provided {@link StateStore} to store its state. 100 | * 101 | * @param stateStore an instance of {@link StateStore} to store state in 102 | * 103 | * @return this 104 | */ 105 | public BreakerBuilder storeStateIn(@NotNull final StateStore stateStore) { 106 | this.stateStore = stateStore; 107 | return this; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/circuitbreakers/DefaultBreaker.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Optional; 6 | import java.util.concurrent.Callable; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.function.Supplier; 9 | 10 | /** 11 | * @author richardnorth 12 | */ 13 | class DefaultBreaker implements Breaker { 14 | 15 | private final TimeSource timeSource; 16 | private final long autoResetInterval; 17 | private final TimeUnit autoResetUnit; 18 | private final StateStore stateStore; 19 | 20 | DefaultBreaker(@NotNull final TimeSource timeSource, final long autoResetInterval, @NotNull final TimeUnit autoResetUnit, @NotNull final StateStore stateStore) { 21 | 22 | this.timeSource = timeSource; 23 | this.autoResetInterval = autoResetInterval; 24 | this.autoResetUnit = autoResetUnit; 25 | this.stateStore = stateStore; 26 | } 27 | 28 | @Override 29 | public void tryDo(@NotNull final Runnable tryIfAlive, @NotNull final Runnable runOnFirstFailure, @NotNull final Runnable runIfBroken) { 30 | if (isBroken()) { 31 | runIfBroken.run(); 32 | } else { 33 | try { 34 | tryIfAlive.run(); 35 | setState(State.OK); 36 | } catch (Exception e) { 37 | setState(State.BROKEN); 38 | setLastFailure(timeSource.getTimeMillis()); 39 | runOnFirstFailure.run(); 40 | runIfBroken.run(); 41 | } 42 | } 43 | } 44 | 45 | @Override 46 | public void tryDo(@NotNull final Runnable tryIfAlive, @NotNull final Runnable runIfBroken) { 47 | tryDo(tryIfAlive, Breaker::NoOp, runIfBroken); 48 | } 49 | 50 | @Override 51 | public void tryDo(@NotNull final Runnable tryIfAlive) { 52 | tryDo(tryIfAlive, Breaker::NoOp, Breaker::NoOp); 53 | } 54 | 55 | @Override 56 | public T tryGet(@NotNull final Callable tryIfAlive, @NotNull final Runnable runOnFirstFailure, @NotNull final Supplier getIfBroken) { 57 | if (isBroken()) { 58 | return getIfBroken.get(); 59 | } else { 60 | try { 61 | T callResult = tryIfAlive.call(); 62 | setState(State.OK); 63 | return callResult; 64 | } catch (Exception e) { 65 | setState(State.BROKEN); 66 | setLastFailure(timeSource.getTimeMillis()); 67 | runOnFirstFailure.run(); 68 | return getIfBroken.get(); 69 | } 70 | } 71 | } 72 | 73 | @Override 74 | public T tryGet(@NotNull final Callable tryIfAlive, @NotNull final Supplier getIfBroken) { 75 | return tryGet(tryIfAlive, Breaker::NoOp, getIfBroken); 76 | } 77 | 78 | @Override 79 | public Optional tryGet(@NotNull final Callable tryIfAlive) { 80 | return Optional.ofNullable(tryGet(tryIfAlive, Breaker::NoOp, () -> null)); 81 | } 82 | 83 | @Override 84 | public State getState() { 85 | return this.stateStore.getState(); 86 | } 87 | 88 | private boolean isBroken() { 89 | boolean broken = this.stateStore.getState() == State.BROKEN; 90 | boolean notAutoResetYet = (timeSource.getTimeMillis() - autoResetUnit.toMillis(autoResetInterval)) < this.stateStore.getLastFailure(); 91 | return broken && notAutoResetYet; 92 | } 93 | 94 | private void setState(@NotNull final State state) { 95 | this.stateStore.setState(state); 96 | } 97 | 98 | private void setLastFailure(final long lastFailure) { 99 | this.stateStore.setLastFailure(lastFailure); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/circuitbreakers/MapBackedStateStore.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * @author richardnorth 9 | */ 10 | class MapBackedStateStore implements StateStore { 11 | private final Map map; 12 | private final String keyPrefix; 13 | 14 | public MapBackedStateStore(@NotNull final Map map, @NotNull final String keyPrefix) { 15 | this.map = map; 16 | this.keyPrefix = keyPrefix; 17 | } 18 | 19 | @Override 20 | public State getState() { 21 | return (State) this.map.getOrDefault(keyPrefix + "_STATE", State.OK); 22 | } 23 | 24 | @Override 25 | public void setState(@NotNull final State state) { 26 | this.map.put(keyPrefix + "_STATE", state); 27 | } 28 | 29 | @Override 30 | public long getLastFailure() { 31 | return (long) this.map.getOrDefault(keyPrefix + "_LAST_FAILURE", 0L); 32 | } 33 | 34 | @Override 35 | public void setLastFailure(final long lastFailure) { 36 | this.map.put(keyPrefix + "_LAST_FAILURE", lastFailure); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/circuitbreakers/SimpleStateStore.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * @author richardnorth 7 | */ 8 | class SimpleStateStore implements StateStore { 9 | private State state = State.OK; 10 | private long lastFailure; 11 | 12 | @Override 13 | public State getState() { 14 | return state; 15 | } 16 | 17 | @Override 18 | public void setState(@NotNull final State state) { 19 | this.state = state; 20 | } 21 | 22 | @Override 23 | public long getLastFailure() { 24 | return lastFailure; 25 | } 26 | 27 | @Override 28 | public void setLastFailure(final long lastFailure) { 29 | this.lastFailure = lastFailure; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/circuitbreakers/State.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | /** 4 | * @author richardnorth 5 | */ 6 | public enum State { 7 | /** 8 | * The breaker is OK, i.e. trying to perform requested primary actions. 9 | */ 10 | OK, 11 | /** 12 | * The breaker is broken, i.e. avoiding calling primary actions, and falling straight through to the fallback actions. 13 | */ 14 | BROKEN 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/circuitbreakers/StateStore.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | /** 6 | * An external store for state which a {@link Breaker} can use. 7 | * 8 | * While a default instance of Breaker will use a simple object local to the current JVM, alternative instances of `StateStore` 9 | * could be created to maintain state somewhere else, e.g.: 10 | * 11 | * * A persistent store on disk. This could be helpful to maintain breaker state between runs of the JVM. 12 | * * A distributed store, such as a cache shared across a cluster of machines. This could be helpful if breaker state 13 | * needs to be shared by many machines. 14 | * 15 | * @author richardnorth 16 | */ 17 | public interface StateStore { 18 | 19 | State getState(); 20 | void setState(@NotNull State state); 21 | 22 | long getLastFailure(); 23 | void setLastFailure(long lastFailure); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/circuitbreakers/TimeSource.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | /** 4 | * @author richardnorth 5 | */ 6 | class TimeSource { 7 | 8 | public long getTimeMillis() { 9 | return System.currentTimeMillis(); 10 | } 11 | 12 | static class DummyTimeSource extends org.rnorth.ducttape.circuitbreakers.TimeSource { 13 | 14 | private long currentTimeMillis = 0L; 15 | 16 | public DummyTimeSource() {} 17 | 18 | public DummyTimeSource(long fixedValue) { 19 | this.currentTimeMillis = fixedValue; 20 | } 21 | 22 | @Override 23 | public long getTimeMillis() { 24 | return currentTimeMillis; 25 | } 26 | 27 | public void setCurrentTimeMillis(long currentTimeMillis) { 28 | this.currentTimeMillis = currentTimeMillis; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/inconsistents/InconsistentResultsException.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.inconsistents; 2 | 3 | /** 4 | * Exception caused by a failure to obtain consistent results. 5 | */ 6 | public class InconsistentResultsException extends ResultsNeverConsistentException { 7 | 8 | protected final Object mostConsistentValue; 9 | protected final long mostConsistentTime; 10 | 11 | public InconsistentResultsException(long timeSinceStart, Object mostConsistentValue, long mostConsistentTime) { 12 | super("After " + timeSinceStart + "ms, results have not become consistent. Most consistent value was " + mostConsistentValue + ", seen for " + mostConsistentTime + "ms", timeSinceStart); 13 | this.mostConsistentValue = mostConsistentValue; 14 | this.mostConsistentTime = mostConsistentTime; 15 | } 16 | 17 | public Object getMostConsistentValue() { 18 | return mostConsistentValue; 19 | } 20 | 21 | public long getMostConsistentTime() { 22 | return mostConsistentTime; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/inconsistents/Inconsistents.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.inconsistents; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.rnorth.ducttape.unreliables.Unreliables; 5 | 6 | import java.util.concurrent.Callable; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import static org.rnorth.ducttape.Preconditions.check; 10 | 11 | /** 12 | * Utility for calling a supplier that may take time to stabilise on a final result. 13 | */ 14 | public class Inconsistents { 15 | 16 | /** 17 | * Retry invocation of a supplier repeatedly until it returns a consistent result for a sufficient time period. 18 | * 19 | * This is intended for calls to components that take an unknown amount of time to stabilise, and where 20 | * repeated checks are the only way to detect that a stable state has been reached. 21 | * 22 | * @param consistentTime how long the result should be consistent for before it is returned 23 | * @param totalTimeout how long in total to wait for stabilisation to occur 24 | * @param timeUnit time unit for time intervals 25 | * @param lambda an UnreliableSupplier which should be called 26 | * @param the return type of the UnreliableSupplier 27 | * @return the result of the supplier if it returned a consistent result for the specified interval 28 | */ 29 | public static T retryUntilConsistent(final int consistentTime, final int totalTimeout, @NotNull final TimeUnit timeUnit, @NotNull final Callable lambda) { 30 | 31 | check("consistent time must be greater than 0", consistentTime > 0); 32 | check("total timeout must be greater than 0", totalTimeout > 0); 33 | 34 | long start = System.currentTimeMillis(); 35 | 36 | Object[] recentValue = {null}; 37 | long[] firstRecentValueTime = {0}; 38 | long[] bestRun = {0}; 39 | Object[] bestRunValue = {null}; 40 | 41 | long consistentTimeInMillis = TimeUnit.MILLISECONDS.convert(consistentTime, timeUnit); 42 | 43 | return Unreliables.retryUntilSuccess(totalTimeout, timeUnit, () -> { 44 | T value = lambda.call(); 45 | 46 | boolean valueIsSame = value == recentValue[0] || (value != null && value.equals(recentValue[0])); 47 | 48 | if (valueIsSame) { 49 | long now = System.currentTimeMillis(); 50 | long timeSinceFirstValue = now - firstRecentValueTime[0]; 51 | 52 | if (timeSinceFirstValue > bestRun[0]) { 53 | bestRun[0] = timeSinceFirstValue; 54 | bestRunValue[0] = value; 55 | } 56 | 57 | if (timeSinceFirstValue > consistentTimeInMillis) { 58 | return value; 59 | } 60 | } else { 61 | // Reset everything and see if the next call yields the same result as this time 62 | recentValue[0] = value; 63 | firstRecentValueTime[0] = System.currentTimeMillis(); 64 | } 65 | 66 | long timeSinceStart = System.currentTimeMillis() - start; 67 | 68 | if (bestRun[0] > 0) { 69 | throw new InconsistentResultsException(timeSinceStart, bestRunValue[0], bestRun[0]); 70 | } else { 71 | throw new ResultsNeverConsistentException(timeSinceStart); 72 | } 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/inconsistents/ResultsNeverConsistentException.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.inconsistents; 2 | 3 | /** 4 | * Exception caused by a failure to obtain consistent results. 5 | */ 6 | public class ResultsNeverConsistentException extends RuntimeException { 7 | 8 | protected final long timeSinceStart; 9 | 10 | public ResultsNeverConsistentException(String message, long timeSinceStart) { 11 | super(message); 12 | this.timeSinceStart = timeSinceStart; 13 | } 14 | 15 | public ResultsNeverConsistentException(long timeSinceStart) { 16 | super("After " + timeSinceStart + "ms, results have not become consistent. The value was never consistent"); 17 | this.timeSinceStart = timeSinceStart; 18 | } 19 | 20 | public long getTimeSinceStart() { 21 | return timeSinceStart; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/ratelimits/ConstantThroughputRateLimiter.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.ratelimits; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** 8 | * A rate limiter that uses a simple 'run every n millis' strategy to achieve constant throughput. 9 | */ 10 | class ConstantThroughputRateLimiter extends RateLimiter { 11 | 12 | private final long timeBetweenInvocations; 13 | 14 | ConstantThroughputRateLimiter(@NotNull Integer rate, @NotNull TimeUnit perTimeUnit) { 15 | this.timeBetweenInvocations = perTimeUnit.toMillis(1) / rate; 16 | } 17 | 18 | @Override 19 | protected long getWaitBeforeNextInvocation() { 20 | 21 | long timeToNextAllowed = (lastInvocation + timeBetweenInvocations) - System.currentTimeMillis(); 22 | 23 | // Clamp wait time to 0< 24 | return Math.max(timeToNextAllowed, 0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/ratelimits/RateLimiter.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.ratelimits; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.concurrent.Callable; 6 | 7 | /** 8 | * Base class for rate limiters. Use RateLimiterBuilder to build new instances. 9 | */ 10 | public abstract class RateLimiter { 11 | 12 | protected long lastInvocation; 13 | 14 | /** 15 | * Invoke a lambda function, with Thread.sleep() being called to limit the execution rate if needed. 16 | * @param lambda a Runnable lamda function to invoke 17 | */ 18 | public void doWhenReady(@NotNull final Runnable lambda) { 19 | 20 | // Wait before proceeding, if needed 21 | long waitBeforeNextInvocation = getWaitBeforeNextInvocation(); 22 | try { 23 | Thread.sleep(waitBeforeNextInvocation); 24 | } catch (InterruptedException ignored) { } 25 | 26 | try { 27 | lambda.run(); 28 | } finally { 29 | lastInvocation = System.currentTimeMillis(); 30 | } 31 | } 32 | 33 | /** 34 | * 35 | * Invoke a lambda function and get the result, with Thread.sleep() being called to limit the execution rate 36 | * if needed. 37 | * @param lambda a Callable lamda function to invoke 38 | * @param return type of the lamda 39 | * @throws Exception rethrown from lambda 40 | * @return result of the lambda call 41 | */ 42 | public T getWhenReady(@NotNull final Callable lambda) throws Exception { 43 | 44 | // Wait before proceeding, if needed 45 | long waitBeforeNextInvocation = getWaitBeforeNextInvocation(); 46 | try { 47 | Thread.sleep(waitBeforeNextInvocation); 48 | } catch (InterruptedException ignored) { } 49 | 50 | try { 51 | return lambda.call(); 52 | } finally { 53 | lastInvocation = System.currentTimeMillis(); 54 | } 55 | } 56 | 57 | protected abstract long getWaitBeforeNextInvocation(); 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/ratelimits/RateLimiterBuilder.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.ratelimits; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import static org.rnorth.ducttape.Preconditions.check; 6 | 7 | /** 8 | * Builder for rate limiters. 9 | */ 10 | public class RateLimiterBuilder { 11 | 12 | private Integer invocations; 13 | private TimeUnit perTimeUnit; 14 | private RateLimiterStrategy strategy; 15 | 16 | private RateLimiterBuilder() { } 17 | 18 | /** 19 | * Obtain a new builder instance. 20 | * @return a new builder 21 | */ 22 | public static RateLimiterBuilder newBuilder() { 23 | return new RateLimiterBuilder(); 24 | } 25 | 26 | /** 27 | * Set the maximum rate that the limiter should allow, expressed as the number of invocations 28 | * allowed in a given time period. 29 | * @param invocations number of invocations 30 | * @param perTimeUnit the time period in which this number of invocations are allowed 31 | * @return the builder 32 | */ 33 | public RateLimiterBuilder withRate(final int invocations, final TimeUnit perTimeUnit) { 34 | this.invocations = invocations; 35 | this.perTimeUnit = perTimeUnit; 36 | return this; 37 | } 38 | 39 | /** 40 | * Configure the rate limiter to use a constant throughput strategy for rate limiting. 41 | * @return the builder 42 | */ 43 | public RateLimiterBuilder withConstantThroughput() { 44 | this.strategy = RateLimiterStrategy.CONSTANT_THROUGHPUT; 45 | return this; 46 | } 47 | 48 | /** 49 | * Build and obtain a configured rate limiter. A rate and rate limiting strategy must have been selected. 50 | * @return the configured rate limiter instance 51 | */ 52 | public RateLimiter build() { 53 | check("A rate must be set", invocations != null); 54 | check("A rate must be set", perTimeUnit != null); 55 | check("A rate limit strategy must be set", strategy != null); 56 | 57 | if (strategy == RateLimiterStrategy.CONSTANT_THROUGHPUT) { 58 | return new ConstantThroughputRateLimiter(invocations, perTimeUnit); 59 | } else { 60 | throw new IllegalStateException(); 61 | } 62 | } 63 | 64 | private enum RateLimiterStrategy { 65 | CONSTANT_THROUGHPUT 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/timeouts/Timeouts.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.timeouts; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.concurrent.*; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | import static org.rnorth.ducttape.Preconditions.check; 9 | 10 | /** 11 | * Utilities to time out on slow running code. 12 | */ 13 | public class Timeouts { 14 | 15 | private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool(new ThreadFactory() { 16 | 17 | final AtomicInteger threadCounter = new AtomicInteger(0); 18 | 19 | @Override 20 | public Thread newThread(@NotNull Runnable r) { 21 | Thread thread = new Thread(r, "ducttape-" + threadCounter.getAndIncrement()); 22 | thread.setDaemon(true); 23 | return thread; 24 | } 25 | }); 26 | 27 | /** 28 | * Execute a lambda expression with a timeout. If it completes within the time, the result will be returned. 29 | * If it does not complete within the time, a TimeoutException will be thrown. 30 | * If it throws an exception, a RuntimeException wrapping that exception will be thrown. 31 | * 32 | * @param timeout how long to wait 33 | * @param timeUnit time unit for time interval 34 | * @param lambda supplier lambda expression (may throw checked exceptions) 35 | * @param return type of the lambda 36 | * @return the result of the successful lambda expression call 37 | */ 38 | public static T getWithTimeout(final int timeout, final TimeUnit timeUnit, @NotNull final Callable lambda) { 39 | 40 | check("timeout must be greater than zero", timeout > 0); 41 | 42 | Future future = EXECUTOR_SERVICE.submit(lambda); 43 | return callFuture(timeout, timeUnit, future); 44 | } 45 | 46 | /** 47 | * Execute a lambda expression with a timeout. If it completes within the time, the result will be returned. 48 | * If it does not complete within the time, a TimeoutException will be thrown. 49 | * If it throws an exception, a RuntimeException wrapping that exception will be thrown. 50 | * 51 | * @param timeout how long to wait 52 | * @param timeUnit time unit for time interval 53 | * @param lambda supplier lambda expression (may throw checked exceptions) 54 | */ 55 | public static void doWithTimeout(final int timeout, @NotNull final TimeUnit timeUnit, @NotNull final Runnable lambda) { 56 | 57 | check("timeout must be greater than zero", timeout > 0); 58 | 59 | Future future = EXECUTOR_SERVICE.submit(lambda); 60 | callFuture(timeout, timeUnit, future); 61 | } 62 | 63 | private static T callFuture(final int timeout, @NotNull final TimeUnit timeUnit, @NotNull final Future future) { 64 | try { 65 | return future.get(timeout, timeUnit); 66 | } catch (ExecutionException e) { 67 | // The cause of the ExecutionException is the actual exception that was thrown 68 | throw new RuntimeException(e.getCause()); 69 | } catch (TimeoutException | InterruptedException e) { 70 | throw new org.rnorth.ducttape.TimeoutException(e); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/org/rnorth/ducttape/unreliables/Unreliables.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.unreliables; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.rnorth.ducttape.timeouts.Timeouts; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.concurrent.Callable; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | import static org.rnorth.ducttape.Preconditions.check; 13 | 14 | /** 15 | * Utilities to support automatic retry of things that may fail. 16 | */ 17 | public abstract class Unreliables { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(Unreliables.class); 20 | 21 | /** 22 | * Call a supplier repeatedly until it returns a result. If an exception is thrown, the call 23 | * will be retried repeatedly until the timeout is hit. 24 | * 25 | * @param timeout how long to wait 26 | * @param timeUnit time unit for time interval 27 | * @param lambda supplier lambda expression (may throw checked exceptions) 28 | * @param return type of the supplier 29 | * @return the result of the successful lambda expression call 30 | */ 31 | public static T retryUntilSuccess(final int timeout, @NotNull final TimeUnit timeUnit, @NotNull final Callable lambda) { 32 | 33 | check("timeout must be greater than zero", timeout > 0); 34 | 35 | final int[] attempt = {0}; 36 | final Exception[] lastException = {null}; 37 | 38 | final AtomicBoolean doContinue = new AtomicBoolean(true); 39 | try { 40 | return Timeouts.getWithTimeout(timeout, timeUnit, () -> { 41 | while (doContinue.get()) { 42 | try { 43 | return lambda.call(); 44 | } catch (Exception e) { 45 | // Failed 46 | LOGGER.trace("Retrying lambda call on attempt {}", attempt[0]++); 47 | lastException[0] = e; 48 | } 49 | } 50 | return null; 51 | }); 52 | } catch (org.rnorth.ducttape.TimeoutException e) { 53 | if (lastException[0] != null) { 54 | throw new org.rnorth.ducttape.TimeoutException("Timeout waiting for result with exception", lastException[0]); 55 | } else { 56 | throw new org.rnorth.ducttape.TimeoutException(e); 57 | } 58 | } finally { 59 | doContinue.set(false); 60 | } 61 | } 62 | 63 | /** 64 | * Call a supplier repeatedly until it returns a result. If an exception is thrown, the call 65 | * will be retried repeatedly until the retry limit is hit. 66 | * 67 | * @param tryLimit how many times to try calling the supplier 68 | * @param lambda supplier lambda expression (may throw checked exceptions) 69 | * @param return type of the supplier 70 | * @return the result of the successful lambda expression call 71 | */ 72 | public static T retryUntilSuccess(final int tryLimit, @NotNull final Callable lambda) { 73 | 74 | check("tryLimit must be greater than zero", tryLimit > 0); 75 | 76 | int attempt = 0; 77 | Exception lastException = null; 78 | 79 | while (attempt < tryLimit) { 80 | try { 81 | return lambda.call(); 82 | } catch (Exception e) { 83 | lastException = e; 84 | attempt++; 85 | } 86 | } 87 | 88 | throw new org.rnorth.ducttape.RetryCountExceededException("Retry limit hit with exception", lastException); 89 | } 90 | 91 | /** 92 | * Call a callable repeatedly until it returns true. If an exception is thrown, the call 93 | * will be retried repeatedly until the timeout is hit. 94 | * 95 | * @param timeout how long to wait 96 | * @param timeUnit time unit for time interval 97 | * @param lambda supplier lambda expression 98 | */ 99 | public static void retryUntilTrue(final int timeout, @NotNull final TimeUnit timeUnit, @NotNull final Callable lambda) { 100 | retryUntilSuccess(timeout, timeUnit, () -> { 101 | if (!lambda.call()) { 102 | throw new RuntimeException("Not ready yet"); 103 | } else { 104 | return null; 105 | } 106 | }); 107 | } 108 | 109 | /** 110 | * Call a callable repeatedly until it returns true. If an exception is thrown, the call 111 | * will be retried repeatedly until the timeout is hit. 112 | * 113 | * @param tryLimit how many times to try calling the supplier 114 | * @param lambda supplier lambda expression 115 | */ 116 | public static void retryUntilTrue(final int tryLimit, @NotNull final Callable lambda) { 117 | retryUntilSuccess(tryLimit, () -> { 118 | if (!lambda.call()) { 119 | throw new RuntimeException("Not ready yet"); 120 | } else { 121 | return null; 122 | } 123 | }); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/CompositeTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.rnorth.ducttape.circuitbreakers.Breaker; 6 | import org.rnorth.ducttape.circuitbreakers.BreakerBuilder; 7 | import org.rnorth.ducttape.circuitbreakers.ExampleService; 8 | import org.rnorth.ducttape.circuitbreakers.State; 9 | import org.rnorth.ducttape.ratelimits.RateLimiter; 10 | import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; 11 | import org.rnorth.ducttape.timeouts.Timeouts; 12 | import org.rnorth.ducttape.unreliables.Unreliables; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | 18 | import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; 19 | import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; 20 | 21 | /** 22 | * Created by rnorth on 29/08/2015. 23 | */ 24 | public class CompositeTest { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(CompositeTest.class); 27 | private Breaker circuitBreaker; 28 | private RateLimiter rateLimiter; 29 | 30 | @Before 31 | public void setUp() throws Exception { 32 | circuitBreaker = BreakerBuilder.newBuilder().build(); 33 | rateLimiter = RateLimiterBuilder.newBuilder() 34 | .withRate(20, TimeUnit.SECONDS) 35 | .withConstantThroughput() 36 | .build(); 37 | } 38 | 39 | @Test 40 | public void simpleCompositeTest() throws Exception { 41 | ExampleService exampleService = new ExampleService(20L); 42 | String result = execute(exampleService); 43 | 44 | assertEquals("the result is passed through", "value", result); 45 | assertEquals("the breaker is not tripped", State.OK, circuitBreaker.getState()); 46 | assertEquals("the service was only called once", 1, exampleService.getInvocationCount()); 47 | } 48 | 49 | @Test 50 | public void simpleCompositeTestWhenTimingOut() throws Exception { 51 | ExampleService exampleService = new ExampleService(110L); 52 | String result = execute(exampleService); 53 | 54 | assertEquals("the default value is returned", "default value", result); 55 | assertEquals("the breaker is tripped", State.BROKEN, circuitBreaker.getState()); 56 | int invocationCount = exampleService.getInvocationCount(); 57 | assertTrue("the service was called less than 40 times (max 20 per second for 2 seconds)", invocationCount <= 40); 58 | } 59 | 60 | private String execute(ExampleService exampleService) throws Exception { 61 | 62 | return circuitBreaker.tryGet(() -> { 63 | return Unreliables.retryUntilSuccess(2, TimeUnit.SECONDS, () -> { 64 | return rateLimiter.getWhenReady(() -> { 65 | return Timeouts.getWithTimeout(100, TimeUnit.MILLISECONDS, () -> { 66 | return exampleService.getValue("Hello World"); 67 | }); 68 | }); 69 | }); 70 | }, () -> { 71 | LOGGER.error("Circuit breaker was tripped"); 72 | }, () -> { 73 | return "default value"; 74 | }); 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/circuitbreakers/AutoResetTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; 9 | import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; 10 | import static org.rnorth.visibleassertions.VisibleAssertions.fail; 11 | 12 | /** 13 | * @author richardnorth 14 | */ 15 | public class AutoResetTest { 16 | 17 | private boolean invoked; 18 | 19 | @Before 20 | public void setUp() throws Exception { 21 | invoked = false; 22 | } 23 | 24 | @Test 25 | public void testRetryAfterExpiry() { 26 | TimeSource.DummyTimeSource dummyTimeSource = new TimeSource.DummyTimeSource(); 27 | 28 | Breaker breaker = BreakerBuilder.newBuilder() 29 | .timeSource(dummyTimeSource) 30 | .autoResetAfter(5, TimeUnit.SECONDS) 31 | .build(); 32 | 33 | // Simulate a failure 34 | assertEquals("The breaker state is initially OK", State.OK, breaker.getState()); 35 | breaker.tryDo(() -> { 36 | throw new RuntimeException(); 37 | }); 38 | assertEquals("The breaker trips after a failure", State.BROKEN, breaker.getState()); 39 | 40 | // Next call should not fire the block 41 | breaker.tryDo(() -> fail("Should not be invoked")); 42 | assertEquals("The breaker remains broken when called again", State.BROKEN, breaker.getState()); 43 | 44 | dummyTimeSource.setCurrentTimeMillis(4999); 45 | breaker.tryDo(() -> fail("Should not be invoked")); 46 | assertEquals("Just before the breaker is due to reset, it remains broken", State.BROKEN, breaker.getState()); 47 | 48 | dummyTimeSource.setCurrentTimeMillis(5000); 49 | breaker.tryDo(this::wasInvoked, () -> fail("Should not be invoked")); 50 | assertEquals("After the reset time has elapsed, the breaker resets to OK", State.OK, breaker.getState()); 51 | assertTrue("After the reset time has elapsed, callables are invoked again", invoked); 52 | } 53 | 54 | @Test 55 | public void testRetryGetAfterExpiry() { 56 | TimeSource.DummyTimeSource dummyTimeSource = new TimeSource.DummyTimeSource(); 57 | 58 | Breaker breaker = BreakerBuilder.newBuilder() 59 | .timeSource(dummyTimeSource) 60 | .autoResetAfter(5, TimeUnit.SECONDS) 61 | .build(); 62 | 63 | // Simulate a failure 64 | assertEquals("The breaker state is initially OK", State.OK, breaker.getState()); 65 | breaker.tryGet(() -> { 66 | throw new RuntimeException(); 67 | }); 68 | assertEquals("The breaker trips after a failure", State.BROKEN, breaker.getState()); 69 | 70 | // Next call should not fire the block 71 | assertEquals("The fallback supplier is invoked after the breaker is tripped", "B", breaker.tryGet(() -> "A", () -> "B")); 72 | assertEquals("The breaker remains broken when called again", State.BROKEN, breaker.getState()); 73 | 74 | dummyTimeSource.setCurrentTimeMillis(4999); 75 | assertEquals("Just before the breaker is due to reset, it remains broken and the fallback supplier is invoked", "B", breaker.tryGet(() -> "A", () -> "B")); 76 | assertEquals("Just before the breaker is due to reset, it remains broken", State.BROKEN, breaker.getState()); 77 | 78 | dummyTimeSource.setCurrentTimeMillis(5000); 79 | assertEquals("After the reset time has elapsed, suppliers are invoked again", "A", breaker.tryGet(() -> "A", () -> "B")); 80 | assertEquals("After the reset time has elapsed, the breaker resets to OK", State.OK, breaker.getState()); 81 | } 82 | 83 | private void wasInvoked() { 84 | invoked = true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/circuitbreakers/ExampleService.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | /** 4 | * @author richardnorth 5 | */ 6 | @SuppressWarnings("ALL") 7 | public class ExampleService { 8 | private long getDelay = 0; 9 | private int invocationCount = 0; 10 | 11 | public ExampleService() { 12 | 13 | } 14 | 15 | public ExampleService(long getDelay) { 16 | this.getDelay = getDelay; 17 | } 18 | 19 | public void sendMessage(Object message) { 20 | 21 | } 22 | 23 | public String getValue(String key) { 24 | try { 25 | Thread.sleep(getDelay); 26 | } catch (InterruptedException ignored) { 27 | } 28 | 29 | invocationCount++; 30 | return "value"; 31 | } 32 | 33 | public int getInvocationCount() { 34 | return invocationCount; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/circuitbreakers/ExampleTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.junit.Test; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.util.*; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.ConcurrentMap; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * Examples for inclusion in documentation. 14 | * 15 | * @author richardnorth 16 | */ 17 | public class ExampleTest { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(ExampleTest.class); 20 | 21 | @Test 22 | public void exampleUsage() { 23 | 24 | Breaker breaker = BreakerBuilder.newBuilder().build(); 25 | ExampleService someUnreliableService = new ExampleService(); 26 | Queue fallbackQueue = new PriorityQueue<>(); 27 | Object message = new Object(); 28 | String key = "key"; 29 | Map fallbackCache = new HashMap<>(); 30 | 31 | // tag::ExampleDoFailFallback[] 32 | breaker.tryDo(() -> { 33 | someUnreliableService.sendMessage(message); 34 | }, () -> { 35 | LOGGER.error("Service failed!"); 36 | }, () -> { 37 | fallbackQueue.add(message); 38 | }); 39 | // end::ExampleDoFailFallback[] 40 | 41 | // tag::ExampleDoFallback[] 42 | breaker.tryDo(() -> { 43 | someUnreliableService.sendMessage(message); 44 | }, () -> { 45 | fallbackQueue.add(message); 46 | }); 47 | // end::ExampleDoFallback[] 48 | 49 | // tag::ExampleDo[] 50 | breaker.tryDo(() -> { 51 | someUnreliableService.sendMessage(message); 52 | }); 53 | // end::ExampleDo[] 54 | 55 | // tag::ExampleGetFailFallback[] 56 | String response = breaker.tryGet(() -> { 57 | return someUnreliableService.getValue(key); 58 | }, () -> { 59 | LOGGER.error("Service failed!"); 60 | }, () -> { 61 | return fallbackCache.get(key); 62 | }); 63 | // end::ExampleGetFailFallback[] 64 | 65 | // tag::ExampleGetFallback[] 66 | response = breaker.tryGet(() -> { 67 | return someUnreliableService.getValue(key); 68 | }, () -> { 69 | return fallbackCache.get(key); 70 | }); 71 | // end::ExampleGetFallback[] 72 | 73 | // tag::ExampleGet[] 74 | Optional optional = breaker.tryGet(() -> { 75 | return someUnreliableService.getValue(key); 76 | }); 77 | 78 | if (optional.isPresent()) { 79 | // do something with optional.get() 80 | } 81 | // end::ExampleGet[] 82 | } 83 | 84 | @Test 85 | public void exampleBuilder() { 86 | 87 | ConcurrentMap myMap = new ConcurrentHashMap<>(); 88 | Breaker breaker; 89 | 90 | // tag::ExampleSimpleBuild[] 91 | breaker = BreakerBuilder.newBuilder() 92 | .build(); 93 | // end::ExampleSimpleBuild[] 94 | 95 | // tag::ExampleAutoResetBuild[] 96 | breaker = BreakerBuilder.newBuilder() 97 | .autoResetAfter(1, TimeUnit.MINUTES) 98 | .build(); 99 | // end::ExampleAutoResetBuild[] 100 | 101 | // tag::ExampleExternalStoreBuild[] 102 | breaker = BreakerBuilder.newBuilder() 103 | .storeStateIn(myMap, "ExampleCircuitBreaker") 104 | .build(); 105 | // end::ExampleExternalStoreBuild[] 106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/circuitbreakers/ExternalStateTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ConcurrentMap; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import static org.mockito.Mockito.*; 12 | import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; 13 | import static org.rnorth.visibleassertions.VisibleAssertions.assertNotEquals; 14 | 15 | /** 16 | * @author richardnorth 17 | */ 18 | public class ExternalStateTest { 19 | 20 | @Test 21 | public void testUseOfStateStore() { 22 | 23 | StateStore mockStateStore = mock(StateStore.class); 24 | 25 | Breaker breaker = BreakerBuilder.newBuilder() 26 | .timeSource(new TimeSource.DummyTimeSource(10)) // 'now' is always 10 27 | .autoResetAfter(1, TimeUnit.MILLISECONDS) 28 | .storeStateIn(mockStateStore) 29 | .build(); 30 | 31 | when(mockStateStore.getState()).thenReturn(State.OK); 32 | assertEquals("When the state store reports state is OK, the first supplier is called", "called", breaker.tryGet(() -> "called").get()); 33 | 34 | when(mockStateStore.getState()).thenReturn(State.BROKEN); 35 | when(mockStateStore.getLastFailure()).thenReturn(10L); 36 | assertEquals("When the state store reports state is BROKEN, the fallback supplier is called", "not called", breaker.tryGet(() -> "called", () -> "not called")); 37 | 38 | when(mockStateStore.getState()).thenReturn(State.BROKEN); 39 | when(mockStateStore.getLastFailure()).thenReturn(9L); 40 | assertEquals("When the state store reports state is BROKEN but expired, the first supplier is called", "called", breaker.tryGet(() -> "called").get()); 41 | 42 | verify(mockStateStore); 43 | } 44 | 45 | @Test 46 | public void testMapStateStore() { 47 | Map map = new HashMap<>(); 48 | 49 | MapBackedStateStore store = new MapBackedStateStore(map, "TEST"); 50 | 51 | assertEquals("The state store defaults to OK", State.OK, store.getState()); // initial state 52 | 53 | store.setState(State.BROKEN); 54 | assertEquals("The state store can be set to BROKEN", State.BROKEN, store.getState()); 55 | 56 | store.setState(State.OK); 57 | assertEquals("The state store can be set to OK", State.OK, store.getState()); 58 | 59 | store.setLastFailure(666L); 60 | assertEquals("The state store last failure time can be set", 666L, store.getLastFailure()); 61 | 62 | MapBackedStateStore otherStoreUsingSameMap = new MapBackedStateStore(map, "ANOTHERPREFIX"); 63 | store.setLastFailure(444L); 64 | assertNotEquals("The state store can hold more than one last failure time", 444L, otherStoreUsingSameMap.getLastFailure()); 65 | otherStoreUsingSameMap.setState(State.OK); 66 | store.setState(State.BROKEN); 67 | assertNotEquals("The state store stores a separate state for each breaker prefix", State.BROKEN, otherStoreUsingSameMap.getState()); 68 | } 69 | 70 | @Test 71 | public void testBuilderUsingMapBackedStore() { 72 | 73 | ConcurrentMap map = new ConcurrentHashMap<>(); 74 | 75 | Breaker breaker = BreakerBuilder.newBuilder() 76 | .storeStateIn(map, "PREFIX") 77 | .build(); 78 | 79 | assertEquals("The state store is not used before the breaker is called", 0, map.size()); 80 | 81 | breaker.tryDo(() -> { throw new RuntimeException(); }); 82 | 83 | assertEquals("The state store is used when the breaker is called", 2, map.size()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/circuitbreakers/SimpleBreakerTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.circuitbreakers; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.stream.IntStream; 10 | 11 | import static java.util.Arrays.asList; 12 | import static org.junit.Assert.assertEquals; 13 | import static org.junit.Assert.assertNotNull; 14 | 15 | /** 16 | * @author richardnorth 17 | */ 18 | public class SimpleBreakerTest { 19 | 20 | private List doCalls; 21 | 22 | @Before 23 | public void setup() { 24 | doCalls = new ArrayList<>(); 25 | } 26 | 27 | @Test 28 | public void testDoFailBrokenMethods() { 29 | Breaker breaker = BreakerBuilder.newBuilder().build(); 30 | 31 | assertNotNull(breaker); 32 | 33 | IntStream.range(0, 5).forEach(j -> 34 | 35 | breaker.tryDo(() -> { 36 | doCalled(); 37 | if (j == 2) throw new RuntimeException(); 38 | }, this::onFailCalled, this::ifBrokenCalled) 39 | 40 | ); 41 | 42 | assertEquals(asList( 43 | Call.DO, 44 | Call.DO, 45 | Call.DO, Call.FAILED, Call.BROKEN, // Fail on the third iteration, calling fail handler and broken handler too 46 | Call.BROKEN, // Just call broken handler 47 | Call.BROKEN), doCalls); 48 | } 49 | 50 | @Test 51 | public void testDoBrokenMethods() { 52 | Breaker breaker = BreakerBuilder.newBuilder().build(); 53 | 54 | assertNotNull(breaker); 55 | 56 | IntStream.range(0, 5).forEach(j -> 57 | 58 | breaker.tryDo(() -> { 59 | doCalled(); 60 | if (j == 2) throw new RuntimeException(); 61 | }, this::ifBrokenCalled) 62 | 63 | ); 64 | 65 | assertEquals(asList( 66 | Call.DO, 67 | Call.DO, 68 | Call.DO, Call.BROKEN, // Fail on the third iteration, calling broken handler too 69 | Call.BROKEN, // Just call broken handler 70 | Call.BROKEN), doCalls); 71 | } 72 | 73 | @Test 74 | public void testJustDoMethod() { 75 | Breaker breaker = BreakerBuilder.newBuilder().build(); 76 | 77 | assertNotNull(breaker); 78 | 79 | IntStream.range(0, 5).forEach(j -> 80 | 81 | breaker.tryDo(() -> { 82 | doCalled(); 83 | if (j == 2) throw new RuntimeException(); 84 | }) 85 | 86 | ); 87 | 88 | assertEquals(asList( 89 | Call.DO, 90 | Call.DO, 91 | Call.DO), doCalls); 92 | } 93 | 94 | @Test 95 | public void testGetFailBrokenMethods() { 96 | Breaker breaker = BreakerBuilder.newBuilder().build(); 97 | 98 | assertNotNull(breaker); 99 | 100 | List gets = asList(IntStream.range(0, 5).mapToObj(j -> 101 | 102 | breaker.tryGet(() -> { 103 | doCalled(); 104 | if (j == 2) throw new RuntimeException(); 105 | return "GET" + j; 106 | }, this::onFailCalled, () -> { 107 | ifBrokenCalled(); 108 | return "BROKEN" + j; 109 | }) 110 | 111 | ).toArray()); 112 | 113 | assertEquals(asList( 114 | "GET0", 115 | "GET1", 116 | "BROKEN2", // Fail on the third iteration, with only the failure return result 117 | "BROKEN3", // Just call broken handler 118 | "BROKEN4"), gets); 119 | 120 | assertEquals(asList( 121 | Call.DO, 122 | Call.DO, 123 | Call.DO, Call.FAILED, Call.BROKEN, // Fail on the third iteration, calling fail handler and broken handler too 124 | Call.BROKEN, // Just call broken handler 125 | Call.BROKEN), doCalls); 126 | } 127 | 128 | @Test 129 | public void testGetBrokenMethods() { 130 | Breaker breaker = BreakerBuilder.newBuilder().build(); 131 | 132 | assertNotNull(breaker); 133 | 134 | List gets = asList(IntStream.range(0, 5).mapToObj(j -> 135 | 136 | breaker.tryGet(() -> { 137 | doCalled(); 138 | if (j == 2) throw new RuntimeException(); 139 | return "GET" + j; 140 | }, () -> { 141 | ifBrokenCalled(); 142 | return "BROKEN" + j; 143 | }) 144 | 145 | ).toArray()); 146 | 147 | assertEquals(asList( 148 | "GET0", 149 | "GET1", 150 | "BROKEN2", // Fail on the third iteration, with only the failure return result 151 | "BROKEN3", // Just call broken handler 152 | "BROKEN4"), gets); 153 | 154 | assertEquals(asList( 155 | Call.DO, 156 | Call.DO, 157 | Call.DO, Call.BROKEN, // Fail on the third iteration, calling fail handler and broken handler too 158 | Call.BROKEN, // Just call broken handler 159 | Call.BROKEN), doCalls); 160 | } 161 | 162 | @Test 163 | public void testJustGetMethod() { 164 | Breaker breaker = BreakerBuilder.newBuilder().build(); 165 | 166 | assertNotNull(breaker); 167 | 168 | List gets = asList(IntStream.range(0, 5).mapToObj(j -> 169 | 170 | breaker.tryGet(() -> { 171 | doCalled(); 172 | if (j == 2) throw new RuntimeException(); 173 | return "GET" + j; 174 | }) 175 | 176 | ).toArray()); 177 | 178 | assertEquals(asList( 179 | Optional.of("GET0"), 180 | Optional.of("GET1"), 181 | Optional.empty(), // Fail on the third iteration, with only the failure return result 182 | Optional.empty(), // Just call broken handler 183 | Optional.empty()), gets); 184 | 185 | assertEquals(asList( 186 | Call.DO, 187 | Call.DO, 188 | Call.DO), doCalls); 189 | } 190 | 191 | private void ifBrokenCalled() { 192 | doCalls.add(Call.BROKEN); 193 | } 194 | 195 | private void onFailCalled() { 196 | doCalls.add(Call.FAILED); 197 | } 198 | 199 | private void doCalled() { 200 | doCalls.add(Call.DO); 201 | } 202 | 203 | private enum Call { 204 | DO, FAILED, BROKEN 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/inconsistents/InconsistentsTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.inconsistents; 2 | 3 | import org.junit.Test; 4 | import org.rnorth.ducttape.TimeoutException; 5 | 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import static org.junit.Assert.assertTrue; 9 | import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; 10 | 11 | 12 | /** 13 | * Tests for Inconsistents class. 14 | */ 15 | public class InconsistentsTest { 16 | 17 | @Test 18 | public void testEventualConsistent() { 19 | long start = System.currentTimeMillis(); 20 | Long result = Inconsistents.retryUntilConsistent(50, 1000, TimeUnit.MILLISECONDS, () -> { 21 | Thread.sleep(10L); 22 | // this result won't be consistent until after 100ms 23 | long now = System.currentTimeMillis(); 24 | return (now - start) / 100; 25 | }); 26 | } 27 | 28 | @Test 29 | public void testNeverConsistent() { 30 | try { 31 | Inconsistents.retryUntilConsistent(50, 1000, TimeUnit.MILLISECONDS, () -> { 32 | Thread.sleep(10L); 33 | return System.currentTimeMillis(); 34 | }); 35 | } catch (TimeoutException e) { 36 | Throwable cause = e.getCause(); 37 | assertEquals("An exception is thrown if the result is never consistent", ResultsNeverConsistentException.class, cause.getClass()); 38 | } 39 | } 40 | 41 | @Test 42 | public void testNotConsistentLongEnough() { 43 | try { 44 | Inconsistents.retryUntilConsistent(50, 1000, TimeUnit.MILLISECONDS, () -> { 45 | Thread.sleep(10L); 46 | return System.currentTimeMillis() / 49; 47 | }); 48 | } catch (TimeoutException e) { 49 | Throwable cause = e.getCause(); 50 | assertEquals("An exception is thrown if the result is never consistent", InconsistentResultsException.class, cause.getClass()); 51 | } 52 | } 53 | 54 | @Test 55 | public void testUnitConversion() { 56 | long start = System.currentTimeMillis(); 57 | 58 | Inconsistents.retryUntilConsistent(1, 5, TimeUnit.SECONDS, () -> { 59 | Thread.sleep(10L); 60 | // this result won't be consistent until after 1.5s 61 | long now = System.currentTimeMillis(); 62 | return (now - start) / 1500; 63 | }); 64 | 65 | assertTrue("At least one second elapsed", System.currentTimeMillis() - start > 1000); 66 | } 67 | } -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/ratelimits/RateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.ratelimits; 2 | 3 | import org.junit.Test; 4 | import org.rnorth.ducttape.TimeoutException; 5 | import org.rnorth.ducttape.timeouts.Timeouts; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; 10 | import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; 11 | 12 | /** 13 | * Tests for RateLimiter. 14 | */ 15 | public class RateLimiterTest { 16 | 17 | @Test 18 | public void testLimitExecutions() { 19 | 20 | int[] testWindow = new int[1]; 21 | 22 | RateLimiter rateLimiter = RateLimiterBuilder.newBuilder() 23 | .withRate(10, TimeUnit.SECONDS) 24 | .withConstantThroughput() 25 | .build(); 26 | 27 | try { 28 | Timeouts.getWithTimeout(2, TimeUnit.SECONDS, ()-> { 29 | //noinspection InfiniteLoopStatement 30 | while (true) { 31 | rateLimiter.doWhenReady(() -> { 32 | testWindow[0]++; 33 | }); 34 | } 35 | }); 36 | } catch (TimeoutException ignored) { 37 | // We're just using a timeout here to limit execution to a given time 38 | } 39 | 40 | // Approximate estimates 41 | assertTrue("The rate limiter should have kept executions at or below 21", testWindow[0] <= 21); 42 | assertTrue("The rate limiter should allowed at least 15 executions", testWindow[0] >= 15); 43 | } 44 | 45 | @Test 46 | public void testLimitExecutionsAndGetResult() { 47 | 48 | int[] testWindow = new int[1]; 49 | int[] lastValue = new int[1]; 50 | 51 | RateLimiter rateLimiter = RateLimiterBuilder.newBuilder() 52 | .withRate(10, TimeUnit.SECONDS) 53 | .withConstantThroughput() 54 | .build(); 55 | 56 | try { 57 | Timeouts.getWithTimeout(2, TimeUnit.SECONDS, ()-> { 58 | //noinspection InfiniteLoopStatement 59 | while (true) { 60 | lastValue[0] = rateLimiter.getWhenReady(() -> { 61 | return ++testWindow[0]; 62 | }); 63 | } 64 | }); 65 | } catch (TimeoutException ignored) { 66 | // We're just using a timeout here to limit execution to a given time 67 | } 68 | 69 | // Approximate estimates 70 | assertTrue("The rate limiter should have kept executions at or below 21", testWindow[0] <= 21); 71 | assertTrue("The rate limiter should allowed at least 15 executions", testWindow[0] >= 15); 72 | 73 | assertEquals("The rate limiter returns a result", testWindow[0], lastValue[0]); 74 | } 75 | } -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/timeouts/TimeoutsTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.timeouts; 2 | 3 | import org.junit.Test; 4 | import org.rnorth.ducttape.TimeoutException; 5 | 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; 9 | import static org.rnorth.visibleassertions.VisibleAssertions.assertThrows; 10 | 11 | /** 12 | * Tests for Timeouts class. 13 | */ 14 | public class TimeoutsTest { 15 | 16 | @Test 17 | public void timeoutThrowsException() { 18 | assertThrows("It throws a TimeoutException if execution time is exceeded", TimeoutException.class, () -> { 19 | Timeouts.doWithTimeout(1, TimeUnit.SECONDS, () -> { 20 | try { 21 | Thread.sleep(2000L); 22 | } catch (InterruptedException ignored) { } 23 | }); 24 | }); 25 | } 26 | 27 | @Test 28 | public void withinTimeIsOk() { 29 | Timeouts.doWithTimeout(1, TimeUnit.SECONDS, () -> { 30 | try { 31 | Thread.sleep(1L); 32 | } catch (InterruptedException ignored) { } 33 | }); 34 | } 35 | 36 | @Test 37 | public void timeoutThrowsExceptionWithoutReturnValue() { 38 | assertThrows("It throws a TimeoutException if execution time is exceeded", TimeoutException.class, () -> { 39 | Timeouts.getWithTimeout(1, TimeUnit.SECONDS, () -> { 40 | try { 41 | Thread.sleep(2000L); 42 | } catch (InterruptedException ignored) { } 43 | return "result"; 44 | }); 45 | }); 46 | } 47 | 48 | @Test 49 | public void withinTimeIsOkAndCanReturnResult() { 50 | String result = Timeouts.getWithTimeout(1, TimeUnit.SECONDS, () -> { 51 | try { 52 | Thread.sleep(1L); 53 | } catch (InterruptedException ignored) { } 54 | return "result"; 55 | }); 56 | 57 | assertEquals("A result is returned from the lambda", "result", result); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/org/rnorth/ducttape/unreliables/UnreliablesTest.java: -------------------------------------------------------------------------------- 1 | package org.rnorth.ducttape.unreliables; 2 | 3 | import org.junit.Test; 4 | import org.rnorth.ducttape.RetryCountExceededException; 5 | import org.rnorth.ducttape.TimeoutException; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | 10 | import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; 11 | import static org.rnorth.visibleassertions.VisibleAssertions.assertFalse; 12 | import static org.rnorth.visibleassertions.VisibleAssertions.assertThrows; 13 | import static org.rnorth.visibleassertions.VisibleAssertions.fail; 14 | 15 | /** 16 | * Tests for Unreliables class. 17 | */ 18 | public class UnreliablesTest { 19 | 20 | @Test 21 | public void testRetryUntilTrueImmediateSuccess() throws Exception { 22 | try { 23 | Unreliables.retryUntilTrue(500, TimeUnit.MILLISECONDS, () -> true); 24 | } catch (TimeoutException e) { 25 | fail("When retrying until true, an immediate return true should be OK but timed out"); 26 | } 27 | } 28 | 29 | @Test 30 | public void testRetryUntilTrueSuccessWithinTimeoutWindow() throws Exception { 31 | try { 32 | Unreliables.retryUntilTrue(500, TimeUnit.MILLISECONDS, () -> { 33 | Thread.sleep(300L); 34 | return true; 35 | }); 36 | } catch (TimeoutException e) { 37 | fail("When retrying until true, a return true within the timeout window should be OK but timed out"); 38 | } 39 | } 40 | 41 | @Test 42 | public void testRetryUntilTrueSuccessWithinTimeoutWindowWithManyFailures() throws Exception { 43 | long start = System.currentTimeMillis(); 44 | try { 45 | Unreliables.retryUntilTrue(500, TimeUnit.MILLISECONDS, () -> { 46 | return System.currentTimeMillis() - start > 300; 47 | }); 48 | } catch (TimeoutException e) { 49 | fail("When retrying until true, a return true within the timeout window should be OK but timed out"); 50 | } 51 | } 52 | 53 | @Test 54 | public void testRetryUntilTrueFailsWhenOutsideTimeoutWindow() throws Exception { 55 | try { 56 | Unreliables.retryUntilTrue(500, TimeUnit.MILLISECONDS, () -> { 57 | Thread.sleep(600L); 58 | return true; 59 | }); 60 | fail("When retrying until true, a return true outside the timeout window should throw a timeout exception"); 61 | } catch (TimeoutException e) { 62 | // ok 63 | } 64 | } 65 | 66 | @Test 67 | public void testRetryUntilSuccessImmediateSuccess() throws Exception { 68 | try { 69 | String result = Unreliables.retryUntilSuccess(500, TimeUnit.MILLISECONDS, () -> "OK"); 70 | assertEquals("A result can be returned using retryUntilSuccess", "OK", result); 71 | } catch (TimeoutException e) { 72 | fail("When retrying until true, an immediate return true should be OK but timed out"); 73 | } 74 | } 75 | 76 | @Test 77 | public void testRetryUntilSuccessWithinTimeoutWindow() throws Exception { 78 | try { 79 | String result = Unreliables.retryUntilSuccess(500, TimeUnit.MILLISECONDS, () -> { 80 | Thread.sleep(300L); 81 | return "OK"; 82 | }); 83 | assertEquals("A result can be returned using retryUntilSuccess", "OK", result); 84 | } catch (TimeoutException e) { 85 | fail("When retrying until true, a return true within the timeout window should be OK but timed out"); 86 | } 87 | } 88 | 89 | @Test 90 | public void testRetryUntilSuccessWithinTimeoutWindowWithManyFailures() throws Exception { 91 | long start = System.currentTimeMillis(); 92 | try { 93 | String result = Unreliables.retryUntilSuccess(500, TimeUnit.MILLISECONDS, () -> { 94 | if (System.currentTimeMillis() - start < 300) { 95 | throw new Exception("FAILURE"); 96 | } 97 | return "OK"; 98 | }); 99 | assertEquals("A result can be returned using retryUntilSuccess", "OK", result); 100 | } catch (TimeoutException e) { 101 | fail("When retrying until true, a return true within the timeout window should be OK but timed out"); 102 | } 103 | } 104 | 105 | @Test 106 | public void testRetryUntilSuccessFailsWhenOutsideTimeoutWindow() throws Exception { 107 | String result = "NOT OK"; 108 | try { 109 | result = Unreliables.retryUntilSuccess(500, TimeUnit.MILLISECONDS, () -> { 110 | Thread.sleep(600L); 111 | return "OK"; 112 | }); 113 | fail("When retrying until true, a return true outside the timeout window should throw a timeout exception"); 114 | } catch (TimeoutException e) { 115 | // ok 116 | assertEquals("A result can be returned using retryUntilSuccess", "NOT OK", result); 117 | } 118 | } 119 | 120 | @Test 121 | public void testRetryUntilSuccessLambdaStopsBeingCalledAfterTimeout() throws Exception { 122 | AtomicBoolean lambdaCalled = new AtomicBoolean(); 123 | try { 124 | Unreliables.retryUntilSuccess(200, TimeUnit.MILLISECONDS, () -> { 125 | lambdaCalled.set(true); 126 | throw new RuntimeException(); 127 | }); 128 | fail("Expected TimeoutException on retryUntilSuccess that always fails."); 129 | } catch (TimeoutException e) { 130 | // ok 131 | Thread.sleep(200); // give worker thread time to stop calling the lambda 132 | lambdaCalled.set(false); 133 | Thread.sleep(200); // give worker thread time to call the lambda if it is still running 134 | assertFalse("Lambda should stop being executed when retryUntilSuccess times out.", lambdaCalled.get()); 135 | } 136 | } 137 | 138 | @Test 139 | public void testRetryUntilSuccessFailsWhenOutsideTimeoutWindowAndCapturesException() throws Exception { 140 | try { 141 | Unreliables.retryUntilSuccess(500, TimeUnit.MILLISECONDS, () -> { 142 | throw new IllegalStateException("This is the exception"); 143 | }); 144 | fail("When retrying until true, a return true outside the timeout window should throw a timeout exception"); 145 | } catch (TimeoutException e) { 146 | // ok 147 | assertEquals("A result can be returned using retryUntilSuccess", "This is the exception", e.getCause().getMessage()); 148 | } 149 | } 150 | 151 | @Test 152 | public void testRetryUntilSuccessPassesForSuccessWithinCount() throws Exception { 153 | 154 | final int[] attempt = {0}; 155 | 156 | String result = Unreliables.retryUntilSuccess(3, () -> { 157 | attempt[0]++; 158 | if (attempt[0] == 2) { 159 | return "OK"; 160 | } else { 161 | throw new IllegalStateException("This will fail sometimes"); 162 | } 163 | }); 164 | assertEquals("If success happens before the retry limit, that's OK", "OK", result); 165 | } 166 | 167 | @Test 168 | public void testRetryUntilSuccessFailsForFailuresOutsideCount() throws Exception { 169 | try { 170 | Unreliables.retryUntilSuccess(3, () -> { 171 | throw new IllegalStateException("This will always fail"); 172 | }); 173 | fail("When retrying until true, a return true outside the timeout window should throw a retry failure exception"); 174 | } catch (RetryCountExceededException e) { 175 | // ok 176 | assertEquals("A result can be returned using retryUntilSuccess", "This will always fail", e.getCause().getMessage()); 177 | } 178 | } 179 | 180 | @Test 181 | public void testRetryUntilTruePassesForSuccessWithinCount() throws Exception { 182 | 183 | final int[] attempt = {0}; 184 | 185 | Unreliables.retryUntilTrue(3, () -> { 186 | attempt[0]++; 187 | if (attempt[0] == 2) { 188 | return true; 189 | } else { 190 | return false; 191 | } 192 | }); 193 | } 194 | 195 | @Test 196 | public void testRetryUntilTrueFailsForFailuresOutsideCount() throws Exception { 197 | 198 | assertThrows("When retrying until true, a return true outside the timeout window should throw a retry failure exception", 199 | RetryCountExceededException.class, 200 | () -> { 201 | Unreliables.retryUntilTrue(3, () -> false); 202 | }); 203 | } 204 | } 205 | --------------------------------------------------------------------------------