├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── com │ └── eternus │ └── ratelimit │ ├── CoarseMemoryTokenStore.java │ ├── EhcacheTokenStore.java │ ├── Enablable.java │ ├── FixedBucket.java │ ├── Key.java │ ├── MemoryTokenStore.java │ ├── RateLimiter.java │ ├── StoreEntry.java │ ├── StoreEntryImpl.java │ ├── Token.java │ ├── TokenInstance.java │ ├── TokenStore.java │ ├── circuitbreaker │ ├── CircuitBreaker.java │ ├── CircuitBreakerConfiguration.java │ ├── CircuitBreakerException.java │ ├── CircuitBreakerImpl.java │ ├── CircuitBreakerListener.java │ ├── CircuitBreakerOpenException.java │ ├── CircuitBreakerState.java │ ├── ClosedState.java │ ├── HalfOpenState.java │ ├── OpenState.java │ └── package-info.java │ ├── jmx │ ├── ManagedRateLimiter.java │ └── ManagedRateLimiterMBean.java │ └── package-info.java └── test └── java └── com └── eternus └── ratelimit ├── FixedBucketTests.java ├── RateLimiterKey.java ├── TestFixedBucketWithCoarseMemoryStore.java ├── TestFixedBucketWithEhcache.java ├── TestFixedBucketWithMemoryStore.java └── circuitbreaker └── TestCircuitBreaker.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # Override line-endings 2 | * text eol=lf 3 | *.jar binary 4 | *.bat text eol=crlf 5 | *.cmd text eol=crlf 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | TEST-* 3 | .classpath 4 | .project 5 | dist 6 | ivy/ivy.jar 7 | target 8 | 9 | build/ 10 | .gradle/ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 James Abley 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Contains the primitives and utilities used to rate-limit / throttle Java 4 | applications, and a CircuitBreaker implementation. 5 | 6 | # Summary 7 | Inspired by reading Cal Henderson's "Building Scalable Web Sites" which talks 8 | briefly about this, and having been on the receiving end of a kicking from 9 | search engines, I wanted to have a simple way of determining whether to bother 10 | processing requests and stop consuming server resources in a graceful way, 11 | rather than grinding to a halt. 12 | 13 | ## Background - types of throttling 14 | 15 | ### Next Service Slot 16 | 17 | Each time a request comes in, we log the time. If it hasn't been a certain 18 | duration since the last request, then abort with a rate-limiting error. 19 | 20 | key = create_key(request) 21 | 22 | entry = gate.get_entry(key) 23 | 24 | if (entry) 25 | response.set_status(SERVICE_UNAVAILABLE) 26 | return 27 | end 28 | 29 | entry = create_entry(expires => '5s') 30 | 31 | gate.put_entry(key, entry) 32 | 33 | ... 34 | 35 | 36 | ### Fixed Bucket 37 | 38 | We define a duration and an acceptable number of requests to be serviced in 39 | that time. Each time a request comes in, we look up the number of calls made 40 | in the current period. If it is at or above the limit, then abort with a 41 | rate-limiting error, otherwise increment the counter and service the request. 42 | 43 | key = create_key(request) 44 | 45 | entry = gate.get_entry(key) 46 | 47 | if (entry.count >= ALLOWED_PER_PERIOD) 48 | response.set_status(SERVICE_UNAVAILABLE) 49 | return 50 | end 51 | 52 | entry.count.increment() 53 | 54 | ... 55 | 56 | From this description, it can be seen that Next Service Slot is essentially 57 | Fixed Bucket with a max size of 1 and an appropriate service period. 58 | 59 | ### Leaky Bucket 60 | 61 | Similar to a Fixed Bucket, except that rather than aborting, we block until 62 | the end of the current time period upon which the bucket counter is 63 | decremented / completely emptied and then we service the request. 64 | 65 | Hardest to implement, has the disadvantage that it will tie up a 66 | request-handling thread (which may cause upstream services to timeout / retry) 67 | but may be a good solution in other contexts. 68 | 69 | key = create_key(request) 70 | 71 | entry = gate.get_entry(key) 72 | 73 | if (entry.count >= ALLOWED_PER_PERIOD) 74 | entry.wait() 75 | end 76 | 77 | entry.count.increment() 78 | 79 | ... 80 | 81 | ## CircuitBreaker 82 | 83 | There is some overlap in the intention of this library with the Circuit Breaker 84 | approach described by Michael Nygard in his excellent book "Release It!"; I've 85 | done some work to add support for that as well. We've been running it in 86 | production for a year and it works well for our purposes. 87 | 88 | Please see the tests for details as to how to use it. 89 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'jacoco' 4 | id 'org.owasp.dependencycheck' version "3.1.1" 5 | } 6 | 7 | repositories { 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | compile 'net.sf.ehcache:ehcache:1.5.0' 13 | compile 'com.google.guava:guava:r07' 14 | testCompile 'junit:junit:4.5' 15 | } 16 | 17 | def versionString = "1.2.3-SNAPSHOT" 18 | 19 | jar { 20 | version = versionString 21 | } 22 | 23 | task sourcesJar(type: Jar) { 24 | classifier = 'sources' 25 | version = versionString 26 | from sourceSets.main.allSource 27 | } 28 | 29 | assemble.dependsOn sourcesJar 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabley/rate-limit/8feb267ff83fb62c7dfe515828b1197e3a8dcde5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user guide at https://docs.gradle.org/4.5.1/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'rate-limit' 11 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/CoarseMemoryTokenStore.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package com.eternus.ratelimit; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.locks.Lock; 9 | import java.util.concurrent.locks.ReentrantLock; 10 | 11 | /** 12 | * {@link TokenStore} that uses a coarse-grained lock to manage access to the internal StoreEntry items. 13 | * 14 | * @author jabley 15 | * 16 | */ 17 | public class CoarseMemoryTokenStore implements TokenStore { 18 | 19 | /** 20 | * The Map used to keep track of {@link StoreEntry} instances. 21 | */ 22 | private final Map cache; 23 | 24 | /** 25 | * The lock used to synchronize on. 26 | */ 27 | private final Lock lock; 28 | 29 | /** 30 | * Creates a new {@link CoarseMemoryTokenStore}. 31 | */ 32 | public CoarseMemoryTokenStore() { 33 | this.cache = new HashMap(); 34 | this.lock = new ReentrantLock(); 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public StoreEntry create(Key key, int timeToLiveInSecs) { 41 | try { 42 | StoreEntryImpl result = new StoreEntryImpl(timeToLiveInSecs); 43 | cache.put(key, result); 44 | return result; 45 | } finally { 46 | lock.unlock(); 47 | } 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public StoreEntry get(Key key) { 54 | lock.lock(); 55 | 56 | StoreEntry result = cache.get(key); 57 | 58 | if (!(result == null || result.isExpired())) { 59 | 60 | /* cache hit with good entry - use it. */ 61 | lock.unlock(); 62 | return result; 63 | } else { 64 | 65 | /* cache miss or expired. keep the lock and the client will call #create(Key, int) */ 66 | result = null; 67 | cache.put(key, result); 68 | } 69 | 70 | return result; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/EhcacheTokenStore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | import net.sf.ehcache.Ehcache; 19 | import net.sf.ehcache.Element; 20 | import net.sf.ehcache.constructs.blocking.BlockingCache; 21 | import net.sf.ehcache.constructs.blocking.LockTimeoutException; 22 | 23 | /** 24 | * {@link TokenStore} implementation that uses ehcache as the backing store for {@link StoreEntry}s. 25 | * 26 | * @author jabley 27 | * 28 | */ 29 | public class EhcacheTokenStore implements TokenStore { 30 | 31 | /** 32 | * The {@link BlockingCache} used to back this {@link TokenStore}. 33 | */ 34 | private BlockingCache cache; 35 | 36 | /** 37 | * Sets the non-null {@link Ehcache} used to back this {@link TokenStore}. 38 | * 39 | * @param cache 40 | * a non-null {@link Ehcache} 41 | */ 42 | public void setCache(Ehcache cache) { 43 | BlockingCache ref; 44 | 45 | if (!(cache instanceof BlockingCache)) { 46 | ref = new BlockingCache(cache); 47 | cache.getCacheManager().replaceCacheWithDecoratedCache(cache, new BlockingCache(cache)); 48 | } else { 49 | ref = (BlockingCache) cache; 50 | } 51 | 52 | this.cache = ref; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public StoreEntry create(Key key, int timeToLive) { 59 | StoreEntryImpl result = new StoreEntryImpl(timeToLive); 60 | Element element = new Element(key, result); 61 | element.setTimeToLive(timeToLive); 62 | cache.put(element); 63 | return result; 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public StoreEntry get(Key key) { 70 | 71 | Element entry = null; 72 | 73 | try { 74 | 75 | /* This may block. */ 76 | entry = cache.get(key); 77 | } catch (LockTimeoutException e) { 78 | throw new RuntimeException(); 79 | } catch (RuntimeException e) { 80 | 81 | /* Release the lock that may have been acquired. */ 82 | cache.put(new Element(key, null)); 83 | } 84 | 85 | StoreEntry result = null; 86 | 87 | if (entry != null) { 88 | 89 | /* 90 | * We don't need to check isExpired() on the result, since ehcache takes care of expiring entries for us. 91 | * c.f. the get(Key) implementation in this class. 92 | */ 93 | result = (StoreEntry) entry.getObjectValue(); 94 | } 95 | 96 | return result; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/Enablable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | /** 19 | * Interface defining whether a service can be enabled or not. 20 | * 21 | * @author jabley 22 | * 23 | */ 24 | public interface Enablable { 25 | 26 | /** 27 | * Returns true if this service is enabled, otherwise false. 28 | * 29 | * @return true if this service is enabled, otherwise false 30 | */ 31 | boolean isEnabled(); 32 | 33 | /** 34 | * Sets the enabled state of this service. 35 | * 36 | * @param enabled 37 | * the enabled state 38 | */ 39 | void setEnabled(boolean enabled); 40 | 41 | } -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/FixedBucket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | /** 19 | *

20 | * {@link RateLimiter} implementation which provides a fixed-bucket algorithm for rate-limiting access to services. 21 | *

22 | * 23 | *

24 | * For a fixed-bucket algorithm, we define a duration and an acceptable number of requests to be serviced in that time. 25 | * Each time a request comes in, we look up the number of calls made in the current period. If it is at or above the 26 | * limit, then abort with a rate-limiting error, otherwise increment the counter and service the request. 27 | *

28 | * 29 | * @author jabley 30 | * 31 | */ 32 | public class FixedBucket implements RateLimiter { 33 | 34 | /** 35 | * Flag which enables / disables this {@link RateLimiter} implementation. 36 | */ 37 | private volatile boolean enabled = true; 38 | 39 | /** 40 | * The time between each service slot, in seconds. 41 | */ 42 | private int timeToLive = 1; 43 | 44 | /** 45 | * The positive maximum number of requests allowed per duration. 46 | */ 47 | private int allowedRequests = 1; 48 | 49 | /** 50 | * The non-null {@link TokenStore}. 51 | */ 52 | private TokenStore cache; 53 | 54 | /** 55 | * Sets the non-null {@link TokenStore} implementation used. 56 | * 57 | * @param cache 58 | * a non-null {@link TokenStore} 59 | */ 60 | public void setTokenStore(TokenStore cache) { 61 | this.cache = cache; 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public int getAllowedRequests() { 68 | return this.allowedRequests; 69 | } 70 | 71 | /** 72 | * {@inheritDoc} 73 | */ 74 | public void setAllowedRequests(int allowedRequests) { 75 | if (allowedRequests > 0) { 76 | this.allowedRequests = allowedRequests; 77 | } 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | public Token getToken(Key key) { 84 | Token result = TokenInstance.UNUSABLE; 85 | 86 | if (!enabled) { 87 | result = TokenInstance.USABLE; 88 | } else { 89 | 90 | StoreEntry entry = cache.get(key); 91 | 92 | if (entry == null) { 93 | 94 | /* Populate the entry, thus unlocking any underlying mutex */ 95 | entry = cache.create(key, timeToLive); 96 | } 97 | 98 | /* Increment the client count and see whether we have hit the maximum allowed clients yet. */ 99 | int current = entry.incrementAndGet(); 100 | 101 | if (current <= allowedRequests) { 102 | result = TokenInstance.USABLE; 103 | } 104 | } 105 | 106 | return result; 107 | } 108 | 109 | /** 110 | * Initializes the {@link FixedBucket} for use. 111 | */ 112 | public void init() { 113 | 114 | } 115 | 116 | /** 117 | * {@inheritDoc} 118 | */ 119 | public boolean isEnabled() { 120 | return enabled; 121 | } 122 | 123 | /** 124 | * {@inheritDoc} 125 | */ 126 | public void setEnabled(boolean enabled) { 127 | this.enabled = enabled; 128 | } 129 | 130 | /** 131 | * {@inheritDoc} 132 | */ 133 | public void setDuration(int durationInSeconds) { 134 | if (durationInSeconds > 0) { 135 | this.timeToLive = durationInSeconds; 136 | } 137 | } 138 | 139 | /** 140 | * {@inheritDoc} 141 | */ 142 | public int getDuration() { 143 | return this.timeToLive; 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/Key.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | /** 19 | * Marker interface for objects passed into the {@link RateLimiter#getToken(Key)} method. Implementations should ensure 20 | * that good implementations of {@link #equals(Object)} and {@link #hashCode()} are provided, to ensure that the 21 | * {@link RateLimiter} implementations can portion out {@link Token}s based on differentiating the {@link Key} 22 | * instances. 23 | * 24 | * @author jabley 25 | * 26 | */ 27 | public interface Key { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/MemoryTokenStore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | import java.util.Map; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.concurrent.locks.Lock; 21 | import java.util.concurrent.locks.ReadWriteLock; 22 | import java.util.concurrent.locks.ReentrantReadWriteLock; 23 | import com.google.common.collect.MapMaker; 24 | 25 | /** 26 | * {@link TokenStore} implementation that is purely in-memory. 27 | * 28 | * @author jabley 29 | * 30 | */ 31 | public class MemoryTokenStore implements TokenStore { 32 | 33 | /** 34 | * The Map used to keep track of {@link StoreEntry} instances. 35 | */ 36 | private final Map cache; 37 | 38 | /** 39 | * The {@link Lock} used to guard reads. 40 | */ 41 | private final Lock r; 42 | 43 | /** 44 | * The {@link Lock} used to guard writes. 45 | */ 46 | private final Lock w; 47 | 48 | /** 49 | * Creates a new {@link MemoryTokenStore}. 50 | */ 51 | public MemoryTokenStore() { 52 | this.cache = new MapMaker().softValues().expiration(120, TimeUnit.SECONDS).makeMap(); 53 | ReadWriteLock lock = new ReentrantReadWriteLock(); 54 | this.r = lock.readLock(); 55 | this.w = lock.writeLock(); 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | public StoreEntry get(Key key) { 62 | 63 | StoreEntry result; 64 | r.lock(); 65 | 66 | try { 67 | result = this.cache.get(key); 68 | } finally { 69 | r.unlock(); 70 | } 71 | 72 | if (!(result == null || result.isExpired())) { 73 | 74 | /* Cache hit with a good entry - use it. */ 75 | return result; 76 | } 77 | 78 | w.lock(); 79 | 80 | result = checkPopulateThisPeriod(key); 81 | 82 | return result; 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | public StoreEntry create(Key key, int timeToLive) { 89 | try { 90 | StoreEntryImpl entry = new StoreEntryImpl(timeToLive); 91 | cache.put(key, entry); 92 | return entry; 93 | } finally { 94 | w.unlock(); 95 | } 96 | } 97 | 98 | /** 99 | * If no usable entry in the cache, then we assume that the write lock is held prior to calling this method. 100 | * 101 | * Returns null to indicate that the context client thread is safe to call {@link #create(Key, int)}, otherwise 102 | * returns a usable {@link StoreEntry}. 103 | * 104 | * @param key 105 | * the non-null {@link Key} 106 | * @return a {@link StoreEntry} - may be null 107 | */ 108 | private StoreEntry checkPopulateThisPeriod(Key key) { 109 | 110 | /* Check the cache again in case it got updated by a different thread. */ 111 | StoreEntry result = this.cache.get(key); 112 | 113 | if (result == null) { 114 | 115 | /* Keep the write lock and expect that the client will call create(Key, int) very soon. */ 116 | } else if (result.isExpired()) { 117 | 118 | /* 119 | * Remove the expired lock and signal to the client that they are the first one in the new period. Keep the 120 | * write lock in the expectation that the client will call create(Key, int), 121 | */ 122 | cache.remove(key); 123 | result = null; 124 | } else { 125 | 126 | /* 127 | * A different thread won and populated it already. Release the write lock and return the good non-null 128 | * result. 129 | */ 130 | w.unlock(); 131 | } 132 | 133 | return result; 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/RateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | /** 19 | * Interface defining how clients can check to see whether they should proceed before doing a request. 20 | * 21 | * @author jabley 22 | * 23 | */ 24 | public interface RateLimiter extends Enablable { 25 | 26 | /** 27 | * Method called by clients to check whether they should service the current request or not. Returns a non-null 28 | * {@link Token} which clients can then call {@link Token#isUsable()} to determine whether to proceed or not. 29 | * 30 | * @param key 31 | * the {@link Key}, which should have a good implementation of {@link #equals(Object)} and 32 | * {@link #hashCode()} to ensure that types of request can be differentiated. 33 | * @return a non-null {@link Token} 34 | */ 35 | Token getToken(Key key); 36 | 37 | /** 38 | * Returns the positive number of allowed requests per service slot duration. 39 | * 40 | * @return the allowed requests value 41 | */ 42 | public int getAllowedRequests(); 43 | 44 | /** 45 | * Sets the positive number of allowed requests per service slot duration. 46 | */ 47 | public void setAllowedRequests(int allowedRequests); 48 | 49 | /** 50 | * Returns the duration for each service slot in seconds. 51 | * 52 | * @param durationInSeconds 53 | * the positive duration 54 | */ 55 | public void setDuration(int durationInSeconds); 56 | 57 | /** 58 | * Returns the duration in seconds for each service slot. 59 | * 60 | * @return the positive number of seconds 61 | */ 62 | public int getDuration(); 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/StoreEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | public interface StoreEntry { 19 | 20 | int incrementAndGet(); 21 | 22 | boolean isExpired(); 23 | } -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/StoreEntryImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | 20 | /** 21 | * {@link StoreEntry} implementation. 22 | * 23 | * @author jabley 24 | * 25 | */ 26 | class StoreEntryImpl implements StoreEntry { 27 | 28 | /** 29 | * The expiry time from the epoch. 30 | */ 31 | private final long expiry; 32 | 33 | /** 34 | * The counter used to keep track of how many times the service has been used for the current period. 35 | */ 36 | private final AtomicInteger counter; 37 | 38 | /** 39 | * Creates a new {@link StoreEntryImpl} which will expire in {@code timeToLive} seconds. 40 | * 41 | * @param timeToLive 42 | * the time to live in seconds 43 | */ 44 | StoreEntryImpl(int timeToLive) { 45 | this.expiry = System.currentTimeMillis() + timeToLive * 1000; 46 | this.counter = new AtomicInteger(0); 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public boolean isExpired() { 53 | return System.currentTimeMillis() > expiry; 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public int incrementAndGet() { 60 | return this.counter.incrementAndGet(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/Token.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | /** 19 | * Interface defining an extensible enumeration for return values from {@link RateLimiter#getToken(Key)} 20 | * 21 | * @author jabley 22 | */ 23 | public interface Token { 24 | 25 | /** 26 | * Returns true if this Token means that the client should be safe to proceed, otherwise false. 27 | * 28 | * @return true if the client should proceed, otherwise false 29 | */ 30 | boolean isUsable(); 31 | } -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/TokenInstance.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | /** 19 | * {@link Token} is defined as an interface in case future clients need to extend it. 20 | * 21 | * @author jabley 22 | * 23 | */ 24 | public enum TokenInstance implements Token { 25 | 26 | /** 27 | * The usable instance of {@link Token}. 28 | */ 29 | USABLE() { 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public boolean isUsable() { 35 | return true; 36 | } 37 | }, 38 | 39 | /** 40 | * The unusable instance of {@link Token}. 41 | */ 42 | UNUSABLE() { 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public boolean isUsable() { 48 | return false; 49 | } 50 | }; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/TokenStore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | /** 19 | * Interface defining how {@link StoreEntry}s are managed. 20 | * 21 | * @author jabley 22 | * 23 | */ 24 | public interface TokenStore { 25 | 26 | /** 27 | * Returns a usable {@link StoreEntry} for the given {@link Key}. A value of {@code null} means that there is no 28 | * such {@link StoreEntry} and the calling client MUST call {@link #create(Key, int)} to avoid 29 | * other clients potentially being blocked without any hope of progressing. By usable, it is meant that the non-null 30 | * {@link StoreEntry} has not expired and can be used to determine whether the current client should be allowed to 31 | * proceed with the rate-limited action or not. 32 | * 33 | * @param key 34 | * the non-null {@link Key} 35 | * @return a {@link StoreEntry} or null 36 | */ 37 | StoreEntry get(Key key); 38 | 39 | /** 40 | * Creates a new {@link StoreEntry} 41 | * 42 | * @param key 43 | * the non-null {@link Key} 44 | * @param timeToLiveInSecs 45 | * the positive time-to-live in seconds 46 | * @return a non-null usable {@link StoreEntry} 47 | */ 48 | StoreEntry create(Key key, int timeToLiveInSecs); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/CircuitBreaker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | /** 19 | *

20 | * Interface defining the operations supported by a circuit breaker. Inspired by the Michael T Nygard book Release It!. 21 | *

22 | * 23 | *

24 | * Circuit breakers are intended for use around integration points, such as making HTTP calls to third-party services 25 | * and to allow throttling of access to resources. 26 | *

27 | * 28 | * @author jabley 29 | * 30 | */ 31 | public interface CircuitBreaker { 32 | 33 | /** 34 | * Allows clients to register interest in being notified of {@link CircuitBreaker} state changes. 35 | * 36 | * @param listener 37 | * a non-null {@link CircuitBreakerListener} 38 | */ 39 | public void addListener(CircuitBreakerListener listener); 40 | 41 | /** 42 | * Clients should call this method after doing any potentially problematic Integration Point calls, if there weren't 43 | * any problems. 44 | */ 45 | public void after(); 46 | 47 | /** 48 | * Called to signal to this {@link CircuitBreaker} that it should attempt to reset and see if the guarded operation 49 | * is successful. 50 | */ 51 | public void attemptReset(); 52 | 53 | /** 54 | * Clients should call this method before doing any potentially problematic Integration Point calls. 55 | * 56 | * @throws CircuitBreakerException 57 | * if there was a problem. Implementations should throw {@link CircuitBreakerOpenException} to allow 58 | * clients to differentiate between failure due to {@link CircuitBreaker} state versus an integration 59 | * point failure 60 | */ 61 | public void before() throws CircuitBreakerException; 62 | 63 | /** 64 | * Returns a human-readable representation of the current state of this {@link CircuitBreaker}. 65 | * 66 | * @return a non-null String 67 | */ 68 | public String getCurrentState(); 69 | 70 | /** 71 | * Returns the number of permitted failures until this {@link CircuitBreaker} will trip. 72 | * 73 | * @return a positive value 74 | */ 75 | public int getThreshold(); 76 | 77 | /** 78 | * Returns the time in milliseconds until this {@link CircuitBreaker} will reset. A negative value implies that this 79 | * {@link CircuitBreaker} is closed and working normally. 80 | * 81 | * @return the time in milliseconds 82 | */ 83 | public long getTimeToResetInMillis(); 84 | 85 | /** 86 | * Returns the total number of times that this {@link CircuitBreaker} has been tripped. 87 | * 88 | * @return a non-negative value 89 | */ 90 | public long getTripCount(); 91 | 92 | /** 93 | * Clients should call this method after doing any potentially problematic Integration Point calls, if there was a 94 | * failure. 95 | */ 96 | public void handleFailure(); 97 | 98 | /** 99 | * Called to signal to this {@link CircuitBreaker} that it should reset to fully closed and let the guarded 100 | * operation attempts happen normally. 101 | */ 102 | public void reset(); 103 | 104 | /** 105 | * Called to transition this {@link CircuitBreaker} from closed (operating normally) to open (something failed). 106 | */ 107 | public void tripBreaker(); 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/CircuitBreakerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | /** 19 | * Configuration bean for a {@link CircuitBreaker} that controls the {@link CircuitBreaker} behaviour in terms of how 20 | * sensitive it is to tripping and how long it will stay open, etc. This is designed to be injected by Spring, etc. 21 | * 22 | * @author jabley 23 | * 24 | */ 25 | public class CircuitBreakerConfiguration { 26 | 27 | /* 28 | * An option considered was to implement an escalating timeout, a la TCP/IP. If it fails, wait 1 second, try again. 29 | * If it fails, wait 2 seconds, try again. If it fails, wait 4 seconds, etc. Seemed like needless complexity at the 30 | * time. 31 | */ 32 | 33 | /** 34 | * The number of times that a {@link CircuitBreaker} will fail before it trips. 35 | */ 36 | private int threshold = 3; 37 | 38 | /** 39 | * The time in milliseconds that a {@link CircuitBreaker} will stay open until it attempts a reset. 40 | */ 41 | private int timeout = 10000; 42 | 43 | /** 44 | * Factory Method to return a new {@link CircuitBreaker} ready for use. 45 | * 46 | * @return a non-null {@link CircuitBreaker} 47 | */ 48 | public CircuitBreaker createCircuitBreaker() { 49 | return new CircuitBreakerImpl(threshold, timeout); 50 | } 51 | 52 | /** 53 | * Returns the number of times that a closed {@link CircuitBreaker} can fail before it will trip. 54 | * 55 | * @return the threshold 56 | */ 57 | public int getThreshold() { 58 | return threshold; 59 | } 60 | 61 | /** 62 | * Sets the number of times that a closed {@link CircuitBreaker} can fail before it will trip. 63 | * 64 | * @param threshold 65 | * the threshold to set - positive integer 66 | */ 67 | public void setThreshold(int threshold) { 68 | this.threshold = threshold; 69 | } 70 | 71 | /** 72 | * Returns the time in milliseconds that a {@link CircuitBreaker} will take to try to reset itself. 73 | * 74 | * @return the timeout 75 | */ 76 | public int getTimeoutInMillis() { 77 | return timeout; 78 | } 79 | 80 | /** 81 | * Sets the time that a {@link CircuitBreaker} will be in the open state before attempting to reset itself. 82 | * 83 | * @param timeout 84 | * the timeout to set 85 | */ 86 | public void setTimeoutInMillis(int timeout) { 87 | this.timeout = timeout; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/CircuitBreakerException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | /** 19 | * General {@link CircuitBreaker} exception class. 20 | * 21 | * @author jabley 22 | * 23 | */ 24 | public class CircuitBreakerException extends Exception { 25 | 26 | /** 27 | * Required for serialization. 28 | */ 29 | private static final long serialVersionUID = -2000914426184917307L; 30 | 31 | /** 32 | * Constructs a new exception with null as its detail message. The cause is not initialized, and may 33 | * subsequently be initialized by a call to {@link #initCause}. 34 | */ 35 | public CircuitBreakerException() { 36 | super(); 37 | } 38 | 39 | /** 40 | * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently 41 | * be initialized by a call to {@link #initCause}. 42 | * 43 | * @param message 44 | * the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} 45 | * method. 46 | */ 47 | public CircuitBreakerException(String message) { 48 | super(message); 49 | } 50 | 51 | /** 52 | * Constructs a new exception with the specified detail message and cause. 53 | *

54 | * Note that the detail message associated with cause is not automatically incorporated in this 55 | * exception's detail message. 56 | * 57 | * @param message 58 | * the detail message (which is saved for later retrieval by the {@link #getMessage()} method). 59 | * @param cause 60 | * the cause (which is saved for later retrieval by the {@link #getCause()} method). (A null 61 | * value is permitted, and indicates that the cause is nonexistent or unknown.) 62 | */ 63 | public CircuitBreakerException(String message, Throwable cause) { 64 | super(message, cause); 65 | } 66 | 67 | /** 68 | * Constructs a new exception with the specified cause and a detail message of 69 | * (cause==null ? null : cause.toString()) (which typically contains the class and detail message of 70 | * cause). This constructor is useful for exceptions that are little more than wrappers for other 71 | * throwables (for example, {@link java.security.PrivilegedActionException}). 72 | * 73 | * @param cause 74 | * the cause (which is saved for later retrieval by the {@link #getCause()} method). (A null 75 | * value is permitted, and indicates that the cause is nonexistent or unknown.) 76 | */ 77 | public CircuitBreakerException(Throwable cause) { 78 | super(cause); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/CircuitBreakerImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.concurrent.atomic.AtomicLong; 21 | import java.util.concurrent.atomic.AtomicReference; 22 | 23 | /** 24 | * Default implementation of {@link CircuitBreaker}. 25 | * 26 | * @author jabley 27 | * 28 | */ 29 | public class CircuitBreakerImpl implements CircuitBreaker { 30 | 31 | /** 32 | * The positive number of failed attempts allowed before this {@link CircuitBreaker} will trip. 33 | */ 34 | private final int threshold; 35 | 36 | /** 37 | * The timeout in milliseconds after which the open circuit breaker will attempt to reset. 38 | */ 39 | private final int timeout; 40 | 41 | /** 42 | * Count of the number of times this {@link CircuitBreaker} has been tripped. 43 | */ 44 | private final AtomicLong tripCount; 45 | 46 | /** 47 | * The non-null current {@link CircuitBreakerState}. 48 | */ 49 | private final AtomicReference state; 50 | 51 | /** 52 | * The non-null list of {@link CircuitBreakerListener}s that wish to be notified of state changes. 53 | */ 54 | private final List listeners; 55 | 56 | /** 57 | * Creates a new {@link CircuitBreakerImpl} with the specified threshold and timeout. 58 | * 59 | * @param threshold 60 | * a positive number of failures allowed before this {@link CircuitBreaker} will trip 61 | * @param timeout 62 | * the time in milliseconds needed for this tripped {@link CircuitBreaker} to attempt a reset 63 | */ 64 | public CircuitBreakerImpl(int threshold, int timeout) { 65 | this.threshold = threshold; 66 | this.timeout = timeout; 67 | this.tripCount = new AtomicLong(); 68 | this.listeners = new ArrayList(); 69 | this.state = new AtomicReference(new ClosedState(threshold)); 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | public void addListener(CircuitBreakerListener listener) { 76 | this.listeners.add(listener); 77 | } 78 | 79 | /** 80 | * {@inheritDoc} 81 | */ 82 | public void after() { 83 | getState().after(this); 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | public void attemptReset() { 90 | setState(new HalfOpenState()); 91 | 92 | notifyListeners(Notifications.ATTEMPT_RESET); 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | */ 98 | public void before() throws CircuitBreakerException { 99 | getState().before(this); 100 | } 101 | 102 | /** 103 | * {@inheritDoc} 104 | */ 105 | public void handleFailure() { 106 | getState().handleFailure(this); 107 | } 108 | 109 | /** 110 | * {@inheritDoc} 111 | */ 112 | public String getCurrentState() { 113 | return getState().toString(); 114 | } 115 | 116 | /** 117 | * {@inheritDoc} 118 | */ 119 | public int getThreshold() { 120 | return this.threshold; 121 | } 122 | 123 | /** 124 | * {@inheritDoc} 125 | */ 126 | public long getTimeToResetInMillis() { 127 | return getState().getTimeToReset(); 128 | } 129 | 130 | /** 131 | * {@inheritDoc} 132 | */ 133 | public long getTripCount() { 134 | return this.tripCount.get(); 135 | } 136 | 137 | /** 138 | * {@inheritDoc} 139 | */ 140 | public void tripBreaker() { 141 | tripCount.incrementAndGet(); 142 | setState(new OpenState(this.timeout)); 143 | 144 | notifyListeners(Notifications.TRIPPED); 145 | } 146 | 147 | /** 148 | * {@inheritDoc} 149 | */ 150 | public void reset() { 151 | setState(new ClosedState(threshold)); 152 | 153 | notifyListeners(Notifications.RESET); 154 | } 155 | 156 | /** 157 | * Returns the non-null current state. 158 | * 159 | * @return a non-null {@link CircuitBreakerState} 160 | */ 161 | private CircuitBreakerState getState() { 162 | return this.state.get(); 163 | } 164 | 165 | /** 166 | * Notify {@link CircuitBreakerListener}s with the appropriate {@link Notifications} function. 167 | * 168 | * @param notifications 169 | * a non-null {@link Notifications} 170 | */ 171 | private void notifyListeners(Notifications notifications) { 172 | for (CircuitBreakerListener listener : this.listeners) { 173 | try { 174 | notifications.notifyListener(listener); 175 | } catch (RuntimeException e) { 176 | 177 | /* ignore and carry on processing the others */ 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * Sets the non-null new state. 184 | * 185 | * @param newState 186 | * the non-null new {@link CircuitBreakerState} 187 | */ 188 | private void setState(CircuitBreakerState newState) { 189 | this.state.set(newState); 190 | } 191 | 192 | /** 193 | * Simple interface defining a Functor for notifying listeners. 194 | * 195 | * @author jabley 196 | * 197 | */ 198 | interface NotifyListener { 199 | 200 | /** 201 | * Method called to notify {@link CircuitBreakerListener}s of a state change in this {@link CircuitBreaker}. 202 | * 203 | * @param listener 204 | * a non-null {@link CircuitBreakerListener} 205 | */ 206 | void notifyListener(CircuitBreakerListener listener); 207 | } 208 | 209 | /** 210 | * Enumeration defining the possible notifications that we can pass to {@link CircuitBreakerListener}s. 211 | * 212 | * @author jabley 213 | * 214 | */ 215 | private static enum Notifications implements NotifyListener { 216 | 217 | /** 218 | * {@link NotifyListener} implementation for when {@link CircuitBreaker#attemptReset()} has been called. 219 | */ 220 | ATTEMPT_RESET() { 221 | 222 | /** 223 | * {@inheritDoc} 224 | */ 225 | public void notifyListener(CircuitBreakerListener listener) { 226 | listener.attemptReset(); 227 | } 228 | }, 229 | 230 | /** 231 | * {@link NotifyListener} implementation for when {@link CircuitBreaker#reset()} has been called. 232 | */ 233 | RESET() { 234 | 235 | /** 236 | * {@inheritDoc} 237 | */ 238 | public void notifyListener(CircuitBreakerListener listener) { 239 | listener.reset(); 240 | } 241 | }, 242 | 243 | /** 244 | * {@link NotifyListener} implementation for when {@link CircuitBreaker#tripBreaker()} has been called. 245 | */ 246 | TRIPPED() { 247 | 248 | /** 249 | * {@inheritDoc} 250 | */ 251 | public void notifyListener(CircuitBreakerListener listener) { 252 | listener.tripped(); 253 | } 254 | }; 255 | 256 | /** 257 | * {@inheritDoc} 258 | */ 259 | public abstract void notifyListener(CircuitBreakerListener listener); 260 | 261 | } 262 | 263 | } 264 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/CircuitBreakerListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package com.eternus.ratelimit.circuitbreaker; 5 | 6 | /** 7 | * Simple listener interface to allow clients to register interest in CircuitBreaker state changes. 8 | * 9 | * N.B. implementations of this interface should be thread-safe, since they can potentially get called by 10 | * multiple threads. 11 | * 12 | * @author jabley 13 | * 14 | */ 15 | public interface CircuitBreakerListener { 16 | 17 | /** 18 | * Called when a {@link CircuitBreaker} is attempting to reset. 19 | */ 20 | void attemptReset(); 21 | 22 | /** 23 | * Called when a {@link CircuitBreaker} has been reset. 24 | */ 25 | void reset(); 26 | 27 | /** 28 | * Called when a {@link CircuitBreaker} has been tripped. 29 | */ 30 | void tripped(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/CircuitBreakerOpenException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | /** 19 | * {@link CircuitBreakerException} subclass used to indicate when the {@link CircuitBreaker} is open and unusable. 20 | * 21 | * @author jabley 22 | * 23 | */ 24 | public class CircuitBreakerOpenException extends CircuitBreakerException { 25 | 26 | /** 27 | * Required for serialization. 28 | */ 29 | private static final long serialVersionUID = -4097710178233282483L; 30 | 31 | /** 32 | * Constructs a new exception with null as its detail message. The cause is not initialized, and may 33 | * subsequently be initialized by a call to {@link #initCause}. 34 | */ 35 | public CircuitBreakerOpenException() { 36 | super(); 37 | } 38 | 39 | /** 40 | * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently 41 | * be initialized by a call to {@link #initCause}. 42 | * 43 | * @param message 44 | * the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} 45 | * method. 46 | */ 47 | public CircuitBreakerOpenException(String message) { 48 | super(message); 49 | } 50 | 51 | /** 52 | * Constructs a new exception with the specified detail message and cause. 53 | *

54 | * Note that the detail message associated with cause is not automatically incorporated in this 55 | * exception's detail message. 56 | * 57 | * @param message 58 | * the detail message (which is saved for later retrieval by the {@link #getMessage()} method). 59 | * @param cause 60 | * the cause (which is saved for later retrieval by the {@link #getCause()} method). (A null 61 | * value is permitted, and indicates that the cause is nonexistent or unknown.) 62 | */ 63 | public CircuitBreakerOpenException(String message, Throwable cause) { 64 | super(message, cause); 65 | } 66 | 67 | /** 68 | * Constructs a new exception with the specified cause and a detail message of 69 | * (cause==null ? null : cause.toString()) (which typically contains the class and detail message of 70 | * cause). This constructor is useful for exceptions that are little more than wrappers for other 71 | * throwables (for example, {@link java.security.PrivilegedActionException}). 72 | * 73 | * @param cause 74 | * the cause (which is saved for later retrieval by the {@link #getCause()} method). (A null 75 | * value is permitted, and indicates that the cause is nonexistent or unknown.) 76 | */ 77 | public CircuitBreakerOpenException(Throwable cause) { 78 | super(cause); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/CircuitBreakerState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | /** 19 | * Interface defining the behaviour of the state of a {@link CircuitBreaker}. 20 | * 21 | * @author jabley 22 | * 23 | */ 24 | interface CircuitBreakerState { 25 | 26 | /** 27 | * 28 | * @param circuitBreaker 29 | * the non-null {@link CircuitBreaker} that this {@link CircuitBreakerState} refers to 30 | */ 31 | void after(CircuitBreaker circuitBreaker); 32 | 33 | /** 34 | * 35 | * @param circuitBreaker 36 | * the non-null {@link CircuitBreaker} that this {@link CircuitBreakerState} refers to 37 | * @throws CircuitBreakerException 38 | * if there was a problem 39 | */ 40 | void before(CircuitBreaker circuitBreaker) throws CircuitBreakerException; 41 | 42 | /** 43 | * 44 | * @param circuitBreaker 45 | * the non-null {@link CircuitBreaker} that this {@link CircuitBreakerState} refers to 46 | */ 47 | void handleFailure(CircuitBreaker circuitBreaker); 48 | 49 | /** 50 | * Returns the time to next reset in milliseconds. A negative value means that the state is closed. 51 | * 52 | * @return the time to next reset in milliseconds 53 | */ 54 | long getTimeToReset(); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/ClosedState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | 20 | /** 21 | * {@link CircuitBreakerState} implementation for when a {@link CircuitBreaker} is closed and requests to integration 22 | * points are working normally. 23 | * 24 | * @author jabley 25 | * 26 | */ 27 | class ClosedState implements CircuitBreakerState { 28 | 29 | /** 30 | * The number of failures seen by this {@link CircuitBreaker}. 31 | */ 32 | private final AtomicInteger failureCount; 33 | 34 | /** 35 | * The number of failures permitted for the {@link CircuitBreaker}, after which the {@link CircuitBreaker} will 36 | * trip. 37 | */ 38 | private final int threshold; 39 | 40 | /** 41 | * Creates a new {@link ClosedState}. 42 | * 43 | * @param threshold 44 | * the positive threshold permitted number of failures after which any subsequent failures will cause the 45 | * {@link CircuitBreaker} to trip open 46 | */ 47 | ClosedState(int threshold) { 48 | this.threshold = threshold; 49 | this.failureCount = new AtomicInteger(); 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public void after(CircuitBreaker circuitBreakerImpl) { 56 | this.failureCount.set(0); 57 | } 58 | 59 | /** 60 | * {@inheritDoc} 61 | */ 62 | public void before(CircuitBreaker circuitBreakerImpl) { 63 | // no-op 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public long getTimeToReset() { 70 | return -1; 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public void handleFailure(CircuitBreaker circuitBreakerImpl) { 77 | int count = this.failureCount.incrementAndGet(); 78 | 79 | if (count > threshold) { 80 | circuitBreakerImpl.tripBreaker(); 81 | } 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | @Override 88 | public String toString() { 89 | return "CLOSED"; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/HalfOpenState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | /** 19 | * {@link CircuitBreakerState} implementation for when the {@link CircuitBreaker} is half-open and testing to see if the 20 | * guarded operation / integration point is usable yet. 21 | * 22 | * @author jabley 23 | * 24 | */ 25 | class HalfOpenState implements CircuitBreakerState { 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | public void after(CircuitBreaker circuitBreakerImpl) { 31 | circuitBreakerImpl.reset(); 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public void before(CircuitBreaker circuitBreakerImpl) throws CircuitBreakerException { 38 | // no-op 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public long getTimeToReset() { 45 | return 0; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public void handleFailure(CircuitBreaker circuitBreakerImpl) { 52 | circuitBreakerImpl.tripBreaker(); 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | @Override 59 | public String toString() { 60 | return "HALF_OPEN"; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/OpenState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.circuitbreaker; 17 | 18 | /** 19 | * {@link CircuitBreakerState} implementation for when a {@link CircuitBreaker} is open. 20 | * 21 | * @author jabley 22 | * 23 | */ 24 | class OpenState implements CircuitBreakerState { 25 | 26 | /** 27 | * The time when the {@link CircuitBreaker} was tripped. 28 | */ 29 | private final long tripTime; 30 | 31 | /** 32 | * The time in milliseconds after which the tripped CircuitBreaker can attempt to reset. 33 | */ 34 | private final int timeout; 35 | 36 | /** 37 | * Creates a new {@link OpenState} with the specified timeout in milliseconds. 38 | * 39 | * @param timeout 40 | * the positive time in milliseconds after which the {@link CircuitBreaker} will attempt to reset 41 | */ 42 | OpenState(int timeout) { 43 | this.tripTime = System.currentTimeMillis(); 44 | this.timeout = timeout; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public void after(CircuitBreaker circuitBreakerImpl) { 51 | // no-op 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public void before(CircuitBreaker circuitBreakerImpl) throws CircuitBreakerException { 58 | long now = System.currentTimeMillis(); 59 | long elapsed = now - this.tripTime; 60 | 61 | if (elapsed > this.timeout) { 62 | circuitBreakerImpl.attemptReset(); 63 | } else { 64 | throw new CircuitBreakerOpenException("Open CircuitBreaker not yet ready for use."); 65 | } 66 | } 67 | 68 | /** 69 | * {@inheritDoc} 70 | */ 71 | public long getTimeToReset() { 72 | long now = System.currentTimeMillis(); 73 | long elapsed = now - this.tripTime; 74 | 75 | if (elapsed < this.timeout) { 76 | 77 | /* There is still some time to go. */ 78 | return this.timeout - elapsed; 79 | } 80 | 81 | /* It will reset on the next client attempt. */ 82 | return 0; 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | public void handleFailure(CircuitBreaker circuitBreakerImpl) { 89 | // no-op 90 | } 91 | 92 | /** 93 | * {@inheritDoc} 94 | */ 95 | @Override 96 | public String toString() { 97 | return "OPEN"; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/circuitbreaker/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | /** 17 | * Contains the classes, interfaces and enums used to create a CircuitBreaker around potentially problematic 18 | * Integration Points. See Michael T Nygard's Release It! 19 | */ 20 | package com.eternus.ratelimit.circuitbreaker; 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/jmx/ManagedRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.jmx; 17 | 18 | import javax.management.MBeanNotificationInfo; 19 | import javax.management.Notification; 20 | import javax.management.NotificationBroadcasterSupport; 21 | import javax.management.monitor.MonitorNotification; 22 | 23 | import com.eternus.ratelimit.Key; 24 | import com.eternus.ratelimit.RateLimiter; 25 | import com.eternus.ratelimit.Token; 26 | 27 | /** 28 | * JMX MBean which will send out notifications around a decorated {@link RateLimiter} implementation. 29 | * 30 | * @author jabley 31 | * 32 | */ 33 | public class ManagedRateLimiter extends NotificationBroadcasterSupport implements ManagedRateLimiterMBean { 34 | 35 | /** 36 | * The name of the JXM notification that will be sent for successfully serviced requests. 37 | */ 38 | private static final String JMX_MONITOR_RATE_LIMIT_SERVICE_TYPE = "jmx.monitor.rate-limit.service"; 39 | 40 | /** 41 | * The non-null delegate. 42 | */ 43 | private final RateLimiter delegate; 44 | 45 | /** 46 | * The JMX notification sequence number. 47 | */ 48 | private long sequenceNumber; 49 | 50 | /** 51 | * Creates a new {@link ManagedRateLimiter} which will delegate the implementation to the specified non-null 52 | * {@link RateLimiter}. 53 | * 54 | * @param delegate 55 | * a non-null {@link RateLimiter} around which this MBean will send notifications 56 | */ 57 | public ManagedRateLimiter(RateLimiter delegate) { 58 | if (delegate == null) { 59 | throw new IllegalArgumentException("delegate cannot be null"); 60 | } 61 | this.delegate = delegate; 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | @Override 68 | public MBeanNotificationInfo[] getNotificationInfo() { 69 | String[] types = new String[] { JMX_MONITOR_RATE_LIMIT_SERVICE_TYPE, 70 | MonitorNotification.THRESHOLD_VALUE_EXCEEDED }; 71 | MBeanNotificationInfo info = new MBeanNotificationInfo(types, Notification.class.getName(), 72 | "rate-limited request processed"); 73 | return new MBeanNotificationInfo[] { info }; 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | public Token getToken(Key key) { 80 | Token token = delegate.getToken(key); 81 | 82 | if (token.isUsable()) { 83 | sendNotification(new Notification(JMX_MONITOR_RATE_LIMIT_SERVICE_TYPE, this, getSequenceNumber(), 84 | "allowed request " + key)); 85 | } else { 86 | sendNotification(new Notification(MonitorNotification.THRESHOLD_VALUE_EXCEEDED, this, getSequenceNumber(), 87 | "denied request " + key)); 88 | } 89 | 90 | return token; 91 | } 92 | 93 | /** 94 | * {@inheritDoc} 95 | */ 96 | public boolean isEnabled() { 97 | return this.delegate.isEnabled(); 98 | } 99 | 100 | /** 101 | * {@inheritDoc} 102 | */ 103 | public void setEnabled(boolean enabled) { 104 | this.delegate.setEnabled(enabled); 105 | } 106 | 107 | /** 108 | * {@inheritDoc} 109 | */ 110 | public int getAllowedRequests() { 111 | return delegate.getAllowedRequests(); 112 | } 113 | 114 | /** 115 | * {@inheritDoc} 116 | */ 117 | public int getDuration() { 118 | return delegate.getDuration(); 119 | } 120 | 121 | /** 122 | * {@inheritDoc} 123 | */ 124 | public void setAllowedRequests(int allowedRequests) { 125 | delegate.setAllowedRequests(allowedRequests); 126 | } 127 | 128 | /** 129 | * {@inheritDoc} 130 | */ 131 | public void setDuration(int durationInSeconds) { 132 | delegate.setDuration(durationInSeconds); 133 | } 134 | 135 | /** 136 | * Returns the next sequence number for the JMX notification. 137 | * 138 | * @return the next positive sequence number 139 | */ 140 | private synchronized long getSequenceNumber() { 141 | return ++this.sequenceNumber; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/jmx/ManagedRateLimiterMBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit.jmx; 17 | 18 | import com.eternus.ratelimit.RateLimiter; 19 | 20 | /** 21 | * MBean interface for managing {@link RateLimiter}s. 22 | * 23 | * @author jabley 24 | * 25 | */ 26 | public interface ManagedRateLimiterMBean extends RateLimiter { 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/eternus/ratelimit/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | /** 17 | * Contains the classes, interfaces and enums that provide rate-limiting, or throttling, primitives. 18 | */ 19 | package com.eternus.ratelimit; 20 | 21 | -------------------------------------------------------------------------------- /src/test/java/com/eternus/ratelimit/FixedBucketTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | import static org.junit.Assert.*; 19 | 20 | import java.util.concurrent.CountDownLatch; 21 | import java.util.concurrent.ExecutorService; 22 | import java.util.concurrent.Executors; 23 | 24 | import org.junit.Test; 25 | 26 | /** 27 | * Tests for {@link FixedBucket} which are defined to use a variety of {@link TokenStore} implementations. 28 | * 29 | * @author jabley 30 | * 31 | */ 32 | public abstract class FixedBucketTests { 33 | 34 | @Test 35 | public void sequentialFixedBucketAccess() { 36 | FixedBucket rateLimiter = new FixedBucket(); 37 | int allowedRequests = 1; 38 | rateLimiter.setAllowedRequests(allowedRequests); 39 | rateLimiter.setTokenStore(createTokenStore()); 40 | rateLimiter.init(); 41 | 42 | RateLimiterKey key = new RateLimiterKey(); 43 | 44 | Token token = rateLimiter.getToken(key); 45 | assertTrue("We have a usable token back for the first request", token.isUsable()); 46 | 47 | token = rateLimiter.getToken(key); 48 | 49 | assertFalse("The second token is not usable, since we assume that the two token" 50 | + " accesses take less than a second to perform", token.isUsable()); 51 | } 52 | 53 | @Test 54 | public void canDoReasonableNumberOfTokenChecksPerSecond() throws Exception { 55 | FixedBucket rateLimiter = new FixedBucket(); 56 | int allowedRequests = 50000; 57 | rateLimiter.setAllowedRequests(allowedRequests); 58 | rateLimiter.setTokenStore(createTokenStore()); 59 | rateLimiter.init(); 60 | 61 | RateLimiterKey key = new RateLimiterKey(); 62 | 63 | Token token; 64 | 65 | for (int i = 0, n = allowedRequests; i < n; ++i) { 66 | token = rateLimiter.getToken(key); 67 | assertTrue("We have a usable token back for the first request", token.isUsable()); 68 | } 69 | 70 | token = rateLimiter.getToken(key); 71 | 72 | assertFalse("The current token is not usable, since we assume that the " + allowedRequests + " token" 73 | + " accesses take less than a second to perform", token.isUsable()); 74 | } 75 | 76 | @Test 77 | public void multipleClientsCanAccessWithoutBlocking() throws Exception { 78 | final FixedBucket rateLimiter = new FixedBucket(); 79 | int allowedRequests = 200; 80 | rateLimiter.setAllowedRequests(allowedRequests); 81 | rateLimiter.setTokenStore(createTokenStore()); 82 | rateLimiter.init(); 83 | 84 | final RateLimiterKey key = new RateLimiterKey(); 85 | 86 | int clientCount = allowedRequests; 87 | Runnable[] clients = new Runnable[clientCount]; 88 | final boolean[] isUsable = new boolean[clientCount]; 89 | 90 | final CountDownLatch startGate = new CountDownLatch(1); 91 | 92 | final CountDownLatch endGate = new CountDownLatch(clientCount); 93 | 94 | 95 | for (int i = 0, n = isUsable.length; i < n; ++i) { 96 | final int j = i; 97 | clients[j] = new Runnable() { 98 | 99 | /** 100 | * {@inheritDoc} 101 | */ 102 | public void run() { 103 | try { 104 | startGate.await(); 105 | 106 | isUsable[j] = rateLimiter.getToken(key).isUsable(); 107 | 108 | } catch (InterruptedException e) { 109 | e.printStackTrace(); 110 | } finally { 111 | endGate.countDown(); 112 | } 113 | } 114 | }; 115 | } 116 | 117 | ExecutorService executor = Executors.newFixedThreadPool(clientCount); 118 | 119 | for (Runnable runnable : clients) { 120 | executor.execute(runnable); 121 | } 122 | 123 | startGate.countDown(); 124 | 125 | endGate.await(); 126 | 127 | for (boolean b : isUsable) { 128 | assertTrue("Token was usable", b); 129 | } 130 | } 131 | 132 | @Test 133 | public void expiryOfTokensIsSupported() throws Exception { 134 | FixedBucket rateLimiter = new FixedBucket(); 135 | int allowedRequests = 50; 136 | rateLimiter.setAllowedRequests(allowedRequests); 137 | rateLimiter.setTokenStore(createTokenStore()); 138 | rateLimiter.setDuration(1); 139 | rateLimiter.init(); 140 | 141 | RateLimiterKey key = new RateLimiterKey(); 142 | 143 | Token token = rateLimiter.getToken(key); 144 | assertTrue("We have a usable token back for the first request", token.isUsable()); 145 | 146 | // Allow the token to expire 147 | Thread.sleep(1001); 148 | 149 | token = rateLimiter.getToken(key); 150 | assertTrue("We have a usable token back for the second request", token.isUsable()); 151 | } 152 | 153 | /** 154 | * Factory Method to return a {@link TokenStore} for test usage. 155 | * 156 | * @return a non-null {@link TokenStore} 157 | */ 158 | protected abstract TokenStore createTokenStore(); 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/test/java/com/eternus/ratelimit/RateLimiterKey.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | /** 19 | * Simple {@link Key} implementation used for tests. It relies on the default {@link #equals(Object)} and 20 | * {@link #hashCode()} implementations, which are sufficient for test purposes, since we treat each instance as the same 21 | * {@link Key} per test. 22 | * 23 | * @author jabley 24 | * 25 | */ 26 | class RateLimiterKey implements Key { 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | @Override 32 | public String toString() { 33 | return "test-key-" + System.identityHashCode(this); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/test/java/com/eternus/ratelimit/TestFixedBucketWithCoarseMemoryStore.java: -------------------------------------------------------------------------------- 1 | package com.eternus.ratelimit; 2 | 3 | 4 | public class TestFixedBucketWithCoarseMemoryStore extends FixedBucketTests { 5 | 6 | @Override 7 | protected TokenStore createTokenStore() { 8 | return new CoarseMemoryTokenStore(); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/eternus/ratelimit/TestFixedBucketWithEhcache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | import net.sf.ehcache.Cache; 19 | import net.sf.ehcache.CacheManager; 20 | import net.sf.ehcache.Ehcache; 21 | 22 | import org.junit.After; 23 | import org.junit.Before; 24 | 25 | 26 | public class TestFixedBucketWithEhcache extends FixedBucketTests { 27 | 28 | private CacheManager cacheManager; 29 | 30 | private Ehcache cache; 31 | 32 | @Before 33 | public void setup() { 34 | this.cache = new Cache("test-token-store", 100, false, false, 100, 10); 35 | this.cacheManager = CacheManager.create(); 36 | this.cacheManager.addCache(this.cache); 37 | } 38 | 39 | @After 40 | public void teardown() { 41 | cacheManager.shutdown(); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | @Override 48 | protected TokenStore createTokenStore() { 49 | EhcacheTokenStore tokenStore = new EhcacheTokenStore(); 50 | tokenStore.setCache(this.cache); 51 | return tokenStore; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/eternus/ratelimit/TestFixedBucketWithMemoryStore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 James Abley 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 | package com.eternus.ratelimit; 17 | 18 | 19 | public class TestFixedBucketWithMemoryStore extends FixedBucketTests { 20 | 21 | /** 22 | * {@inheritDoc} 23 | */ 24 | @Override 25 | protected TokenStore createTokenStore() { 26 | return new MemoryTokenStore(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/eternus/ratelimit/circuitbreaker/TestCircuitBreaker.java: -------------------------------------------------------------------------------- 1 | package com.eternus.ratelimit.circuitbreaker; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | 7 | public class TestCircuitBreaker { 8 | 9 | @Test 10 | public void basicUsage() throws Exception { 11 | CircuitBreakerConfiguration config = new CircuitBreakerConfiguration(); 12 | config.setThreshold(1); 13 | config.setTimeoutInMillis(100); 14 | CircuitBreaker circuitBreaker = config.createCircuitBreaker(); 15 | 16 | assertEquals(0L, circuitBreaker.getTripCount()); 17 | assertEquals("CLOSED", circuitBreaker.getCurrentState()); 18 | 19 | Runnable failingOperation = new Runnable() { 20 | 21 | /** 22 | * {@inheritDoc} 23 | */ 24 | public void run() { 25 | doFailingOp(); 26 | } 27 | }; 28 | 29 | tryGuardedOperation(circuitBreaker, failingOperation); 30 | 31 | assertEquals(0L, circuitBreaker.getTripCount()); 32 | assertEquals("CLOSED", circuitBreaker.getCurrentState()); 33 | 34 | tryGuardedOperation(circuitBreaker, failingOperation); 35 | 36 | assertEquals("circuit breaker has been tripped after second failure", 1L, circuitBreaker.getTripCount()); 37 | assertEquals("OPEN", circuitBreaker.getCurrentState()); 38 | } 39 | 40 | @Test 41 | public void willAttemptReset() throws Exception { 42 | CircuitBreakerConfiguration config = new CircuitBreakerConfiguration(); 43 | config.setThreshold(1); 44 | config.setTimeoutInMillis(100); 45 | CircuitBreaker circuitBreaker = config.createCircuitBreaker(); 46 | 47 | assertEquals(0L, circuitBreaker.getTripCount()); 48 | assertEquals("CLOSED", circuitBreaker.getCurrentState()); 49 | 50 | Runnable failingOperation = new Runnable() { 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | public void run() { 56 | doFailingOp(); 57 | } 58 | }; 59 | 60 | tryGuardedOperation(circuitBreaker, failingOperation); 61 | 62 | assertEquals(0L, circuitBreaker.getTripCount()); 63 | assertEquals("CLOSED", circuitBreaker.getCurrentState()); 64 | 65 | tryGuardedOperation(circuitBreaker, failingOperation); 66 | 67 | assertEquals("circuit breaker has been tripped after second failure", 1L, circuitBreaker.getTripCount()); 68 | assertEquals("OPEN", circuitBreaker.getCurrentState()); 69 | 70 | Thread.sleep(100); 71 | 72 | tryGuardedOperation(circuitBreaker, new Runnable() { 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | public void run() { 78 | // no-op - simulate successful operation 79 | } 80 | }); 81 | 82 | assertEquals("Operation was successful and the CircuitBreaker is now closed again", "CLOSED", circuitBreaker 83 | .getCurrentState()); 84 | } 85 | 86 | @Test 87 | public void canBeResetAndSuccessfullyCarryOn() throws Exception { 88 | CircuitBreakerConfiguration config = new CircuitBreakerConfiguration(); 89 | config.setThreshold(0); 90 | config.setTimeoutInMillis(100); 91 | CircuitBreaker circuitBreaker = config.createCircuitBreaker(); 92 | 93 | assertEquals(0L, circuitBreaker.getTripCount()); 94 | assertEquals("CLOSED", circuitBreaker.getCurrentState()); 95 | 96 | Runnable failingOperation = new Runnable() { 97 | 98 | /** 99 | * {@inheritDoc} 100 | */ 101 | public void run() { 102 | doFailingOp(); 103 | } 104 | }; 105 | 106 | tryGuardedOperation(circuitBreaker, failingOperation); 107 | 108 | assertEquals("circuit breaker has been tripped after first failure", 1L, circuitBreaker.getTripCount()); 109 | assertEquals("OPEN", circuitBreaker.getCurrentState()); 110 | 111 | circuitBreaker.reset(); 112 | 113 | assertEquals("CLOSED", circuitBreaker.getCurrentState()); 114 | 115 | tryGuardedOperation(circuitBreaker, new Runnable() { 116 | 117 | /** 118 | * {@inheritDoc} 119 | */ 120 | public void run() { 121 | // no-op - simulate successful operation 122 | } 123 | }); 124 | 125 | assertEquals("Operation was successful and the CircuitBreaker is now closed again", "CLOSED", circuitBreaker 126 | .getCurrentState()); 127 | 128 | } 129 | 130 | private void tryGuardedOperation(CircuitBreaker circuitBreaker, Runnable operation) { 131 | try { 132 | circuitBreaker.before(); 133 | operation.run(); 134 | circuitBreaker.after(); 135 | } catch (Throwable e) { 136 | circuitBreaker.handleFailure(); 137 | } 138 | } 139 | 140 | private void doFailingOp() { 141 | throw new RuntimeException("Simulate failing operation"); 142 | } 143 | } 144 | --------------------------------------------------------------------------------