├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── OSSMETADATA ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ocelli-core ├── build.gradle └── src │ ├── main │ └── java │ │ └── netflix │ │ └── ocelli │ │ ├── AbstractLoadBalancerEvent.java │ │ ├── CloseableInstance.java │ │ ├── DelayStrategy.java │ │ ├── Host.java │ │ ├── Instance.java │ │ ├── InstanceCollector.java │ │ ├── InstanceEvent.java │ │ ├── InstanceEventListener.java │ │ ├── InstanceManager.java │ │ ├── InstanceQuarantiner.java │ │ ├── InstanceToNotification.java │ │ ├── Instances.java │ │ ├── KeyedInstance.java │ │ ├── LoadBalancer.java │ │ ├── LoadBalancerEvent.java │ │ ├── LoadBalancerEventListener.java │ │ ├── LoadBalancerStrategy.java │ │ ├── SnapshotToInstance.java │ │ ├── functions │ │ ├── Actions.java │ │ ├── Connectors.java │ │ ├── Delays.java │ │ ├── Failures.java │ │ ├── Functions.java │ │ ├── Limiters.java │ │ ├── Metrics.java │ │ ├── Retrys.java │ │ ├── Stopwatches.java │ │ ├── Topologies.java │ │ └── Weightings.java │ │ ├── loadbalancer │ │ ├── ChoiceOfTwoLoadBalancer.java │ │ ├── RandomWeightedLoadBalancer.java │ │ ├── RoundRobinLoadBalancer.java │ │ └── weighting │ │ │ ├── ClientsAndWeights.java │ │ │ ├── EqualWeightStrategy.java │ │ │ ├── InverseMaxWeightingStrategy.java │ │ │ ├── LinearWeightingStrategy.java │ │ │ └── WeightingStrategy.java │ │ ├── retrys │ │ ├── BackupRequestRetryStrategy.java │ │ └── ExponentialBackoff.java │ │ ├── stats │ │ ├── CKMSQuantiles.java │ │ ├── ExponentialAverage.java │ │ ├── Frugal2UQuantiles.java │ │ └── Quantiles.java │ │ ├── topologies │ │ └── RingTopology.java │ │ └── util │ │ ├── AtomicDouble.java │ │ ├── RandomBlockingQueue.java │ │ ├── RpsEstimator.java │ │ ├── RxUtil.java │ │ ├── SingleMetric.java │ │ ├── StateMachine.java │ │ └── Stopwatch.java │ └── test │ ├── java │ └── netflix │ │ └── ocelli │ │ ├── InstanceQuarantinerTest.java │ │ ├── LoadBalancerTest.java │ │ ├── client │ │ ├── Behaviors.java │ │ ├── Connects.java │ │ ├── ManualFailureDetector.java │ │ ├── Operations.java │ │ ├── ResponseObserver.java │ │ ├── TestClient.java │ │ ├── TestClientConnector.java │ │ ├── TestClientConnectorFactory.java │ │ └── TrackingOperation.java │ │ ├── functions │ │ └── GuardsTest.java │ │ ├── loadbalancer │ │ ├── ChoiceOfTwoLoadBalancerTest.java │ │ └── weighting │ │ │ ├── BaseWeightingStrategyTest.java │ │ │ ├── IntClientAndMetrics.java │ │ │ ├── InverseMaxWeightingStrategyTest.java │ │ │ └── LinearWeightingStrategyTest.java │ │ ├── perf │ │ └── PerfTest.java │ │ ├── retry │ │ ├── BackupRequestStrategyTest.java │ │ └── RetryFailedTestRule.java │ │ ├── toplogies │ │ └── TopologiesTest.java │ │ └── util │ │ ├── CountDownAction.java │ │ └── RandomQueueTest.java │ └── resources │ └── log4j.xml ├── ocelli-eureka ├── build.gradle └── src │ ├── main │ └── java │ │ └── netflix │ │ └── ocelli │ │ └── eureka │ │ └── EurekaInterestManager.java │ └── test │ └── java │ └── netflix │ └── ocelli │ └── eureka │ └── EurekaInterestManagerTest.java ├── ocelli-eureka2 ├── build.gradle └── src │ ├── main │ └── java │ │ └── netflix │ │ └── ocelli │ │ └── eureka2 │ │ └── Eureka2InterestManager.java │ └── test │ └── java │ └── netflix │ └── ocelli │ └── eureka2 │ └── Eureka2InterestManagerTest.java ├── ocelli-examples ├── build.gradle └── src │ └── main │ ├── java │ └── netflix │ │ └── ocelli │ │ └── examples │ │ └── rxnetty │ │ └── http │ │ ├── ChoiceOfTwo.java │ │ ├── HttpExampleUtils.java │ │ ├── RandomWeighted.java │ │ └── RoundRobin.java │ └── resources │ └── log4j.properties ├── ocelli-rxnetty ├── build.gradle └── src │ ├── main │ └── java │ │ └── netflix │ │ └── ocelli │ │ └── rxnetty │ │ ├── FailureListener.java │ │ ├── internal │ │ ├── AbstractLoadBalancer.java │ │ ├── HostCollector.java │ │ ├── HostConnectionProvider.java │ │ └── HostHolder.java │ │ └── protocol │ │ ├── WeightAware.java │ │ ├── WeightComparator.java │ │ ├── http │ │ ├── HttpLoadBalancer.java │ │ └── WeightedHttpClientListener.java │ │ └── tcp │ │ ├── ComparableTcpClientListener.java │ │ ├── TcpLoadBalancer.java │ │ └── WeightedTcpClientListener.java │ └── test │ ├── conf │ └── log4j.xml │ └── java │ └── netflix │ └── ocelli │ └── rxnetty │ └── internal │ ├── AbstractLoadBalancerTest.java │ ├── LoadBalancerRule.java │ └── LoadBalancingProviderTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | #*.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.data 27 | *.redo 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store* 32 | ehthumbs.db 33 | Icon? 34 | Thumbs.db 35 | 36 | # Editor Files # 37 | ################ 38 | *~ 39 | *.swp 40 | 41 | # Gradle Files # 42 | ################ 43 | .gradle 44 | 45 | # Build output directories # 46 | ############################ 47 | /target 48 | */target 49 | /build 50 | */build 51 | /bin 52 | */bin 53 | 54 | # IntelliJ specific files/directories # 55 | ####################################### 56 | out 57 | .idea 58 | *.ipr 59 | *.iws 60 | *.iml 61 | atlassian-ide-plugin.xml 62 | 63 | # Eclipse specific files/directories # 64 | ###################################### 65 | .classpath 66 | .project 67 | .settings 68 | .metadata 69 | 70 | # NetBeans specific files/directories # 71 | ####################################### 72 | .nbattrs 73 | 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix/ocelli/071b370bab981d429ff0718ebdb7dadb0b4a7df1/CHANGELOG.md -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=archived 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is Ocelli? 2 | 3 | Ocelli is a client side reactive load balancer based on RxJava. 4 | 5 | ```java 6 | Observable loadBalancer = ...; 7 | 8 | loadBalancer 9 | .concatMap((client) -> client.doSomething()) 10 | .retry(2) 11 | .subscribe(); 12 | ``` 13 | 14 | 15 | ## Why Ocelli? 16 | 17 | In biology Ocelli is a simple eye as opposed to a complex eye. Ocelli the library is a simple load balancer implementation as opposed to very complex load balancers that do too many via magic or are too tightly coupled with the client transport. Don't know how to pronounce Ocelli? Goto http://www.merriam-webster.com/audio.php?file=ocellu02&word=ocelli 18 | 19 | ## How is Ocelli Reactive? 20 | 21 | Ocelli exposes a very simple API to choose a client. A new client is emitted for each subscription to the return Observable based on changes within the load balancer and optimization based on the load balancing algorithm. Ocelli reacts to changes in the network topology either via server membership events or server failure detection. 22 | 23 | ## How is Ocelli Functional? 24 | 25 | Ocelli is policy driven by delegating key strategies and metric sources via simple functions. This approach simplifies the load balancer implementation so that it is decoupled from the speicific client SPI. These functions are essentially the glue between the load balancer algorithm and the client SPI. 26 | 27 | ## Key design principles 28 | 29 | ### Metrics 30 | Ocelli doesn't attempt to track metrics for client libraries. That role is delegated to the client libraries since most libraries already perform that task. Ocelli simply provides hooks (via functions) for libraries to provide the metric value. 31 | 32 | ### Retry 33 | Retry friendly, not retry magic! Code that performs operations in multiple layers is easily susceptible to retry storms where each layer is unaware of retries in the next. Ocelli makes retries possible by tracking state so that retries are efficient but does not force any particular retry policy. Instead Ocelli let's the caller specify retry policieis via RxJava's built in retry operations such as retry(), retryWhen() and onErrorResumeNext(). 34 | 35 | ### Composability 36 | Ocelli is composable in that the load balancer can be combined with other functionality such as caching, retry and failover. Ocelli delegates as much functionality to the RxJava operators to give users of the library full control. In most cases specific policies may be specified in just one line of code. 37 | 38 | ### Light configuration 39 | TODO 40 | 41 | ## Key features 42 | 43 | ### Pluggable load balancing strategy 44 | 45 | ### Pluggable metrics gathering 46 | 47 | ### Partitioning 48 | 49 | Services are sometimes partitioned to distribute data, load or functionality. Ocelli provides a partitioning mechanism whereby Servers can be members of 0 to many partitions. The client can then choose to route traffic only to one partition and fall back to other partitions. For example, partitioning can be used to implement a rack aware topology where all requests are first directed at the same rack as the client but can fail over to other racks. 50 | 51 | ### Round robin vs. Weighted Round Robin 52 | 53 | ### Pluggable weighting strategy 54 | 55 | #### Identity 56 | 57 | The weight provided by the function is used as the weight. The highest value translates into a higher weight. 58 | 59 | #### reverseMax 60 | 61 | The weight is calculated as the difference between the max and the value. The lowest value therefore translates into a higher 62 | 63 | ### Testing 64 | 65 | ### Topologies 66 | 67 | Topologies are similar to partitions but deal more with the changing position of the client host within a larger topology. Topologies also don't deal with data or functional aspects of a service. For example, to limit the number of hosts to which a client will connect the entire set of servers (including the client) are arranged into a ring and the client connects only to a subset of hosts within the ring using consistent hashing to ensure even distribution. As servers come in an out of existence Ocelli will adjust its target set of hosts to meet the new topology. 68 | 69 | ### Rate limiter 70 | TODO 71 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Netflix, Inc. 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 | plugins { 18 | id 'nebula.netflixoss' version '2.2.9' 19 | id 'nebula.provided-base' version '2.2.0' 20 | } 21 | 22 | ext.githubProjectName = rootProject.name 23 | 24 | configure(subprojects) { 25 | 26 | apply plugin: 'nebula.netflixoss' 27 | apply plugin: 'java' 28 | apply plugin: 'nebula.provided-base' 29 | 30 | sourceCompatibility = JavaVersion.VERSION_1_7 31 | targetCompatibility = JavaVersion.VERSION_1_7 32 | 33 | repositories { 34 | mavenLocal() 35 | maven { url 'https://oss.jfrog.org/libs-snapshot' } 36 | jcenter() 37 | } 38 | 39 | group = "com.netflix.ocelli" 40 | 41 | dependencies { 42 | 43 | compile 'io.reactivex:rxjava:1.0.+' 44 | compile 'org.slf4j:slf4j-api:1.7.2' 45 | 46 | testCompile 'org.slf4j:slf4j-log4j12:1.7.2' 47 | testCompile 'junit:junit:4.12' 48 | testCompile "org.hamcrest:hamcrest-library:1.3" 49 | testCompile "org.mockito:mockito-core:1.+" 50 | } 51 | 52 | eclipse { 53 | classpath { 54 | downloadSources = true 55 | downloadJavadoc = true 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | release.scope=patch 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix/ocelli/071b370bab981d429ff0718ebdb7dadb0b4a7df1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 28 12:24:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /ocelli-core/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Netflix, Inc. 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 | dependencies { 18 | testCompile 'com.google.guava:guava:14.0.1' 19 | } 20 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/AbstractLoadBalancerEvent.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | @SuppressWarnings("rawtypes") 4 | public class AbstractLoadBalancerEvent implements LoadBalancerEvent { 5 | 6 | protected final T name; 7 | protected final boolean isTimed; 8 | protected final boolean isError; 9 | 10 | protected AbstractLoadBalancerEvent(T name, boolean isTimed, boolean isError) { 11 | this.isTimed = isTimed; 12 | this.name = name; 13 | this.isError = isError; 14 | } 15 | 16 | @Override 17 | public T getType() { 18 | return name; 19 | } 20 | 21 | @Override 22 | public boolean isTimed() { 23 | return isTimed; 24 | } 25 | 26 | @Override 27 | public boolean isError() { 28 | return isError; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) { 34 | return true; 35 | } 36 | if (!(o instanceof AbstractLoadBalancerEvent)) { 37 | return false; 38 | } 39 | 40 | AbstractLoadBalancerEvent that = (AbstractLoadBalancerEvent) o; 41 | 42 | if (isError != that.isError) { 43 | return false; 44 | } 45 | if (isTimed != that.isTimed) { 46 | return false; 47 | } 48 | if (name != that.name) { 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | int result = name.hashCode(); 58 | result = 31 * result + (isTimed ? 1 : 0); 59 | result = 31 * result + (isError ? 1 : 0); 60 | return result; 61 | } 62 | } -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/CloseableInstance.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import rx.Observable; 4 | import rx.functions.Func1; 5 | import rx.subjects.BehaviorSubject; 6 | 7 | /** 8 | * An Instance that can be manually closed to indicate it is no longer 9 | * in existence and should be removed from the connection pool. 10 | * 11 | * @author elandau 12 | * 13 | * @param 14 | */ 15 | public class CloseableInstance extends Instance { 16 | 17 | public static CloseableInstance from(T value) { 18 | return from(value, BehaviorSubject.create()); 19 | } 20 | 21 | public static CloseableInstance from(final T value, final BehaviorSubject lifecycle) { 22 | return new CloseableInstance(value, lifecycle); 23 | } 24 | 25 | public static Func1> toMember() { 26 | return new Func1>() { 27 | @Override 28 | public CloseableInstance call(T t) { 29 | return from(t); 30 | } 31 | }; 32 | } 33 | 34 | private T value; 35 | private BehaviorSubject lifecycle; 36 | 37 | public CloseableInstance(T value, BehaviorSubject lifecycle) { 38 | this.value = value; 39 | this.lifecycle = lifecycle; 40 | } 41 | 42 | public String toString() { 43 | return "CloseableInstance[" + getValue() + "]"; 44 | } 45 | 46 | /** 47 | * onComplete the instance's lifecycle Observable 48 | */ 49 | public void close() { 50 | lifecycle.onCompleted(); 51 | } 52 | 53 | @Override 54 | public Observable getLifecycle() { 55 | return lifecycle; 56 | } 57 | 58 | @Override 59 | public T getValue() { 60 | return value; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/DelayStrategy.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | /** 4 | * Strategy to determine the backoff delay after N consecutive failures. 5 | * 6 | * @author elandau 7 | * 8 | */ 9 | public interface DelayStrategy { 10 | long get(int consecutiveFailures); 11 | } 12 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/Host.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import java.util.Map; 4 | 5 | 6 | /** 7 | * A class for expressing a host. 8 | * 9 | * @author Nitesh Kant 10 | */ 11 | public class Host { 12 | 13 | private String hostName; 14 | private int port; 15 | private Map attributes; 16 | 17 | public Host(String hostName, int port) { 18 | this.hostName = hostName; 19 | this.port = port; 20 | } 21 | 22 | public Host(String hostName, int port, Map attributes) { 23 | this.hostName = hostName; 24 | this.port = port; 25 | this.attributes = attributes; 26 | } 27 | 28 | public String getHostName() { 29 | return hostName; 30 | } 31 | 32 | public int getPort() { 33 | return port; 34 | } 35 | 36 | public String getAttributes(String key, String defaultValue) { 37 | if (attributes != null && attributes.containsKey(key)) { 38 | return attributes.get(key); 39 | } 40 | return defaultValue; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) { 46 | return true; 47 | } 48 | if (!(o instanceof Host)) { 49 | return false; 50 | } 51 | 52 | Host host = (Host) o; 53 | 54 | return port == host.port && !(hostName != null ? !hostName.equals(host.hostName) : host.hostName != null); 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | int result = hostName != null ? hostName.hashCode() : 0; 60 | result = 31 * result + port; 61 | return result; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | StringBuilder sb = new StringBuilder(); 67 | sb.append("Host [") 68 | .append("hostName=").append(hostName) 69 | .append(", port=").append(port); 70 | 71 | if (attributes != null && attributes.isEmpty()) { 72 | sb.append(", attr=").append(attributes); 73 | } 74 | return sb.append("]").toString(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/Instance.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import rx.Observable; 4 | 5 | /** 6 | * An {@link Instance} encapsulates a generic entity as well as its lifecycle. Lifecycle 7 | * is managed as an Observable that onCompletes when the entity is no longer in the 8 | * pool. This technique is also used to introduce topologies and quarantine logic for any 9 | * client type where each incarnation of the entity within the load balancer has its own 10 | * lifecycle. 11 | * 12 | * Instance is used internally in Ocelli and should not be created directly other than 13 | * for implementing specific entity registry solutions, such as Eureka. 14 | * 15 | * @see LoadBalancer 16 | * 17 | * @author elandau 18 | * 19 | * @param 20 | */ 21 | public abstract class Instance { 22 | public static Instance create(final T value, final Observable lifecycle) { 23 | return new Instance() { 24 | @Override 25 | public Observable getLifecycle() { 26 | return lifecycle; 27 | } 28 | 29 | @Override 30 | public T getValue() { 31 | return value; 32 | } 33 | }; 34 | } 35 | 36 | /** 37 | * Return the lifecycle for this object 38 | * @return 39 | */ 40 | public abstract Observable getLifecycle(); 41 | 42 | /** 43 | * Return the instance object which could be an address or an actual client implementation 44 | * @return 45 | */ 46 | public abstract T getValue(); 47 | 48 | public String toString() { 49 | return "Instance[" + getValue() + "]"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/InstanceCollector.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import rx.Observable; 4 | import rx.Observable.Operator; 5 | import rx.Observable.Transformer; 6 | import rx.Subscriber; 7 | import rx.Subscription; 8 | import rx.functions.Action0; 9 | import rx.functions.Action1; 10 | import rx.functions.Func0; 11 | import rx.functions.Func1; 12 | import rx.subscriptions.CompositeSubscription; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Set; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | 21 | /** 22 | * From a list of Instance maintain a List of active Instance. Add when T is up and remove 23 | * when T is either down or Instance failed or completed. 24 | * 25 | * @author elandau 26 | * 27 | * @param 28 | */ 29 | public class InstanceCollector implements Transformer, List> { 30 | 31 | private final Func0> instanceStoreFactory; 32 | 33 | private InstanceCollector(Func0> instanceStoreFactory) { 34 | this.instanceStoreFactory = instanceStoreFactory; 35 | } 36 | 37 | public static InstanceCollector create() { 38 | return create(new Func0>() { 39 | @Override 40 | public Map call() { 41 | return new ConcurrentHashMap(); 42 | } 43 | }); 44 | } 45 | 46 | public static InstanceCollector create(Func0> instanceStoreFactory) { 47 | return new InstanceCollector(instanceStoreFactory); 48 | } 49 | 50 | // TODO: Move this into a utils package 51 | public static Action1> toMap(final Map map, final Func1 keyFunc) { 52 | return new Action1>() { 53 | @Override 54 | public void call(final Instance t1) { 55 | map.put(keyFunc.call(t1.getValue()), t1.getValue()); 56 | t1.getLifecycle().doOnCompleted(new Action0() { 57 | @Override 58 | public void call() { 59 | map.remove(t1.getValue()); 60 | } 61 | }); 62 | } 63 | }; 64 | } 65 | 66 | @Override 67 | public Observable> call(Observable> o) { 68 | return o.lift(new Operator, Instance>() { 69 | @Override 70 | public Subscriber> call(final Subscriber> s) { 71 | final CompositeSubscription cs = new CompositeSubscription(); 72 | final Map instances = instanceStoreFactory.call(); 73 | s.add(cs); 74 | 75 | return new Subscriber>() { 76 | @Override 77 | public void onCompleted() { 78 | s.onCompleted(); 79 | } 80 | 81 | @Override 82 | public void onError(Throwable e) { 83 | s.onError(e); 84 | } 85 | 86 | @Override 87 | public void onNext(final Instance t) { 88 | Subscription sub = t.getLifecycle().doOnCompleted(new Action0() { 89 | @Override 90 | public void call() { 91 | Subscription sub = instances.remove(t.getValue()); 92 | cs.remove(sub); 93 | s.onNext(instances.keySet()); 94 | } 95 | }).subscribe(); 96 | 97 | instances.put(t.getValue(), sub); 98 | 99 | s.onNext(instances.keySet()); 100 | } 101 | }; 102 | } 103 | }) 104 | .map(new Func1, List>() { 105 | @Override 106 | public List call(Set instances) { 107 | ArrayList snapshot = new ArrayList(instances.size()); 108 | snapshot.addAll(instances); 109 | 110 | // Make an immutable copy of the list 111 | return Collections.unmodifiableList(snapshot); 112 | } 113 | }); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/InstanceEvent.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | @SuppressWarnings("rawtypes") 4 | public class InstanceEvent extends AbstractLoadBalancerEvent { 5 | 6 | public enum EventType implements MetricEventType { 7 | 8 | /* Connection specific events. */ 9 | ExecutionSuccess(true, false, Void.class), 10 | ExecutionFailed(true, true, Void.class), 11 | 12 | ; 13 | 14 | private final boolean isTimed; 15 | private final boolean isError; 16 | private final Class optionalDataType; 17 | 18 | EventType(boolean isTimed, boolean isError, Class optionalDataType) { 19 | this.isTimed = isTimed; 20 | this.isError = isError; 21 | this.optionalDataType = optionalDataType; 22 | } 23 | 24 | @Override 25 | public boolean isTimed() { 26 | return isTimed; 27 | } 28 | 29 | @Override 30 | public boolean isError() { 31 | return isError; 32 | } 33 | 34 | @Override 35 | public Class getOptionalDataType() { 36 | return optionalDataType; 37 | } 38 | } 39 | 40 | public static final InstanceEvent EXECUTION_SUCCESS = from(EventType.ExecutionSuccess); 41 | public static final InstanceEvent EXECUTION_FAILED = from(EventType.ExecutionFailed); 42 | 43 | /*Always refer to as constants*/protected InstanceEvent(T name, boolean isTimed, boolean isError) { 44 | super(name, isTimed, isError); 45 | } 46 | 47 | private static InstanceEvent from(EventType type) { 48 | return new InstanceEvent(type, type.isTimed(), type.isError()); 49 | } 50 | } -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/InstanceEventListener.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | public abstract class InstanceEventListener implements LoadBalancerEventListener> { 6 | 7 | @Override 8 | public void onEvent(InstanceEvent event, long duration, TimeUnit timeUnit, Throwable throwable, Object value) { 9 | switch ((InstanceEvent.EventType) event.getType()) { 10 | case ExecutionSuccess: 11 | onExecutionSuccess(duration, timeUnit); 12 | break; 13 | case ExecutionFailed: 14 | onExecutionFailed(duration, timeUnit, throwable); 15 | break; 16 | } 17 | } 18 | 19 | protected void onExecutionFailed(long duration, TimeUnit timeUnit, Throwable throwable) { 20 | // No Op 21 | } 22 | 23 | protected void onExecutionSuccess(long duration, TimeUnit timeUnit) { 24 | // No Op 25 | } 26 | 27 | @Override 28 | public void onCompleted() { 29 | // No Op 30 | } 31 | 32 | @Override 33 | public void onSubscribe() { 34 | // No Op 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/InstanceManager.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import java.util.ArrayList; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import java.util.concurrent.ConcurrentMap; 6 | 7 | import rx.Observable; 8 | import rx.Subscriber; 9 | import rx.subjects.PublishSubject; 10 | 11 | /** 12 | * InstanceSubject can be used as a basic bridge from an add/remove host membership 13 | * paradigm to Ocelli's internal Instance with lifecycle representation of entity 14 | * membership in the load balancer. 15 | * 16 | * @see LoadBalancer 17 | * 18 | * @author elandau 19 | */ 20 | public class InstanceManager extends Observable> { 21 | 22 | private final PublishSubject> subject; 23 | private final ConcurrentMap> instances; 24 | 25 | public static InstanceManager create() { 26 | return new InstanceManager(); 27 | } 28 | 29 | public InstanceManager() { 30 | this(PublishSubject.>create(), new ConcurrentHashMap>()); 31 | } 32 | 33 | private InstanceManager(final PublishSubject> subject, final ConcurrentMap> instances) { 34 | super(new OnSubscribe>() { 35 | @Override 36 | public void call(Subscriber> s) { 37 | // TODO: This is a very naive implementation that may have race conditions 38 | // whereby instances may be dropped 39 | Observable 40 | .from(new ArrayList>(instances.values())) 41 | .concatWith(subject).subscribe(s); 42 | } 43 | }); 44 | this.subject = subject; 45 | this.instances = instances; 46 | } 47 | 48 | /** 49 | * Add an entity to the source, which feeds into a load balancer 50 | * @param t 51 | * @return 52 | */ 53 | public CloseableInstance add(T t) { 54 | CloseableInstance member = CloseableInstance.from(t); 55 | CloseableInstance existing = instances.putIfAbsent(t, member); 56 | if (null == existing) { 57 | subject.onNext(member); 58 | return member; 59 | } 60 | return existing; 61 | } 62 | 63 | /** 64 | * Remove an entity from the source. If the entity exists it's lifecycle will 65 | * onComplete. 66 | * 67 | * @param t 68 | * @return 69 | */ 70 | public CloseableInstance remove(T t) { 71 | CloseableInstance member = instances.remove(t); 72 | if (member != null) { 73 | member.close(); 74 | } 75 | return member; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/InstanceToNotification.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import netflix.ocelli.InstanceToNotification.InstanceNotification; 4 | import rx.Notification; 5 | import rx.Observable; 6 | import rx.functions.Func1; 7 | 8 | public class InstanceToNotification implements Func1, Observable>> { 9 | 10 | public static InstanceToNotification create() { 11 | return new InstanceToNotification(); 12 | } 13 | 14 | public enum Kind { 15 | OnAdd, 16 | OnRemove 17 | } 18 | 19 | public static class InstanceNotification { 20 | private final Instance value; 21 | private final Kind kind; 22 | 23 | public InstanceNotification(Instance instance, Kind kind) { 24 | this.value = instance; 25 | this.kind = kind; 26 | } 27 | 28 | public Kind getKind() { 29 | return kind; 30 | } 31 | 32 | public Instance getInstance() { 33 | return value; 34 | } 35 | 36 | public String toString() { 37 | return "Notification[" + value + " " + kind + "]"; 38 | } 39 | } 40 | 41 | @Override 42 | public Observable> call(final Instance instance) { 43 | return Observable 44 | .just(new InstanceNotification(instance, Kind.OnAdd)) 45 | .concatWith(instance.getLifecycle().materialize().map(new Func1, InstanceNotification>() { 46 | @Override 47 | public InstanceNotification call(Notification t1) { 48 | return new InstanceNotification(instance, Kind.OnRemove); 49 | } 50 | })); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/Instances.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import rx.Observable; 4 | import rx.functions.Func1; 5 | 6 | public abstract class Instances { 7 | public static Func1, Instance> transform(final Func1 func) { 8 | return new Func1, Instance>() { 9 | @Override 10 | public Instance call(final Instance primary) { 11 | final S s = func.call(primary.getValue()); 12 | return new Instance() { 13 | @Override 14 | public Observable getLifecycle() { 15 | return primary.getLifecycle(); 16 | } 17 | 18 | @Override 19 | public S getValue() { 20 | return s; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "Instance[" + primary.getValue() + " -> " + getValue() + "]"; 26 | } 27 | }; 28 | } 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/KeyedInstance.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import rx.Observable; 4 | import rx.Observable.Transformer; 5 | import rx.functions.Func1; 6 | import rx.observables.GroupedObservable; 7 | 8 | public class KeyedInstance { 9 | 10 | private final K key; 11 | private final Instance member; 12 | 13 | KeyedInstance(K key, Instance member) { 14 | this.key = key; 15 | this.member = member; 16 | } 17 | 18 | /** 19 | * Partition Members into multiple partitions based on a partition function. 20 | * It's possible for a member to exist in multiple partitions. Each partition 21 | * is a GroupedObservable of members for that partition alone. This stream can 22 | * be fed into a load balancer. 23 | * 24 | * Partitions are useful in the following use cases. 25 | * 26 | * 1. Hosts are grouped into VIPs where each VIP subset can service a certain subset of 27 | * requests. In the example below API's provided by vip1 can be serviced by Hosts 1,2,3 28 | * whereas API's provied by vip2 can only be serviced by hosts 2 and 3. 29 | * 30 | * VIP : F(Host) -> O(vip) Multiple vips 31 | * 32 | * Host1, Host2, Host3 33 | * Host2, Host3 34 | * 35 | * 2. Shard or hash aware clients using consistent hashing (ex. Cassandra) or sharding (ex. EvCache) 36 | * will opt to send traffic only to nodes that can own the data. The partitioner function 37 | * will return the tokenRangeId or shardId for each host. Note that for replication factor of 1 38 | * each shard will contain only 1 host while for higher replication factors each shard will contain 39 | * multiple hosts (equal to the number of shards) and that these hosts will overlap. 40 | * 41 | * Host1, Host2 42 | * Host2, Host3 43 | * Host3, Host4 44 | * Host4, Host5 45 | * 46 | * @author elandau 47 | * 48 | * @param Client type 49 | * @param The partition key 50 | */ 51 | public static Transformer, GroupedObservable>> partitionBy(final Func1> partitioner) { 52 | return new Transformer, GroupedObservable>>() { 53 | @Override 54 | public Observable>> call(final Observable> o) { 55 | return o 56 | .flatMap(new Func1, Observable>>() { 57 | @Override 58 | public Observable> call(final Instance member) { 59 | return partitioner 60 | .call(member.getValue()) 61 | .map(new Func1>() { 62 | @Override 63 | public KeyedInstance call(K key) { 64 | return new KeyedInstance(key, member); 65 | } 66 | }); 67 | } 68 | }) 69 | .groupBy( 70 | new Func1, K>() { 71 | @Override 72 | public K call(KeyedInstance t1) { 73 | return t1.key; 74 | } 75 | }, 76 | new Func1, Instance>() { 77 | @Override 78 | public Instance call(KeyedInstance t1) { 79 | return t1.member; 80 | } 81 | }); 82 | } 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/LoadBalancerEvent.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | @SuppressWarnings("rawtypes") 4 | public interface LoadBalancerEvent { 5 | 6 | T getType(); 7 | 8 | boolean isTimed(); 9 | 10 | boolean isError(); 11 | 12 | /** 13 | * This interface is a "best-practice" rather than a contract as a more strongly required contract is for the event 14 | * type to be an enum. 15 | */ 16 | interface MetricEventType { 17 | 18 | boolean isTimed(); 19 | 20 | boolean isError(); 21 | 22 | Class getOptionalDataType(); 23 | } 24 | } -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/LoadBalancerEventListener.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | public interface LoadBalancerEventListener > { 6 | 7 | int NO_DURATION = -1; 8 | Object NO_VALUE = null; 9 | Throwable NO_ERROR = null; 10 | TimeUnit NO_TIME_UNIT = null; 11 | 12 | /** 13 | * Event callback for any {@link MetricsEvent}. The parameters passed are all the contextual information possible for 14 | * any event. There presence or absence will depend on the type of event. 15 | * 16 | * @param event Event for which this callback has been invoked. This will never be {@code null} 17 | * @param duration If the passed event is {@link MetricsEvent#isTimed()} then the actual duration, else 18 | * {@link #NO_DURATION} 19 | * @param timeUnit The time unit for the duration, if exists, else {@link #NO_TIME_UNIT} 20 | * @param throwable If the passed event is {@link MetricsEvent#isError()} then the cause of the error, else 21 | * {@link #NO_ERROR} 22 | * @param value If the passed event requires custom object to be passed, then that object, else {@link #NO_VALUE} 23 | */ 24 | void onEvent(E event, long duration, TimeUnit timeUnit, Throwable throwable, Object value); 25 | 26 | /** 27 | * Marks the end of all event callbacks. No methods on this listener will ever be called once this method is called. 28 | */ 29 | void onCompleted(); 30 | 31 | /** 32 | * A callback when this listener is subscribed to a {@link MetricEventsPublisher}. 33 | */ 34 | void onSubscribe(); 35 | } 36 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/LoadBalancerStrategy.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Strategy that when given a list of candidates selects the single best one. 7 | * @author elandau 8 | * 9 | * @param 10 | */ 11 | public interface LoadBalancerStrategy { 12 | T choose(List candidates); 13 | } 14 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/SnapshotToInstance.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import rx.Observable; 4 | import rx.Observable.Transformer; 5 | import rx.functions.Func1; 6 | import rx.functions.Func2; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | 16 | /** 17 | * Utility class to convert a full snapshot of pool members, T, to an Observable> 18 | * by diffing the new list with the last list. Any {@link Instance} that has been removed 19 | * will have it's lifecycle terminated. 20 | * 21 | * @author elandau 22 | * 23 | * @param 24 | */ 25 | public class SnapshotToInstance implements Transformer, Instance> { 26 | 27 | public class State { 28 | final Map> members; 29 | final List> newInstances; 30 | 31 | public State() { 32 | members = new HashMap>(); 33 | newInstances = Collections.emptyList(); 34 | } 35 | 36 | public State(State toCopy) { 37 | members = new HashMap>(toCopy.members); 38 | newInstances = new ArrayList<>(); 39 | } 40 | } 41 | 42 | @Override 43 | public Observable> call(Observable> snapshots) { 44 | return snapshots.scan(new State(), new Func2, State>() { 45 | @Override 46 | public State call(State state, List instances) { 47 | State newState = new State(state); 48 | Set keysToRemove = new HashSet(newState.members.keySet()); 49 | 50 | for (T ii : instances) { 51 | keysToRemove.remove(ii); 52 | if (!newState.members.containsKey(ii)) { 53 | CloseableInstance member = CloseableInstance.from(ii); 54 | newState.members.put(ii, member); 55 | newState.newInstances.add(member); 56 | } 57 | } 58 | 59 | for (T tKey : keysToRemove) { 60 | CloseableInstance removed = newState.members.remove(tKey); 61 | if (null != removed) { 62 | removed.close(); 63 | } 64 | } 65 | 66 | return newState; 67 | } 68 | }).concatMap(new Func1>>() { 69 | @Override 70 | public Observable> call(State state) { 71 | return Observable.from(state.newInstances); 72 | } 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Actions.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean; 4 | 5 | import rx.functions.Action0; 6 | 7 | public abstract class Actions { 8 | public static Action0 once(final Action0 delegate) { 9 | return new Action0() { 10 | private AtomicBoolean called = new AtomicBoolean(false); 11 | @Override 12 | public void call() { 13 | if (called.compareAndSet(false, true)) { 14 | delegate.call(); 15 | } 16 | } 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Connectors.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import rx.Observable; 4 | import rx.functions.Func1; 5 | 6 | public abstract class Connectors { 7 | public static Func1> never() { 8 | return new Func1>() { 9 | @Override 10 | public Observable call(C client) { 11 | return Observable.never(); 12 | } 13 | }; 14 | } 15 | 16 | public static Func1> immediate() { 17 | return new Func1>() { 18 | @Override 19 | public Observable call(C client) { 20 | return Observable.empty(); 21 | } 22 | }; 23 | } 24 | 25 | public static Func1> failure(final Throwable t) { 26 | return new Func1>() { 27 | @Override 28 | public Observable call(C client) { 29 | return Observable.error(t); 30 | } 31 | }; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Delays.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import netflix.ocelli.DelayStrategy; 6 | 7 | public abstract class Delays { 8 | public static DelayStrategy fixed(final long delay, final TimeUnit units) { 9 | return new DelayStrategy() { 10 | @Override 11 | public long get(int count) { 12 | return TimeUnit.MILLISECONDS.convert(delay, units); 13 | } 14 | }; 15 | } 16 | 17 | public static DelayStrategy linear(final long delay, final TimeUnit units) { 18 | return new DelayStrategy() { 19 | @Override 20 | public long get(int count) { 21 | return count * TimeUnit.MILLISECONDS.convert(delay, units); 22 | } 23 | }; 24 | } 25 | 26 | public static DelayStrategy exp(final long step, final TimeUnit units) { 27 | return new DelayStrategy() { 28 | @Override 29 | public long get(int count) { 30 | if (count < 0) 31 | count = 0; 32 | else if (count > 30) 33 | count = 30; 34 | return (1 << count) * TimeUnit.MILLISECONDS.convert(step, units); 35 | } 36 | }; 37 | } 38 | 39 | public static DelayStrategy boundedExp(final long step, final long max, final TimeUnit units) { 40 | return new DelayStrategy() { 41 | @Override 42 | public long get(int count) { 43 | if (count < 0) 44 | count = 0; 45 | else if (count > 30) 46 | count = 30; 47 | long delay = (1 << count) * TimeUnit.MILLISECONDS.convert(step, units); 48 | if (delay > max) { 49 | return max; 50 | } 51 | return delay; 52 | } 53 | }; 54 | } 55 | 56 | public static DelayStrategy immediate() { 57 | return new DelayStrategy() { 58 | @Override 59 | public long get(int t1) { 60 | return 0L; 61 | } 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Failures.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import rx.Observable; 4 | import rx.functions.Func1; 5 | 6 | public abstract class Failures { 7 | public static Func1> never() { 8 | return new Func1>() { 9 | @Override 10 | public Observable call(C client) { 11 | return Observable.never(); 12 | } 13 | }; 14 | } 15 | 16 | public static Func1> always(final Throwable t) { 17 | return new Func1>() { 18 | @Override 19 | public Observable call(C client) { 20 | return Observable.error(t); 21 | } 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Functions.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import rx.functions.Func1; 4 | 5 | public abstract class Functions { 6 | public static Func1 log() { 7 | return new Func1() { 8 | @Override 9 | public Integer call(Integer t1) { 10 | return (int)Math.ceil(Math.log(t1)); 11 | } 12 | }; 13 | } 14 | 15 | public static Func1 memoize(final Integer value) { 16 | return new Func1() { 17 | @Override 18 | public Integer call(Integer t1) { 19 | return Math.min(value, t1); 20 | } 21 | }; 22 | } 23 | 24 | public static Func1 log_log() { 25 | return new Func1() { 26 | @Override 27 | public Integer call(Integer t1) { 28 | return (int)Math.ceil(Math.log(Math.log(t1))); 29 | } 30 | }; 31 | } 32 | 33 | public static Func1 identity() { 34 | return new Func1() { 35 | @Override 36 | public Integer call(Integer t1) { 37 | return t1; 38 | } 39 | }; 40 | } 41 | 42 | public static Func1 sqrt() { 43 | return new Func1() { 44 | @Override 45 | public Integer call(Integer t1) { 46 | return (int)Math.ceil(Math.sqrt((double)t1)); 47 | } 48 | }; 49 | } 50 | 51 | public static Func1 root(final double pow) { 52 | return new Func1() { 53 | @Override 54 | public Integer call(Integer t1) { 55 | return (int)Math.ceil(Math.pow((double)t1, 1/pow)); 56 | } 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Limiters.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import netflix.ocelli.stats.ExponentialAverage; 4 | import rx.functions.Func1; 5 | 6 | public abstract class Limiters { 7 | /** 8 | * Guard against excessive backup requests using exponential moving average 9 | * on a per sample basis which is incremented by 1 for each request and by 0 10 | * for each backup request. A backup request is allowed as long as the average 11 | * is able the expected percentile of primary requests to backup requests. 12 | * 13 | * Note that this implementation favors simplicity over accuracy and has 14 | * many drawbacks. 15 | * 1. Backup requests are per request and not tied to any time window 16 | * 2. I have yet to determine an equation that selects the proper window 17 | * for the requested ratio so that the updated exponential moving average 18 | * allows the ratio number of requests. 19 | * @param ratio 20 | * @param window 21 | * @return 22 | */ 23 | public static Func1 exponential(final double ratio, final int window) { 24 | return new Func1() { 25 | private ExponentialAverage exp = new ExponentialAverage(window, 0); 26 | @Override 27 | public Boolean call(Boolean isPrimary) { 28 | if (isPrimary) { 29 | exp.add(1L); 30 | return true; 31 | } 32 | if (exp.get() > ratio) { 33 | exp.add(0L); 34 | return true; 35 | } 36 | return false; 37 | } 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Metrics.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import rx.functions.Func0; 4 | import netflix.ocelli.stats.CKMSQuantiles; 5 | import netflix.ocelli.stats.Quantiles; 6 | import netflix.ocelli.util.SingleMetric; 7 | 8 | /** 9 | * Utility class for creating common strategies for tracking specific types of metrics 10 | * 11 | * @author elandau 12 | * 13 | */ 14 | public class Metrics { 15 | public static Func0> memoizeFactory(final T value) { 16 | return new Func0>() { 17 | @Override 18 | public SingleMetric call() { 19 | return memoize(value); 20 | } 21 | }; 22 | } 23 | 24 | /** 25 | * Return a predetermine constant value regardless of samples added. 26 | * @param value 27 | * @return 28 | */ 29 | public static SingleMetric memoize(final T value) { 30 | return new SingleMetric() { 31 | @Override 32 | public void add(T sample) { 33 | } 34 | 35 | @Override 36 | public void reset() { 37 | } 38 | 39 | @Override 40 | public T get() { 41 | return value; 42 | } 43 | }; 44 | } 45 | 46 | public static Func0> quantileFactory(final double percentile) { 47 | return new Func0>() { 48 | @Override 49 | public SingleMetric call() { 50 | return quantile(percentile); 51 | } 52 | }; 53 | } 54 | 55 | /** 56 | * Use the default CKMSQuantiles algorithm to track a specific percentile 57 | * @param percentile 58 | * @return 59 | */ 60 | public static SingleMetric quantile(final double percentile) { 61 | return quantile(new CKMSQuantiles(new CKMSQuantiles.Quantile[]{new CKMSQuantiles.Quantile(percentile, 1)}), percentile); 62 | } 63 | 64 | /** 65 | * Use an externally provided Quantiles algorithm to track a single percentile. Note that 66 | * quantiles may be shared and should track homogeneous operations. 67 | * 68 | * @param quantiles 69 | * @param percentile 70 | * @return 71 | */ 72 | public static SingleMetric quantile(final Quantiles quantiles, final double percentile) { 73 | return new SingleMetric() { 74 | @Override 75 | public void add(Long sample) { 76 | quantiles.insert(sample.intValue()); 77 | } 78 | 79 | @Override 80 | public void reset() { 81 | } 82 | 83 | @Override 84 | public Long get() { 85 | return (long)quantiles.get(percentile); 86 | } 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Retrys.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import netflix.ocelli.retrys.ExponentialBackoff; 6 | import rx.Observable; 7 | import rx.functions.Func1; 8 | 9 | public abstract class Retrys { 10 | public static Func1 ALWAYS = new Func1() { 11 | @Override 12 | public Boolean call(Throwable t1) { 13 | return true; 14 | } 15 | }; 16 | 17 | public static Func1 NEVER = new Func1() { 18 | @Override 19 | public Boolean call(Throwable t1) { 20 | return false; 21 | } 22 | }; 23 | 24 | /** 25 | * Exponential backoff 26 | * @param maxRetrys 27 | * @param timeslice 28 | * @param units 29 | * @return 30 | */ 31 | public static Func1, Observable> exponentialBackoff(final int maxRetrys, final long timeslice, final TimeUnit units) { 32 | return new ExponentialBackoff(maxRetrys, timeslice, -1, units, ALWAYS); 33 | } 34 | 35 | /** 36 | * Bounded exponential backoff 37 | * @param maxRetrys 38 | * @param timeslice 39 | * @param units 40 | * @return 41 | */ 42 | public static Func1, Observable> exponentialBackoff(final int maxRetrys, final long timeslice, final TimeUnit units, final long maxDelay) { 43 | return new ExponentialBackoff(maxRetrys, timeslice, maxDelay, units, ALWAYS); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Stopwatches.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import netflix.ocelli.util.Stopwatch; 6 | import rx.functions.Func0; 7 | 8 | /** 9 | * Utility class to create common Stopwatch factories in the form of Func0 10 | * functions 11 | * 12 | * @author elandau 13 | * 14 | */ 15 | public class Stopwatches { 16 | /** 17 | * Stopwatch that calls System.nanoTime() 18 | * 19 | * @return 20 | */ 21 | public static Func0 systemNano() { 22 | return new Func0() { 23 | @Override 24 | public Stopwatch call() { 25 | return new Stopwatch() { 26 | private final long startTime = System.nanoTime(); 27 | 28 | @Override 29 | public long elapsed(TimeUnit units) { 30 | return units.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS); 31 | } 32 | }; 33 | } 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Topologies.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import netflix.ocelli.topologies.RingTopology; 4 | import rx.functions.Func1; 5 | 6 | /** 7 | * Convenience class for creating different topologies that filter clients into 8 | * a specific arrangement that limit the set of clients this instance will communicate 9 | * with. 10 | * 11 | * @author elandau 12 | * 13 | */ 14 | public abstract class Topologies { 15 | 16 | public static > RingTopology ring(K id, Func1 idFunc, Func1 countFunc) { 17 | return new RingTopology(id, idFunc, countFunc); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/functions/Weightings.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import rx.functions.Func1; 4 | import netflix.ocelli.loadbalancer.weighting.EqualWeightStrategy; 5 | import netflix.ocelli.loadbalancer.weighting.InverseMaxWeightingStrategy; 6 | import netflix.ocelli.loadbalancer.weighting.LinearWeightingStrategy; 7 | import netflix.ocelli.loadbalancer.weighting.WeightingStrategy; 8 | 9 | public abstract class Weightings { 10 | /** 11 | * @return Strategy that provides a uniform weight to each client 12 | */ 13 | public static WeightingStrategy uniform() { 14 | return new EqualWeightStrategy(); 15 | } 16 | 17 | /** 18 | * @param func 19 | * @return Strategy that uses the output of the function as the weight 20 | */ 21 | public static WeightingStrategy identity(Func1 func) { 22 | return new LinearWeightingStrategy(func); 23 | } 24 | 25 | /** 26 | * @param func 27 | * @return Strategy that sets the weight to the difference between the max 28 | * value of all clients and the client value. 29 | */ 30 | public static WeightingStrategy inverseMax(Func1 func) { 31 | return new InverseMaxWeightingStrategy(func); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/loadbalancer/ChoiceOfTwoLoadBalancer.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer; 2 | 3 | import netflix.ocelli.LoadBalancerStrategy; 4 | 5 | import java.util.Comparator; 6 | import java.util.List; 7 | import java.util.NoSuchElementException; 8 | import java.util.Random; 9 | 10 | /** 11 | * This selector chooses 2 random hosts and picks the host with the 'best' 12 | * performance where that determination is deferred to a customizable function. 13 | * 14 | * This implementation is based on the paper 'The Power of Two Choices in 15 | * Randomized Load Balancing' http://www.eecs.harvard.edu/~michaelm/postscripts/tpds2001.pdf 16 | * This paper states that selecting the best of 2 random servers results in an 17 | * exponential improvement over selecting a single random node (also includes 18 | * round robin) but that adding a third (or more) servers does not yield a significant 19 | * performance improvement. 20 | * 21 | * @author elandau 22 | * 23 | * @param 24 | */ 25 | public class ChoiceOfTwoLoadBalancer implements LoadBalancerStrategy { 26 | public static ChoiceOfTwoLoadBalancer create(final Comparator func) { 27 | return new ChoiceOfTwoLoadBalancer(func); 28 | } 29 | 30 | private final Comparator func; 31 | private final Random rand = new Random(); 32 | 33 | public ChoiceOfTwoLoadBalancer(final Comparator func2) { 34 | func = func2; 35 | } 36 | 37 | /** 38 | * @throws NoSuchElementException 39 | */ 40 | @Override 41 | public T choose(List candidates) { 42 | if (candidates.isEmpty()) { 43 | throw new NoSuchElementException("No servers available in the load balancer"); 44 | } 45 | else if (candidates.size() == 1) { 46 | return candidates.get(0); 47 | } 48 | else { 49 | int pos = rand.nextInt(candidates.size()); 50 | T first = candidates.get(pos); 51 | T second = candidates.get((rand.nextInt(candidates.size()-1) + pos + 1) % candidates.size()); 52 | 53 | return func.compare(first, second) >= 0 ? first : second; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/loadbalancer/RandomWeightedLoadBalancer.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer; 2 | 3 | import netflix.ocelli.LoadBalancerStrategy; 4 | import netflix.ocelli.loadbalancer.weighting.ClientsAndWeights; 5 | import netflix.ocelli.loadbalancer.weighting.WeightingStrategy; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.NoSuchElementException; 10 | import java.util.Random; 11 | 12 | /** 13 | * Select the next element using a random number. 14 | * 15 | * The weights are sorted such as that each cell in the array represents the 16 | * sum of the previous weights plus its weight. This structure makes it 17 | * possible to do a simple binary search using a random number from 0 to 18 | * total weights. 19 | * 20 | * Runtime complexity is O(log N) 21 | * 22 | * @author elandau 23 | * 24 | */ 25 | public class RandomWeightedLoadBalancer implements LoadBalancerStrategy { 26 | public static RandomWeightedLoadBalancer create(final WeightingStrategy strategy) { 27 | return new RandomWeightedLoadBalancer(strategy); 28 | } 29 | 30 | private final WeightingStrategy strategy; 31 | private final Random rand = new Random(); 32 | 33 | public RandomWeightedLoadBalancer(final WeightingStrategy strategy) { 34 | this.strategy = strategy; 35 | } 36 | 37 | /** 38 | * @throws NoSuchElementException 39 | */ 40 | @Override 41 | public T choose(List local) { 42 | final ClientsAndWeights caw = strategy.call(local); 43 | if (caw.isEmpty()) { 44 | throw new NoSuchElementException("No servers available in the load balancer"); 45 | } 46 | 47 | int total = caw.getTotalWeights(); 48 | if (total == 0) { 49 | return caw.getClient(rand.nextInt(caw.size())); 50 | } 51 | 52 | int pos = Collections.binarySearch(caw.getWeights(), rand.nextInt(total)); 53 | return caw.getClient(pos >= 0? pos+1 : -pos - 1); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/loadbalancer/RoundRobinLoadBalancer.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer; 2 | 3 | import netflix.ocelli.LoadBalancerStrategy; 4 | 5 | import java.util.List; 6 | import java.util.NoSuchElementException; 7 | import java.util.Random; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | /** 11 | * Very simple LoadBlancer that when subscribed to gets an ImmutableList of active clients 12 | * and round robins on the elements in that list 13 | * 14 | * @author elandau 15 | */ 16 | public class RoundRobinLoadBalancer implements LoadBalancerStrategy { 17 | public static RoundRobinLoadBalancer create() { 18 | return create(new Random().nextInt(1000)); 19 | } 20 | 21 | public static RoundRobinLoadBalancer create(int seedPosition) { 22 | return new RoundRobinLoadBalancer(seedPosition); 23 | } 24 | 25 | private final AtomicInteger position; 26 | 27 | public RoundRobinLoadBalancer() { 28 | position = new AtomicInteger(new Random().nextInt(1000)); 29 | } 30 | 31 | public RoundRobinLoadBalancer(int seedPosition) { 32 | position = new AtomicInteger(seedPosition); 33 | } 34 | 35 | /** 36 | * @throws NoSuchElementException 37 | */ 38 | @Override 39 | public T choose(List local) { 40 | if (local.isEmpty()) { 41 | throw new NoSuchElementException("No servers available in the load balancer"); 42 | } 43 | else { 44 | int pos = Math.abs(position.incrementAndGet()); 45 | return local.get(pos % local.size()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/loadbalancer/weighting/ClientsAndWeights.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import java.util.List; 4 | 5 | 6 | public class ClientsAndWeights { 7 | private final List clients; 8 | private final List weights; 9 | 10 | public ClientsAndWeights(List clients, List weights) { 11 | this.clients = clients; 12 | this.weights = weights; 13 | } 14 | 15 | public List getClients() { 16 | return clients; 17 | } 18 | 19 | public List getWeights() { 20 | return weights; 21 | } 22 | 23 | public boolean isEmpty() { 24 | return clients.isEmpty(); 25 | } 26 | 27 | public int size() { 28 | return clients.size(); 29 | } 30 | 31 | public int getTotalWeights() { 32 | if (weights == null || weights.isEmpty()) { 33 | return 0; 34 | } 35 | return weights.get(weights.size() -1); 36 | } 37 | 38 | public C getClient(int index) { 39 | return clients.get(index); 40 | } 41 | 42 | public int getWeight(int index) { 43 | if (weights == null) { 44 | return 0; 45 | } 46 | return weights.get(index); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "ClientsAndWeights [clients=" + clients + ", weights=" + weights + ']'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/loadbalancer/weighting/EqualWeightStrategy.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import java.util.List; 4 | 5 | 6 | /** 7 | * Strategy where all clients have the same weight 8 | * @author elandau 9 | * 10 | * @param 11 | * @param 12 | * @param 13 | */ 14 | public class EqualWeightStrategy implements WeightingStrategy { 15 | 16 | @Override 17 | public ClientsAndWeights call(List clients) { 18 | return new ClientsAndWeights(clients, null); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/loadbalancer/weighting/InverseMaxWeightingStrategy.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import rx.functions.Func1; 7 | 8 | /** 9 | * Weighting strategy that gives an inverse weight to the highest rate. Using 10 | * this strategy higher input values receive smaller weights. 11 | * 12 | * For example, if the weight is based on pending requests then an input of 13 | * [1, 5, 10, 1] pending request counts would yield the following weights 14 | * [10, 6, 1, 10] using the formula : w(i) = max - w(i) + 1 15 | * 16 | * Note that 1 is added to ensure that we don't have a 0 weight, which is invalid. 17 | * 18 | * @author elandau 19 | * 20 | * @param 21 | */ 22 | public class InverseMaxWeightingStrategy implements WeightingStrategy { 23 | 24 | private Func1 func; 25 | 26 | public InverseMaxWeightingStrategy(Func1 func) { 27 | this.func = func; 28 | } 29 | 30 | @Override 31 | public ClientsAndWeights call(List clients) { 32 | ArrayList weights = new ArrayList(clients.size()); 33 | 34 | if (clients.size() > 0) { 35 | Integer max = 0; 36 | for (int i = 0; i < clients.size(); i++) { 37 | int weight = func.call(clients.get(i)); 38 | if (weight > max) { 39 | max = weight; 40 | } 41 | weights.add(i, weight); 42 | } 43 | 44 | int sum = 0; 45 | for (int i = 0; i < weights.size(); i++) { 46 | sum += (max - weights.get(i)) + 1; 47 | weights.set(i, sum); 48 | } 49 | } 50 | return new ClientsAndWeights(clients, weights); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/loadbalancer/weighting/LinearWeightingStrategy.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import rx.functions.Func1; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class LinearWeightingStrategy implements WeightingStrategy { 9 | 10 | private final Func1 func; 11 | 12 | public LinearWeightingStrategy(Func1 func) { 13 | this.func = func; 14 | } 15 | 16 | @Override 17 | public ClientsAndWeights call(List clients) { 18 | ArrayList weights = new ArrayList(clients.size()); 19 | 20 | if (!clients.isEmpty()) { 21 | for (C client : clients) { 22 | weights.add(func.call(client)); 23 | } 24 | 25 | int sum = 0; 26 | for (int i = 0; i < weights.size(); i++) { 27 | sum += weights.get(i); 28 | weights.set(i, sum); 29 | } 30 | } 31 | return new ClientsAndWeights(clients, weights); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/loadbalancer/weighting/WeightingStrategy.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import java.util.List; 4 | 5 | import rx.functions.Func1; 6 | 7 | /** 8 | * Contract for strategy to determine client weights from a list of clients 9 | * 10 | * @author elandau 11 | * 12 | * @param 13 | */ 14 | public interface WeightingStrategy extends Func1, ClientsAndWeights> { 15 | /** 16 | * Run the weighting algorithm on the active set of clients and their associated statistics and 17 | * return an object containing the weights 18 | */ 19 | ClientsAndWeights call(List clients); 20 | } 21 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/retrys/BackupRequestRetryStrategy.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.retrys; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | import netflix.ocelli.functions.Metrics; 7 | import netflix.ocelli.functions.Retrys; 8 | import netflix.ocelli.functions.Stopwatches; 9 | import netflix.ocelli.util.SingleMetric; 10 | import netflix.ocelli.util.Stopwatch; 11 | import rx.Observable; 12 | import rx.Observable.OnSubscribe; 13 | import rx.Observable.Transformer; 14 | import rx.Scheduler; 15 | import rx.Subscriber; 16 | import rx.functions.Action1; 17 | import rx.functions.Func0; 18 | import rx.functions.Func1; 19 | import rx.schedulers.Schedulers; 20 | 21 | /** 22 | * Retry strategy that kicks off a second request if the first request does not 23 | * respond within an expected amount of time. The original request remains in 24 | * flight until either one responds. The strategy tracks response latencies and 25 | * feeds them into a SingleMetric that is used to determine the backup request 26 | * timeout. A common metric to use is the 90th percentile response time. 27 | * 28 | * Note that the same BackupRequestRetryStrategy instance is stateful and should 29 | * be used for all requests. Multiple BackupRequestRetryStrategy instances may be 30 | * used for different request types known to have varying response latency 31 | * distributions. 32 | * 33 | * Usage, 34 | * 35 | * {@code 36 | *
 37 |  * 
 38 |  * BackupRequestRetryStrategy strategy = BackupRequestRetryStrategy.builder()
 39 |  *                  .withTimeoutMetric(Metrics.quantile(0.90))
 40 |  *                  .withIsRetriablePolicy(somePolicyThatReturnsTrueOnRetriableErrors)
 41 |  *                  .build();
 42 |  *                  
 43 |  * loadBalancer
 44 |  *      .flatMap(operation)
 45 |  *      .compose(strategy)
 46 |  *      .subscribe(responseHandler)
 47 |  * 
48 | * code} 49 | * 50 | * @author elandau 51 | * 52 | * @param 53 | */ 54 | public class BackupRequestRetryStrategy implements Transformer { 55 | public static Func0 DEFAULT_CLOCK = Stopwatches.systemNano(); 56 | 57 | private final Func0 sw; 58 | private final SingleMetric metric; 59 | private final Func1 retriableError; 60 | private final Scheduler scheduler; 61 | 62 | public static class Builder { 63 | private Func0 sw = DEFAULT_CLOCK; 64 | private SingleMetric metric = Metrics.memoize(10L); 65 | private Func1 retriableError = Retrys.ALWAYS; 66 | private Scheduler scheduler = Schedulers.computation(); 67 | 68 | /** 69 | * Function to determine if an exception is retriable or not. A non 70 | * retriable exception will result in an immediate error being returned 71 | * while the first retriable exception on either the primary or secondary 72 | * request will be ignored to allow the other request to complete. 73 | * @param retriableError 74 | */ 75 | public Builder withIsRetriablePolicy(Func1 retriableError) { 76 | this.retriableError = retriableError; 77 | return this; 78 | } 79 | 80 | /** 81 | * Function to determine the backup request timeout for each operation. 82 | * @param func 83 | * @param units 84 | */ 85 | public Builder withTimeoutMetric(SingleMetric metric) { 86 | this.metric = metric; 87 | return this; 88 | } 89 | 90 | /** 91 | * Provide an external scheduler to drive the backup timeout. Use this 92 | * to test with a TestScheduler 93 | * 94 | * @param scheduler 95 | */ 96 | public Builder withScheduler(Scheduler scheduler) { 97 | this.scheduler = scheduler; 98 | return this; 99 | } 100 | 101 | /** 102 | * Factory for creating stopwatches. A new stopwatch is created per operation. 103 | * @param clock 104 | */ 105 | public Builder withStopwatch(Func0 sw) { 106 | this.sw = sw; 107 | return this; 108 | } 109 | 110 | public BackupRequestRetryStrategy build() { 111 | return new BackupRequestRetryStrategy(this); 112 | } 113 | } 114 | 115 | public static Builder builder() { 116 | return new Builder(); 117 | } 118 | 119 | private BackupRequestRetryStrategy(Builder builder) { 120 | this.metric = builder.metric; 121 | this.retriableError = builder.retriableError; 122 | this.scheduler = builder.scheduler; 123 | this.sw = builder.sw; 124 | } 125 | 126 | @Override 127 | public Observable call(final Observable o) { 128 | Observable timedO = Observable.create(new OnSubscribe() { 129 | @Override 130 | public void call(Subscriber s) { 131 | final Stopwatch timer = sw.call(); 132 | o.doOnNext(new Action1() { 133 | @Override 134 | public void call(T t1) { 135 | metric.add(timer.elapsed(TimeUnit.MILLISECONDS)); 136 | } 137 | }).subscribe(s); 138 | } 139 | }); 140 | 141 | return Observable 142 | .just(timedO, timedO.delaySubscription(metric.get(), TimeUnit.MILLISECONDS, scheduler)) 143 | .flatMap(new Func1, Observable>() { 144 | final AtomicInteger counter = new AtomicInteger(); 145 | 146 | @Override 147 | public Observable call(Observable t1) { 148 | return t1.onErrorResumeNext(new Func1>() { 149 | @Override 150 | public Observable call(Throwable e) { 151 | if (counter.incrementAndGet() == 2 || !retriableError.call(e)) { 152 | return Observable.error(e); 153 | } 154 | return Observable.never(); 155 | } 156 | }); 157 | } 158 | }) 159 | .take(1); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/retrys/ExponentialBackoff.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.retrys; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import rx.Observable; 7 | import rx.functions.Func1; 8 | 9 | /** 10 | * Func1 to be passed to retryWhen which implements a robust random exponential backoff. The 11 | * random part of the exponential backoff ensures that some randomness is inserted so that multiple 12 | * clients blocked on a non-responsive resource spread out the retries to mitigate a thundering 13 | * herd. 14 | * 15 | * This class maintains retry count state and should be instantiated for entire top level request. 16 | * 17 | * @author elandau 18 | */ 19 | public class ExponentialBackoff implements Func1, Observable> { 20 | private static final int MAX_SHIFT = 30; 21 | 22 | private final int maxRetrys; 23 | private final long maxDelay; 24 | private final long slice; 25 | private final TimeUnit units; 26 | private final Func1 retryable; 27 | 28 | private static final Random rand = new Random(); 29 | 30 | private int tryCount; 31 | 32 | /** 33 | * Construct an exponential backoff 34 | * 35 | * @param maxRetrys Maximum number of retires to attempt 36 | * @param slice - Time interval multiplied by backoff amount 37 | * @param maxDelay - Upper bound allowable backoff delay 38 | * @param units - Time unit for slice and maxDelay 39 | * @param retryable - Function that returns true if the error is retryable or false if not. 40 | */ 41 | public ExponentialBackoff(int maxRetrys, long slice, long maxDelay, TimeUnit units, Func1 retryable) { 42 | this.maxDelay = maxDelay; 43 | this.maxRetrys = maxRetrys; 44 | this.slice = slice; 45 | this.units = units; 46 | this.retryable = retryable; 47 | 48 | this.tryCount = 0; 49 | } 50 | 51 | @Override 52 | public Observable call(Observable error) { 53 | return error.flatMap(new Func1>() { 54 | @Override 55 | public Observable call(Throwable e) { 56 | // First make sure the error is actually retryable 57 | if (!retryable.call(e)) { 58 | return Observable.error(e); 59 | } 60 | 61 | if (tryCount >= maxRetrys) { 62 | return Observable.error(new Exception("Failed with " + tryCount + " retries", e)); 63 | } 64 | 65 | // Calculate the number of slices to wait 66 | int slices = (1 << Math.min(MAX_SHIFT, tryCount)); 67 | slices = (slices + rand.nextInt(slices+1)) / 2; 68 | long delay = slices * slice; 69 | if (maxDelay > 0 && delay > maxDelay) { 70 | delay = maxDelay; 71 | } 72 | tryCount++; 73 | return Observable.timer(delay, units); 74 | } 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/stats/ExponentialAverage.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.stats; 2 | 3 | import netflix.ocelli.util.AtomicDouble; 4 | import netflix.ocelli.util.SingleMetric; 5 | import rx.functions.Func0; 6 | 7 | public class ExponentialAverage implements SingleMetric { 8 | 9 | private final double k; 10 | private final AtomicDouble ema; 11 | private final double initial; 12 | 13 | public static Func0> factory(final int N, final double initial) { 14 | return new Func0>() { 15 | @Override 16 | public SingleMetric call() { 17 | return new ExponentialAverage(N, initial); 18 | } 19 | }; 20 | } 21 | 22 | public ExponentialAverage(int N, double initial) { 23 | this.initial = initial; 24 | this.k = 2.0/(double)(N+1); 25 | this.ema = new AtomicDouble(initial); 26 | } 27 | 28 | @Override 29 | public void add(Long sample) { 30 | double next; 31 | double current; 32 | do { 33 | current = ema.get(); 34 | next = sample * k + current * (1-k); 35 | } while(!ema.compareAndSet(current, next)); 36 | } 37 | 38 | @Override 39 | public Long get() { 40 | return (long)ema.get(); 41 | } 42 | 43 | @Override 44 | public void reset() { 45 | ema.set(initial); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/stats/Frugal2UQuantiles.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.stats; 2 | 3 | import java.util.Random; 4 | 5 | /** 6 | * Implementation of the Frugal2U Algorithm. 7 | * 8 | * Reference: 9 | * Ma, Qiang, S. Muthukrishnan, and Mark Sandler. "Frugal Streaming for 10 | * Estimating Quantiles." Space-Efficient Data Structures, Streams, and 11 | * Algorithms. Springer Berlin Heidelberg, 2013. 77-96. 12 | * 13 | * Original code: 14 | * More info: http://blog.aggregateknowledge.com/2013/09/16/sketch-of-the-day-frugal-streaming/ 15 | * 16 | * @author Maycon Viana Bordin 17 | */ 18 | public class Frugal2UQuantiles implements Quantiles { 19 | 20 | private final Quantile quantiles[]; 21 | 22 | public Frugal2UQuantiles(Quantile[] quantiles) { 23 | this.quantiles = quantiles; 24 | } 25 | 26 | public Frugal2UQuantiles(double[] quantiles, int initialEstimate) { 27 | this.quantiles = new Quantile[quantiles.length]; 28 | for (int i=0; i m && r.nextDouble() > 1-q) { 69 | step += sign * f(step); 70 | 71 | if (step > 0) { 72 | m += step; 73 | } else { 74 | m += 1; 75 | } 76 | 77 | if (m > s) { 78 | step += (s - m); 79 | m = s; 80 | } 81 | 82 | if (sign < 0) { 83 | step = 1; 84 | } 85 | 86 | sign = 1; 87 | } else if (s < m && r.nextDouble() > q) { 88 | step += -sign * f(step); 89 | 90 | if (step > 0) { 91 | m -= step; 92 | } else { 93 | m--; 94 | } 95 | 96 | if (m < s) { 97 | step += (m - s); 98 | m = s; 99 | } 100 | 101 | if (sign > 0) { 102 | step = 1; 103 | } 104 | 105 | sign = -1; 106 | } 107 | } 108 | 109 | int f(int step) { 110 | return 1; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/stats/Quantiles.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.stats; 2 | 3 | /** 4 | * Contract for tracking percentiles in a dataset 5 | * 6 | * @author elandau 7 | */ 8 | public interface Quantiles { 9 | /** 10 | * Add a sample 11 | * @param value 12 | */ 13 | public void insert(int value); 14 | 15 | /** 16 | * @param get (0 .. 1.0) 17 | * @return Get the Nth percentile 18 | */ 19 | public int get(double percentile); 20 | } 21 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/util/AtomicDouble.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.util; 2 | 3 | import java.util.concurrent.atomic.AtomicLong; 4 | 5 | /** 6 | * Utility class to track an atomic double for use in non blocking algorithms (@see ExponentialAverage) 7 | * 8 | * @author elandau 9 | */ 10 | public class AtomicDouble { 11 | 12 | private AtomicLong bits; 13 | 14 | public AtomicDouble() { 15 | this(0f); 16 | } 17 | 18 | public AtomicDouble(double initialValue) { 19 | bits = new AtomicLong(Double.doubleToLongBits(initialValue)); 20 | } 21 | 22 | public final boolean compareAndSet(double expect, double update) { 23 | return bits.compareAndSet(Double.doubleToLongBits(expect), 24 | Double.doubleToLongBits(update)); 25 | } 26 | 27 | public final void set(double newValue) { 28 | bits.set(Double.doubleToLongBits(newValue)); 29 | } 30 | 31 | public final double get() { 32 | return Double.longBitsToDouble(bits.get()); 33 | } 34 | 35 | public final double getAndSet(double newValue) { 36 | return Double.longBitsToDouble(bits.getAndSet(Double.doubleToLongBits(newValue))); 37 | } 38 | 39 | public final boolean weakCompareAndSet(double expect, double update) { 40 | return bits.weakCompareAndSet(Double.doubleToLongBits(expect), 41 | Double.doubleToLongBits(update)); 42 | } 43 | 44 | public double doubleValue() { return (double) get(); } 45 | public int intValue() { return (int) get(); } 46 | public long longValue() { return (long) get(); } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/util/RpsEstimator.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.util; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | 6 | public class RpsEstimator { 7 | private static final double MICROS_PER_SECOND = TimeUnit.MICROSECONDS.convert(1, TimeUnit.SECONDS); 8 | 9 | private AtomicLong sampleCounter = new AtomicLong(); 10 | private AtomicLong lastCheckpoint = new AtomicLong(); 11 | 12 | private volatile long estimatedRps; 13 | private volatile long nextCheckpoint; 14 | private volatile long lastFlushTime = System.nanoTime(); 15 | 16 | public static class State { 17 | long count; 18 | long rps; 19 | } 20 | 21 | public RpsEstimator(long initialRps) { 22 | this.estimatedRps = 0; 23 | this.nextCheckpoint = 1000; 24 | } 25 | 26 | public State addSample() { 27 | long counter = sampleCounter.incrementAndGet(); 28 | if (counter - lastCheckpoint.get() == nextCheckpoint) { 29 | long count = counter - lastCheckpoint.get(); 30 | synchronized (this) { 31 | lastCheckpoint.set(counter); 32 | long now = System.nanoTime(); 33 | estimatedRps = (long) (count * MICROS_PER_SECOND / (lastFlushTime - now)); 34 | lastFlushTime = now; 35 | nextCheckpoint = estimatedRps; 36 | 37 | State state = new State(); 38 | state.count = count; 39 | state.rps = estimatedRps; 40 | return state; 41 | } 42 | } 43 | else if (counter - lastCheckpoint.get() > 2 * estimatedRps) { 44 | nextCheckpoint = (counter - lastCheckpoint.get()) * 2; 45 | } 46 | 47 | return null; 48 | } 49 | 50 | long getSampleCount() { 51 | return sampleCounter.get(); 52 | } 53 | 54 | long getRps() { 55 | return estimatedRps; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/util/SingleMetric.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.util; 2 | 3 | /** 4 | * Contract for tracking a single Metric. For example, a SingleMetric may track an exponential moving 5 | * average where add() is called for each new sample and get() is called to get the current 6 | * exponential moving average 7 | * 8 | * @author elandau 9 | * 10 | * @param 11 | */ 12 | public interface SingleMetric { 13 | /** 14 | * Add a new sample 15 | * @param sample 16 | */ 17 | void add(T sample); 18 | 19 | /** 20 | * Reset the value to default 21 | */ 22 | void reset(); 23 | 24 | /** 25 | * @return The latest calculated value 26 | */ 27 | T get(); 28 | } 29 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/util/StateMachine.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.util; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import rx.Observable; 12 | import rx.Observable.OnSubscribe; 13 | import rx.Subscriber; 14 | import rx.functions.Action1; 15 | import rx.functions.Action2; 16 | import rx.functions.Func0; 17 | import rx.functions.Func1; 18 | import rx.subjects.PublishSubject; 19 | 20 | public class StateMachine implements Action1 { 21 | private static final Logger LOG = LoggerFactory.getLogger(StateMachine.class); 22 | 23 | public static class State { 24 | private String name; 25 | private Func1> enter; 26 | private Func1> exit; 27 | private Map> transitions = new HashMap>(); 28 | private Set ignore = new HashSet(); 29 | 30 | public static State create(String name) { 31 | return new State(name); 32 | } 33 | 34 | public State(String name) { 35 | this.name = name; 36 | } 37 | 38 | public State onEnter(Func1> func) { 39 | this.enter = func; 40 | return this; 41 | } 42 | 43 | public State onExit(Func1> func) { 44 | this.exit = func; 45 | return this; 46 | } 47 | 48 | public State transition(E event, State state) { 49 | transitions.put(event, state); 50 | return this; 51 | } 52 | 53 | public State ignore(E event) { 54 | ignore.add(event); 55 | return this; 56 | } 57 | 58 | Observable enter(T context) { 59 | if (enter != null) 60 | return enter.call(context); 61 | return Observable.empty(); 62 | } 63 | 64 | Observable exit(T context) { 65 | if (exit != null) 66 | exit.call(context); 67 | return Observable.empty(); 68 | } 69 | 70 | State next(E event) { 71 | return transitions.get(event); 72 | } 73 | 74 | public String toString() { 75 | return name; 76 | } 77 | } 78 | 79 | private volatile State state; 80 | private final T context; 81 | private final PublishSubject events = PublishSubject.create(); 82 | 83 | public static StateMachine create(T context, State initial) { 84 | return new StateMachine(context, initial); 85 | } 86 | 87 | public StateMachine(T context, State initial) { 88 | this.state = initial; 89 | this.context = context; 90 | } 91 | 92 | public Observable start() { 93 | return Observable.create(new OnSubscribe() { 94 | @Override 95 | public void call(Subscriber sub) { 96 | sub.add(events.collect(new Func0() { 97 | 98 | @Override 99 | public T call() { 100 | return context; 101 | } 102 | 103 | }, new Action2() { 104 | @Override 105 | public void call(T context, E event) { 106 | LOG.trace("{} : {}({})", context, state, event); 107 | final State next = state.next(event); 108 | if (next != null) { 109 | state.exit(context); 110 | state = next; 111 | next.enter(context).subscribe(StateMachine.this); 112 | } 113 | else if (!state.ignore.contains(event)) { 114 | LOG.warn("Unexpected event {} in state {} for {} ", event, state, context); 115 | } 116 | } 117 | }) 118 | .subscribe()); 119 | 120 | state.enter(context); 121 | } 122 | }); 123 | } 124 | 125 | @Override 126 | public void call(E event) { 127 | events.onNext(event); 128 | } 129 | 130 | public State getState() { 131 | return state; 132 | } 133 | 134 | public T getContext() { 135 | return context; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /ocelli-core/src/main/java/netflix/ocelli/util/Stopwatch.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.util; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | /** 6 | * A stopwatch starts counting when the object is created and can be used 7 | * to track how long operations take. For simplicity this contract does 8 | * not provide a mechanism to stop, restart, or clear the stopwatch. Instead 9 | * it just returns the elapsed time since the object was created. 10 | * 11 | * @author elandau 12 | * @see {@link Stopwatches} 13 | */ 14 | public interface Stopwatch { 15 | /** 16 | * Elapsed time since object was created. 17 | * @param units 18 | * @return 19 | */ 20 | long elapsed(TimeUnit units); 21 | } 22 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/LoadBalancerTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli; 2 | 3 | import com.google.common.collect.Lists; 4 | import netflix.ocelli.InstanceQuarantiner.IncarnationFactory; 5 | import netflix.ocelli.functions.Delays; 6 | import netflix.ocelli.loadbalancer.ChoiceOfTwoLoadBalancer; 7 | import netflix.ocelli.loadbalancer.RoundRobinLoadBalancer; 8 | import org.hamcrest.MatcherAssert; 9 | import org.junit.Test; 10 | import rx.Observable; 11 | import rx.Subscription; 12 | import rx.functions.Func0; 13 | import rx.functions.Func1; 14 | import rx.schedulers.TestScheduler; 15 | 16 | import java.util.Comparator; 17 | import java.util.LinkedHashMap; 18 | import java.util.Map; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import static org.hamcrest.Matchers.*; 22 | 23 | public class LoadBalancerTest { 24 | 25 | private static final Comparator COMPARE_BY_WEIGHT = new Comparator() { 26 | @Override 27 | public int compare(ClientWithWeight o1, ClientWithWeight o2) { 28 | return o1.weight.compareTo(o2.weight); 29 | } 30 | }; 31 | 32 | private static final Func1, Instance> CLIENT_FROM_ADDRESS = new Func1, Instance>() { 33 | @Override 34 | public Instance call(Instance t1) { 35 | return Instance.create(new ClientWithWeight(t1.getValue(), 1), t1.getLifecycle()); 36 | } 37 | }; 38 | 39 | @Test 40 | public void createFromFixedList() { 41 | LoadBalancer lb = LoadBalancer 42 | .fromFixedSource(Lists.newArrayList("host1:8080", "host2:8080")) 43 | .build(RoundRobinLoadBalancer.create(0), 44 | InstanceCollector.create(new Func0>() { 45 | @Override 46 | public Map call() { 47 | return new LinkedHashMap(); 48 | } 49 | })) 50 | ; 51 | 52 | MatcherAssert.assertThat("Unexpected first host chosen.", lb.next(), is("host2:8080")); 53 | MatcherAssert.assertThat("Unexpected second host chosen.", lb.next(), is("host1:8080")); 54 | } 55 | 56 | @Test 57 | public void createFromFixedListAndConvertToDifferentType() { 58 | LoadBalancer lb = LoadBalancer 59 | .fromFixedSource(Lists.newArrayList("host1:8080", "host2:8080")) 60 | .convertTo(CLIENT_FROM_ADDRESS) 61 | .build(RoundRobinLoadBalancer.create(0), 62 | InstanceCollector.create(new Func0>() { 63 | @Override 64 | public Map call() { 65 | return new LinkedHashMap(); 66 | } 67 | })) 68 | ; 69 | 70 | MatcherAssert.assertThat("Unexpected first host chosen.", lb.next().address, equalTo("host2:8080")); 71 | MatcherAssert.assertThat("Unexpected second host chosen.", lb.next().address, equalTo("host1:8080")); 72 | } 73 | 74 | @Test 75 | public void createFromFixedListWithAdvancedAlgorithm() { 76 | LoadBalancer lb = LoadBalancer 77 | .fromFixedSource( 78 | Lists.newArrayList(new ClientWithWeight("host1:8080", 1), new ClientWithWeight("host2:8080", 2))) 79 | .build(ChoiceOfTwoLoadBalancer.create(COMPARE_BY_WEIGHT), 80 | InstanceCollector.create(new Func0>() { 81 | @Override 82 | public Map call() { 83 | return new LinkedHashMap(); 84 | } 85 | })) 86 | ; 87 | 88 | MatcherAssert.assertThat("Unexpected first host chosen.", lb.next().address, equalTo("host2:8080")); 89 | MatcherAssert.assertThat("Unexpected second host chosen.", lb.next().address, equalTo("host2:8080")); 90 | } 91 | 92 | @Test 93 | public void createFromFixedAndUseQuaratiner() { 94 | TestScheduler scheduler = new TestScheduler(); 95 | 96 | LoadBalancer lb = LoadBalancer 97 | .fromFixedSource(Lists.newArrayList(new ClientWithLifecycle("host1:8080"), 98 | new ClientWithLifecycle("host2:8080"))) 99 | .withQuarantiner(new IncarnationFactory() { 100 | @Override 101 | public ClientWithLifecycle create(ClientWithLifecycle client, 102 | InstanceEventListener listener, 103 | Observable lifecycle) { 104 | return new ClientWithLifecycle(client, listener); 105 | } 106 | }, Delays.fixed(10, TimeUnit.SECONDS), scheduler) 107 | .build(RoundRobinLoadBalancer.create(0), 108 | InstanceCollector.create(new Func0>() { 109 | @Override 110 | public Map call() { 111 | return new LinkedHashMap(); 112 | } 113 | })); 114 | 115 | ClientWithLifecycle clientToFail = lb.next(); 116 | MatcherAssert.assertThat("Unexpected host chosen before failure.", clientToFail.address, equalTo("host2:8080")); 117 | clientToFail.forceFail(); 118 | 119 | MatcherAssert.assertThat("Unexpected first host chosen post failure.", lb.next().address, equalTo("host1:8080")); 120 | MatcherAssert.assertThat("Unexpected second host chosen post failure.", lb.next().address, equalTo("host1:8080")); 121 | } 122 | 123 | static class ClientWithLifecycle { 124 | private final String address; 125 | private final InstanceEventListener listener; 126 | 127 | public ClientWithLifecycle(String address) { 128 | this.address = address; 129 | listener = null; 130 | } 131 | 132 | public ClientWithLifecycle(ClientWithLifecycle parent, InstanceEventListener listener) { 133 | address = parent.address; 134 | this.listener = listener; 135 | } 136 | 137 | public void forceFail() { 138 | listener.onEvent(InstanceEvent.EXECUTION_FAILED, 0, TimeUnit.MILLISECONDS, new Throwable("Failed"), null); 139 | } 140 | } 141 | 142 | 143 | static class ClientWithWeight { 144 | private final String address; 145 | private final Integer weight; 146 | 147 | public ClientWithWeight(String address, int weight) { 148 | this.address = address; 149 | this.weight = weight; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/Behaviors.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import netflix.ocelli.util.RxUtil; 4 | import rx.Observable; 5 | import rx.Scheduler; 6 | import rx.functions.Func1; 7 | import rx.schedulers.Schedulers; 8 | 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | public class Behaviors { 13 | public static Func1> delay(final long amount, final TimeUnit units) { 14 | return delay(amount, units, Schedulers.computation()); 15 | } 16 | 17 | public static Func1> delay(final long amount, final TimeUnit units, final Scheduler scheduler) { 18 | return new Func1>() { 19 | @Override 20 | public Observable call(final TestClient client) { 21 | return Observable 22 | .just(client) 23 | .delay(amount, units, scheduler) 24 | ; 25 | } 26 | }; 27 | } 28 | 29 | public static Func1> immediate() { 30 | return new Func1>() { 31 | @Override 32 | public Observable call(TestClient client) { 33 | return Observable 34 | .just(client); 35 | } 36 | }; 37 | } 38 | 39 | public static Func1> failure(final long amount, final TimeUnit units) { 40 | return failure(amount, units, Schedulers.computation()); 41 | } 42 | 43 | public static Func1> failure(final long amount, final TimeUnit units, final Scheduler scheduler) { 44 | return new Func1>() { 45 | @Override 46 | public Observable call(TestClient client) { 47 | return Observable.timer(amount, units, scheduler) 48 | .flatMap(new Func1>() { 49 | @Override 50 | public Observable call(Long t1) { 51 | return Observable.error(new Exception("SimulatedErrorBehavior")); 52 | } 53 | }); 54 | } 55 | }; 56 | } 57 | 58 | public static Func1> failFirst(final int num) { 59 | return new Func1>() { 60 | private int counter; 61 | @Override 62 | public Observable call(TestClient client) { 63 | if (counter++ < num) { 64 | return Observable.error(new Exception("Failure-" + counter)); 65 | } 66 | return Observable.just(client); 67 | } 68 | }; 69 | } 70 | 71 | public static Func1> failure() { 72 | return new Func1>() { 73 | @Override 74 | public Observable call(TestClient client) { 75 | return Observable 76 | .just(client) 77 | .concatWith(Observable.error(new Exception("SimulatedErrorBehavior"))); 78 | } 79 | }; 80 | } 81 | 82 | public static Func1> degradation(final long initial, final long step, final TimeUnit units) { 83 | return new Func1>() { 84 | private AtomicLong counter = new AtomicLong(0); 85 | 86 | @Override 87 | public Observable call(TestClient client) { 88 | return Observable 89 | .just(client) 90 | .delay(initial + counter.incrementAndGet() + step, units); 91 | } 92 | }; 93 | } 94 | 95 | public static Func1> proportionalToLoad(final long baseline, final long step, final TimeUnit units) { 96 | return new Func1>() { 97 | private AtomicLong counter = new AtomicLong(0); 98 | 99 | @Override 100 | public Observable call(TestClient client) { 101 | final long count = counter.incrementAndGet(); 102 | return Observable 103 | .just(client) 104 | .delay(baseline + count + step, units) 105 | .finallyDo(RxUtil.decrement(counter)); 106 | } 107 | }; 108 | } 109 | 110 | public static Func1> empty() { 111 | return new Func1>() { 112 | @Override 113 | public Observable call(TestClient t1) { 114 | return Observable.empty(); 115 | } 116 | }; 117 | } 118 | 119 | // public static poissonDelay() 120 | // public static gaussianDelay(); 121 | // public static gcPauses(); 122 | } 123 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/Connects.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import rx.Observable; 4 | import rx.functions.Func1; 5 | 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public class Connects { 9 | public static Observable delay(final long timeout, final TimeUnit units) { 10 | return Observable.timer(timeout, units).ignoreElements().cast(Void.class); 11 | } 12 | 13 | public static Observable failure() { 14 | return Observable.error(new Exception("Connectus interruptus")); 15 | } 16 | 17 | public static Observable failure(final long timeout, final TimeUnit units) { 18 | return Observable.timer(timeout, units).concatMap(new Func1>() { 19 | @Override 20 | public Observable call(Long t1) { 21 | return Observable.error(new Exception("Connectus interruptus")); 22 | } 23 | }); 24 | } 25 | 26 | public static Observable immediate() { 27 | return Observable.empty(); 28 | } 29 | 30 | public static Observable never() { 31 | return Observable.never(); 32 | } 33 | 34 | public static Observable error(Exception e) { 35 | return Observable.error(e); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/ManualFailureDetector.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import java.util.concurrent.ConcurrentMap; 4 | 5 | import rx.Observable; 6 | import rx.functions.Func1; 7 | import rx.subjects.PublishSubject; 8 | 9 | import com.google.common.collect.Maps; 10 | 11 | public class ManualFailureDetector implements Func1> { 12 | private ConcurrentMap> clients = Maps.newConcurrentMap(); 13 | 14 | @Override 15 | public Observable call(TestClient client) { 16 | PublishSubject subject = PublishSubject.create(); 17 | PublishSubject prev = clients.putIfAbsent(client, subject); 18 | if (prev != null) { 19 | subject = prev; 20 | } 21 | return subject; 22 | } 23 | 24 | public PublishSubject get(TestClient client) { 25 | return clients.get(client); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/Operations.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import rx.Observable; 4 | import rx.functions.Func1; 5 | 6 | import java.net.SocketTimeoutException; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | public class Operations { 10 | 11 | public static Func1> delayed(final long duration, final TimeUnit units) { 12 | return new Func1>() { 13 | @Override 14 | public Observable call(final TestClient server) { 15 | return Observable 16 | .interval(duration, units) 17 | .first() 18 | .map(new Func1() { 19 | @Override 20 | public String call(Long t1) { 21 | return server + "-ok"; 22 | } 23 | }); 24 | } 25 | }; 26 | } 27 | 28 | public static Func1> timeout(final long duration, final TimeUnit units) { 29 | return new Func1>() { 30 | @Override 31 | public Observable call(final TestClient server) { 32 | return Observable 33 | .interval(duration, units) 34 | .flatMap(new Func1>() { 35 | @Override 36 | public Observable call(Long t1) { 37 | return Observable.error(new SocketTimeoutException("Timeout")); 38 | } 39 | }); 40 | } 41 | }; 42 | } 43 | 44 | public static TrackingOperation tracking(String response) { 45 | return new TrackingOperation(response); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/ResponseObserver.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import rx.Observer; 4 | 5 | import java.util.concurrent.CountDownLatch; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public class ResponseObserver implements Observer { 9 | private volatile Throwable t; 10 | private volatile String response; 11 | private CountDownLatch latch = new CountDownLatch(1); 12 | 13 | @Override 14 | public void onCompleted() { 15 | latch.countDown(); 16 | } 17 | 18 | @Override 19 | public void onError(Throwable e) { 20 | this.t = e; 21 | latch.countDown(); 22 | } 23 | 24 | @Override 25 | public void onNext(String t) { 26 | this.response = t; 27 | } 28 | 29 | public String await(long duration, TimeUnit units) throws Throwable { 30 | latch.await(duration, units); 31 | if (this.t != null) 32 | throw this.t; 33 | return response; 34 | } 35 | 36 | public String get() { 37 | return response; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/TestClient.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import java.util.concurrent.Semaphore; 6 | import java.util.concurrent.atomic.AtomicLong; 7 | 8 | import netflix.ocelli.util.RxUtil; 9 | import rx.Observable; 10 | import rx.Observer; 11 | import rx.functions.Func1; 12 | import rx.functions.Func2; 13 | 14 | public class TestClient { 15 | private final String id; 16 | private final Func1> behavior; 17 | private final Observable connect; 18 | private final int concurrency = 10; 19 | private final Semaphore sem = new Semaphore(concurrency); 20 | private final Set vips = new HashSet(); 21 | private String rack; 22 | 23 | public static Func1> byVip() { 24 | return new Func1>() { 25 | @Override 26 | public Observable call(TestClient t1) { 27 | return Observable.from(t1.vips).concatWith(Observable.just("*")); 28 | } 29 | }; 30 | } 31 | 32 | public static Func1> byRack() { 33 | return new Func1>() { 34 | @Override 35 | public Observable call(TestClient t1) { 36 | return Observable.just(t1.rack); 37 | } 38 | }; 39 | } 40 | 41 | public static Func1 byPendingRequestCount() { 42 | return new Func1() { 43 | @Override 44 | public Integer call(TestClient t1) { 45 | return t1.sem.availablePermits(); 46 | } 47 | }; 48 | } 49 | 50 | public static TestClient create(String id, Observable connect, Func1> behavior) { 51 | return new TestClient(id, connect, behavior); 52 | } 53 | 54 | public static TestClient create(String id, Func1> behavior) { 55 | return new TestClient(id, Connects.immediate(), behavior); 56 | } 57 | 58 | public static Func2> func() { 59 | return new Func2>() { 60 | @Override 61 | public Observable call(TestClient client, String request) { 62 | return client.execute(new Func1>() { 63 | @Override 64 | public Observable call(TestClient t1) { 65 | return Observable.just(t1.id()); 66 | } 67 | }); 68 | } 69 | }; 70 | } 71 | 72 | public TestClient(String id, Observable connect, Func1> behavior) { 73 | this.id = id; 74 | this.behavior = behavior; 75 | this.connect = connect; 76 | } 77 | 78 | public Observable connect() { 79 | return connect; 80 | } 81 | 82 | public TestClient withVip(String vip) { 83 | this.vips.add(vip); 84 | return this; 85 | } 86 | 87 | public TestClient withRack(String rack) { 88 | this.rack = rack; 89 | return this; 90 | } 91 | 92 | public Set vips() { 93 | return this.vips; 94 | } 95 | 96 | public String rack() { 97 | return this.rack; 98 | } 99 | 100 | public String id() { 101 | return this.id; 102 | } 103 | 104 | private AtomicLong executeCount = new AtomicLong(0); 105 | private AtomicLong onNextCount = new AtomicLong(0); 106 | private AtomicLong onCompletedCount = new AtomicLong(0); 107 | private AtomicLong onSubscribeCount = new AtomicLong(0); 108 | private AtomicLong onUnSubscribeCount = new AtomicLong(0); 109 | private AtomicLong onErrorCount = new AtomicLong(0); 110 | 111 | public long getExecuteCount() { 112 | return executeCount.get(); 113 | } 114 | 115 | public long getOnNextCount() { 116 | return onNextCount.get(); 117 | } 118 | 119 | public long getOnCompletedCount() { 120 | return onCompletedCount.get(); 121 | } 122 | 123 | public long getOnErrorCount() { 124 | return onErrorCount.get(); 125 | } 126 | 127 | public long getOnSubscribeCount() { 128 | return onSubscribeCount.get(); 129 | } 130 | 131 | public long getOnUnSubscribeCount() { 132 | return onUnSubscribeCount.get(); 133 | } 134 | 135 | 136 | public boolean hasError() { 137 | return onErrorCount.get() > 0; 138 | } 139 | 140 | public Observable execute(Func1> operation) { 141 | this.executeCount.incrementAndGet(); 142 | return behavior.call(this) 143 | .doOnSubscribe(RxUtil.increment(onSubscribeCount)) 144 | .doOnSubscribe(RxUtil.acquire(sem)) 145 | .doOnUnsubscribe(RxUtil.increment(onUnSubscribeCount)) 146 | .concatMap(operation) 147 | .doOnEach(new Observer() { 148 | @Override 149 | public void onCompleted() { 150 | onCompletedCount.incrementAndGet(); 151 | sem.release(); 152 | } 153 | 154 | @Override 155 | public void onError(Throwable e) { 156 | onErrorCount.incrementAndGet(); 157 | } 158 | 159 | @Override 160 | public void onNext(String t) { 161 | onNextCount.incrementAndGet(); 162 | } 163 | }); 164 | } 165 | 166 | public String toString() { 167 | return "Host[id=" + id + "]"; 168 | } 169 | 170 | @Override 171 | public int hashCode() { 172 | final int prime = 31; 173 | int result = 1; 174 | result = prime * result + ((id == null) ? 0 : id.hashCode()); 175 | return result; 176 | } 177 | 178 | @Override 179 | public boolean equals(Object obj) { 180 | if (this == obj) 181 | return true; 182 | if (obj == null) 183 | return false; 184 | if (getClass() != obj.getClass()) 185 | return false; 186 | TestClient other = (TestClient) obj; 187 | if (id == null) { 188 | if (other.id != null) 189 | return false; 190 | } else if (!id.equals(other.id)) 191 | return false; 192 | return true; 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/TestClientConnector.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import rx.Observable; 4 | import rx.Observable.OnSubscribe; 5 | import rx.Subscriber; 6 | import rx.subjects.PublishSubject; 7 | 8 | public class TestClientConnector implements OnSubscribe { 9 | 10 | private final PublishSubject stream = PublishSubject.create(); 11 | private final TestClient client; 12 | 13 | public TestClientConnector(TestClient client) { 14 | this.client = client; 15 | } 16 | 17 | @Override 18 | public void call(Subscriber s) { 19 | s.onCompleted(); 20 | stream.onNext(client); 21 | } 22 | 23 | public Observable stream() { 24 | return stream; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/TestClientConnectorFactory.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import java.util.concurrent.ConcurrentMap; 4 | 5 | import rx.Observable; 6 | import rx.functions.Func1; 7 | 8 | import com.google.common.collect.Maps; 9 | 10 | public class TestClientConnectorFactory implements Func1> { 11 | private ConcurrentMap connectors = Maps.newConcurrentMap(); 12 | 13 | @Override 14 | public Observable call(TestClient client) { 15 | return Observable.create(get(client)); 16 | } 17 | 18 | public TestClientConnector get(TestClient client) { 19 | TestClientConnector connector = new TestClientConnector(client); 20 | TestClientConnector prev = connectors.putIfAbsent(client, connector); 21 | if (prev != null) { 22 | connector = prev; 23 | } 24 | return connector; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/client/TrackingOperation.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.client; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import rx.Observable; 6 | import rx.functions.Func1; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class TrackingOperation implements Func1> { 12 | private static final Logger LOG = LoggerFactory.getLogger(TrackingOperation.class); 13 | 14 | private final String response; 15 | 16 | private List servers = new ArrayList(); 17 | 18 | public TrackingOperation(String response) { 19 | this.response = response; 20 | } 21 | 22 | @Override 23 | public Observable call(final TestClient client) { 24 | servers.add(client); 25 | return client.execute(new Func1>() { 26 | @Override 27 | public Observable call(TestClient t1) { 28 | return Observable.just(response); 29 | } 30 | }); 31 | } 32 | 33 | public List getServers() { 34 | return servers; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/functions/GuardsTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.functions; 2 | 3 | import org.junit.Test; 4 | 5 | import rx.functions.Func1; 6 | 7 | public class GuardsTest { 8 | @Test 9 | public void shouldRejectAfter100Percent() { 10 | Func1 guard = Limiters.exponential(0.90, 30); 11 | int discarded = 0; 12 | for (int i = 0; i < 100; i++) { 13 | guard.call(true); 14 | 15 | if (!guard.call(false)) { 16 | discarded++; 17 | } 18 | } 19 | System.out.println("Discarded : " + discarded); 20 | } 21 | 22 | @Test 23 | public void shouldRejectAfter90Percent() { 24 | Func1 guard = Limiters.exponential(0.90, 30); 25 | 26 | int discarded = 0; 27 | for (int i = 0; i < 100; i++) { 28 | guard.call(true); 29 | if (i % 5 == 0) { 30 | if (!guard.call(false)) { 31 | discarded++; 32 | } 33 | } 34 | } 35 | 36 | System.out.println("Discarded : " + discarded); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/loadbalancer/ChoiceOfTwoLoadBalancerTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer; 2 | 3 | import com.google.common.collect.Lists; 4 | import netflix.ocelli.LoadBalancer; 5 | import org.junit.Assert; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.rules.TestName; 9 | import rx.subjects.BehaviorSubject; 10 | 11 | import java.util.Comparator; 12 | import java.util.List; 13 | import java.util.NoSuchElementException; 14 | import java.util.concurrent.atomic.AtomicIntegerArray; 15 | 16 | public class ChoiceOfTwoLoadBalancerTest { 17 | @Rule 18 | public TestName name = new TestName(); 19 | 20 | private static final Comparator COMPARATOR = new Comparator() { 21 | @Override 22 | public int compare(Integer o1, Integer o2) { 23 | return o1 - o2; 24 | } 25 | }; 26 | 27 | @Test(expected=NoSuchElementException.class) 28 | public void testEmpty() { 29 | BehaviorSubject> source = BehaviorSubject.create(); 30 | 31 | LoadBalancer lb = LoadBalancer.fromSnapshotSource(source).build(ChoiceOfTwoLoadBalancer.create(COMPARATOR)); 32 | 33 | source.onNext(Lists.newArrayList()); 34 | 35 | lb.next(); 36 | } 37 | 38 | @Test 39 | public void testOne() { 40 | BehaviorSubject> source = BehaviorSubject.create(); 41 | LoadBalancer lb = LoadBalancer.fromSnapshotSource(source).build(ChoiceOfTwoLoadBalancer.create(COMPARATOR)); 42 | 43 | source.onNext(Lists.newArrayList(0)); 44 | 45 | for (int i = 0; i < 100; i++) { 46 | Assert.assertEquals(0, (int)lb.next()); 47 | } 48 | } 49 | 50 | @Test 51 | public void testTwo() { 52 | BehaviorSubject> source = BehaviorSubject.create(); 53 | LoadBalancer lb = LoadBalancer.fromSnapshotSource(source).build(ChoiceOfTwoLoadBalancer.create(COMPARATOR)); 54 | 55 | source.onNext(Lists.newArrayList(0,1)); 56 | 57 | AtomicIntegerArray counts = new AtomicIntegerArray(2); 58 | 59 | for (int i = 0; i < 100; i++) { 60 | counts.incrementAndGet(lb.next()); 61 | } 62 | Assert.assertEquals(counts.get(0), 0); 63 | Assert.assertEquals(counts.get(1), 100); 64 | } 65 | 66 | @Test 67 | public void testMany() { 68 | BehaviorSubject> source = BehaviorSubject.create(); 69 | LoadBalancer lb = LoadBalancer.fromSnapshotSource(source).build(ChoiceOfTwoLoadBalancer.create(COMPARATOR)); 70 | 71 | source.onNext(Lists.newArrayList(0,1,2,3,4,5,6,7,8,9)); 72 | 73 | AtomicIntegerArray counts = new AtomicIntegerArray(10); 74 | 75 | for (int i = 0; i < 100000; i++) { 76 | counts.incrementAndGet(lb.next()); 77 | } 78 | Double[] pct = new Double[counts.length()]; 79 | for (int i = 0; i < counts.length(); i++) { 80 | pct[i] = counts.get(i)/100000.0; 81 | } 82 | 83 | for (int i = 1; i < counts.length(); i++) { 84 | Assert.assertTrue(counts.get(i) > counts.get(i-1)); 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/loadbalancer/weighting/BaseWeightingStrategyTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import netflix.ocelli.LoadBalancer; 7 | 8 | import org.junit.Ignore; 9 | 10 | import rx.exceptions.OnErrorNotImplementedException; 11 | import rx.functions.Action1; 12 | 13 | import com.google.common.base.Function; 14 | import com.google.common.base.Joiner; 15 | import com.google.common.collect.Collections2; 16 | import com.google.common.collect.Lists; 17 | 18 | @Ignore 19 | public class BaseWeightingStrategyTest { 20 | 21 | /** 22 | * Creates a list of clients 23 | * 24 | * @param weights 25 | * @return 26 | */ 27 | static List create(Integer... weights) { 28 | List cam = new ArrayList(weights.length); 29 | int counter = 0; 30 | for (int i = 0; i < weights.length; i++) { 31 | cam.add(new IntClientAndMetrics(counter++, weights[i])); 32 | } 33 | return cam; 34 | } 35 | 36 | /** 37 | * Get an array of weights with indexes matching the list of clients. 38 | * @param caw 39 | * @return 40 | */ 41 | static int[] getWeights(ClientsAndWeights caw) { 42 | int[] weights = new int[caw.size()]; 43 | for (int i = 0; i < caw.size(); i++) { 44 | weights[i] = caw.getWeight(i); 45 | } 46 | return weights; 47 | } 48 | 49 | /** 50 | * Run a simulation of 'count' selects and update the clients 51 | * @param strategy 52 | * @param N 53 | * @param count 54 | * @return 55 | * @throws Throwable 56 | */ 57 | static Integer[] simulate(LoadBalancer lb, int N, int count) throws Throwable { 58 | // Set up array of counts 59 | final Integer[] counts = new Integer[N]; 60 | for (int i = 0; i < N; i++) { 61 | counts[i] = 0; 62 | } 63 | 64 | // Run simulation 65 | for (int i = 0; i < count; i++) { 66 | try { 67 | lb.toObservable().subscribe(new Action1() { 68 | @Override 69 | public void call(IntClientAndMetrics t1) { 70 | counts[t1.getClient()] = counts[t1.getClient()] + 1; 71 | } 72 | }); 73 | } 74 | catch (OnErrorNotImplementedException e) { 75 | throw e.getCause(); 76 | } 77 | } 78 | return counts; 79 | } 80 | 81 | static Integer[] roundToNearest(Integer[] counts, int amount) { 82 | int middle = amount / 2; 83 | for (int i = 0; i < counts.length; i++) { 84 | counts[i] = amount * ((counts[i] + middle) / amount); 85 | } 86 | return counts; 87 | } 88 | 89 | static String printClients(IntClientAndMetrics[] clients) { 90 | return Joiner.on(", ").join(Collections2.transform(Lists.newArrayList(clients), new Function() { 91 | @Override 92 | public Integer apply(IntClientAndMetrics arg0) { 93 | return arg0.getClient(); 94 | } 95 | })); 96 | } 97 | 98 | static String printMetrics(IntClientAndMetrics[] clients) { 99 | return Joiner.on(", ").join(Collections2.transform(Lists.newArrayList(clients), new Function() { 100 | @Override 101 | public Integer apply(IntClientAndMetrics arg0) { 102 | return arg0.getMetrics(); 103 | } 104 | })); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/loadbalancer/weighting/IntClientAndMetrics.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import rx.functions.Func1; 4 | 5 | public class IntClientAndMetrics { 6 | private Integer client; 7 | private Integer metrics; 8 | 9 | public IntClientAndMetrics(int client, int metrics) { 10 | this.client = client; 11 | this.metrics = metrics; 12 | } 13 | 14 | public Integer getClient() { 15 | return client; 16 | } 17 | 18 | public Integer getMetrics() { 19 | return metrics; 20 | } 21 | 22 | public static Func1 BY_METRIC = new Func1() { 23 | @Override 24 | public Integer call(IntClientAndMetrics t1) { 25 | return t1.getMetrics(); 26 | } 27 | }; 28 | 29 | @Override 30 | public String toString() { 31 | return "IntClientAndMetrics [client=" + client + ", metrics=" + metrics + "]"; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/loadbalancer/weighting/InverseMaxWeightingStrategyTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import com.google.common.collect.Lists; 4 | import netflix.ocelli.LoadBalancer; 5 | import netflix.ocelli.loadbalancer.RandomWeightedLoadBalancer; 6 | import netflix.ocelli.retry.RetryFailedTestRule; 7 | import netflix.ocelli.retry.RetryFailedTestRule.Retry; 8 | import org.junit.Assert; 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | import rx.subjects.BehaviorSubject; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.NoSuchElementException; 16 | 17 | public class InverseMaxWeightingStrategyTest extends BaseWeightingStrategyTest { 18 | @Rule 19 | public RetryFailedTestRule retryRule = new RetryFailedTestRule(); 20 | 21 | BehaviorSubject> subject = BehaviorSubject.create(); 22 | LoadBalancer lb = LoadBalancer.fromSnapshotSource(subject).build(RandomWeightedLoadBalancer.create( 23 | new InverseMaxWeightingStrategy(IntClientAndMetrics.BY_METRIC))); 24 | 25 | @Test(expected=NoSuchElementException.class) 26 | public void testEmptyClients() throws Throwable { 27 | List clients = create(); 28 | subject.onNext(clients); 29 | 30 | List counts = Arrays.asList(roundToNearest(simulate(lb, clients.size(), 1000), 100)); 31 | Assert.assertEquals(Lists.newArrayList(), counts); 32 | } 33 | 34 | @Test 35 | @Retry(5) 36 | public void testOneClient() throws Throwable { 37 | List clients = create(10); 38 | subject.onNext(clients); 39 | 40 | List counts = Arrays.asList(roundToNearest(simulate(lb, clients.size(), 1000), 100)); 41 | Assert.assertEquals(Lists.newArrayList(1000), counts); 42 | } 43 | 44 | @Test 45 | @Retry(5) 46 | public void testEqualsWeights() throws Throwable { 47 | List clients = create(1,1,1,1); 48 | subject.onNext(clients); 49 | 50 | List counts = Arrays.asList(roundToNearest(simulate(lb, clients.size(), 4000), 100)); 51 | Assert.assertEquals(Lists.newArrayList(1000, 1000, 1000, 1000), counts); 52 | } 53 | 54 | @Test 55 | @Retry(5) 56 | public void testDifferentWeights() throws Throwable { 57 | List clients = create(1,2,3,4); 58 | subject.onNext(clients); 59 | 60 | List counts = Arrays.asList(roundToNearest(simulate(lb, clients.size(), 4000), 100)); 61 | Assert.assertEquals(Lists.newArrayList(1600, 1200, 800, 400), counts); 62 | } 63 | 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/loadbalancer/weighting/LinearWeightingStrategyTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.loadbalancer.weighting; 2 | 3 | import com.google.common.collect.Lists; 4 | import netflix.ocelli.LoadBalancer; 5 | import netflix.ocelli.loadbalancer.RandomWeightedLoadBalancer; 6 | import netflix.ocelli.retry.RetryFailedTestRule; 7 | import netflix.ocelli.retry.RetryFailedTestRule.Retry; 8 | import org.junit.Assert; 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | import rx.subjects.BehaviorSubject; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.NoSuchElementException; 16 | 17 | public class LinearWeightingStrategyTest extends BaseWeightingStrategyTest { 18 | 19 | @Rule 20 | public RetryFailedTestRule retryRule = new RetryFailedTestRule(); 21 | 22 | BehaviorSubject> subject = BehaviorSubject.create(); 23 | LoadBalancer lb = LoadBalancer.fromSnapshotSource(subject).build(RandomWeightedLoadBalancer.create( 24 | new LinearWeightingStrategy(IntClientAndMetrics.BY_METRIC))); 25 | 26 | @Test(expected=NoSuchElementException.class) 27 | public void testEmptyClients() throws Throwable { 28 | List clients = create(); 29 | subject.onNext(clients); 30 | 31 | List counts = Arrays.asList(roundToNearest(simulate(lb, clients.size(), 1000), 100)); 32 | Assert.assertEquals(Lists.newArrayList(), counts); 33 | } 34 | 35 | @Test 36 | @Retry(5) 37 | public void testOneClient() throws Throwable { 38 | List clients = create(10); 39 | subject.onNext(clients); 40 | 41 | List counts = Arrays.asList(roundToNearest(simulate(lb, clients.size(), 1000), 100)); 42 | Assert.assertEquals(Lists.newArrayList(1000), counts); 43 | } 44 | 45 | @Test 46 | @Retry(5) 47 | public void testEqualsWeights() throws Throwable { 48 | List clients = create(1,1,1,1); 49 | subject.onNext(clients); 50 | 51 | List counts = Arrays.asList(roundToNearest(simulate(lb, clients.size(), 4000), 100)); 52 | Assert.assertEquals(Lists.newArrayList(1000, 1000, 1000, 1000), counts); 53 | } 54 | 55 | @Test 56 | @Retry(5) 57 | public void testDifferentWeights() throws Throwable { 58 | List clients = create(1,2,3,4); 59 | subject.onNext(clients); 60 | 61 | List counts = Arrays.asList(roundToNearest(simulate(lb, clients.size(), 4000), 100)); 62 | Assert.assertEquals(Lists.newArrayList(400, 800, 1200, 1600), counts); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/perf/PerfTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.perf; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import netflix.ocelli.LoadBalancer; 7 | import netflix.ocelli.client.Behaviors; 8 | import netflix.ocelli.client.Connects; 9 | import netflix.ocelli.client.TestClient; 10 | 11 | import org.junit.BeforeClass; 12 | import org.junit.Ignore; 13 | import org.junit.Test; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | public class PerfTest { 18 | private static final Logger LOG = LoggerFactory.getLogger(PerfTest.class); 19 | 20 | private static final int NUM_HOSTS = 1000; 21 | 22 | private LoadBalancer selector; 23 | 24 | @BeforeClass 25 | public static void setup() { 26 | List hosts = new ArrayList(); 27 | for (int i = 0; i < NUM_HOSTS-1; i++) { 28 | // hosts.add(TestHost.create("host-"+i, Connects.immediate(), Behaviors.delay(100 + (int)(100 * RANDOM.nextDouble()), TimeUnit.MILLISECONDS))); 29 | // hosts.add(TestHost.create("host-"+i, Connects.immediate(), Behaviors.proportionalToLoad(100, 10, TimeUnit.MILLISECONDS))); 30 | hosts.add(TestClient.create("host-"+i, Connects.immediate(), Behaviors.immediate())); 31 | } 32 | 33 | // hosts.add(TestHost.create("degrading", Connects.immediate(), Behaviors.degradation(100, 50, TimeUnit.MILLISECONDS))); 34 | 35 | // source = Observable 36 | // .from(hosts) 37 | // .map(MembershipEvent.toEvent(EventType.ADD)); 38 | } 39 | 40 | @Test 41 | @Ignore 42 | public void perf() throws InterruptedException { 43 | // ClientLifecycleFactory factory = 44 | // FailureDetectingClientLifecycleFactory.builder() 45 | // .build(); 46 | // this.selector = RoundRobinLoadBalancer.create(source.lift(ClientCollector.create(factory))); 47 | // 48 | //// this.selector.prime(10).toBlocking().last(); 49 | // 50 | // Observable.range(1, 10) 51 | // .subscribe(new Action1() { 52 | // @Override 53 | // public void call(final Integer id) { 54 | // Observable.interval(100, TimeUnit.MILLISECONDS, Schedulers.newThread()) 55 | // .subscribe(new Action1() { 56 | // @Override 57 | // public void call(final Long counter) { 58 | // Observable.create(selector) 59 | // .concatMap(new TrackingOperation(counter + "")) 60 | // .retry() 61 | // .subscribe(new Action1() { 62 | // @Override 63 | // public void call(String t1) { 64 | // LOG.info("{} - {} - {}", id, counter, t1); 65 | // } 66 | // }); 67 | // } 68 | // }); 69 | // } 70 | // }); 71 | } 72 | 73 | @Test 74 | @Ignore 75 | public void perf2() throws InterruptedException { 76 | // ClientLifecycleFactory factory = 77 | // FailureDetectingClientLifecycleFactory.builder() 78 | // .build(); 79 | // 80 | // this.selector = RoundRobinLoadBalancer.create(source.lift(ClientCollector.create(factory))); 81 | // 82 | // final AtomicLong messageCount = new AtomicLong(0); 83 | // 84 | // Observable.range(1, 400) 85 | // .subscribe(new Action1() { 86 | // @Override 87 | // public void call(final Integer id) { 88 | // Observable.interval(10, TimeUnit.MILLISECONDS, Schedulers.newThread()) 89 | // .subscribe(new Action1() { 90 | // @Override 91 | // public void call(final Long counter) { 92 | // Observable.create(selector) 93 | // .concatMap(new TrackingOperation(counter + "")) 94 | // .retry() 95 | // .subscribe(new Action1() { 96 | // @Override 97 | // public void call(String t1) { 98 | // messageCount.incrementAndGet(); 99 | // 100 | //// LOG.info("{} - {} - {}", id, counter, t1); 101 | // } 102 | // }); 103 | // } 104 | // }); 105 | // } 106 | // }); 107 | // 108 | // Observable.interval(1, TimeUnit.SECONDS).subscribe(new Action1() { 109 | // private long previous = 0; 110 | // @Override 111 | // public void call(Long t1) { 112 | // long current = messageCount.get(); 113 | // LOG.info("Rate : " + (current - previous) + " Host count: " + selector.all().count().toBlocking().first()); 114 | // previous = current; 115 | // } 116 | // }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/retry/RetryFailedTestRule.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.retry; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import org.junit.rules.TestRule; 9 | import org.junit.runner.Description; 10 | import org.junit.runners.model.Statement; 11 | 12 | public class RetryFailedTestRule implements TestRule { 13 | private int attemptNumber; 14 | 15 | @Target({ElementType.METHOD}) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | public @interface Retry { 18 | int value(); 19 | } 20 | 21 | public RetryFailedTestRule() { 22 | this.attemptNumber = 0; 23 | } 24 | 25 | public Statement apply(final Statement base, final Description description) { 26 | Retry retry = description.getAnnotation(Retry.class); 27 | final int retryCount = retry == null ? 1 : retry.value(); 28 | 29 | return new Statement() { 30 | @Override 31 | public void evaluate() throws Throwable { 32 | Throwable caughtThrowable = null; 33 | 34 | for (attemptNumber = 0; attemptNumber <= retryCount; ++attemptNumber) { 35 | try { 36 | base.evaluate(); 37 | 38 | System.err.println(description.getDisplayName() + ": attempt number " + attemptNumber + " succeeded"); 39 | 40 | return; 41 | } catch (Throwable t) { 42 | caughtThrowable = t; 43 | 44 | System.err.println(description.getDisplayName() + ": attempt number " + attemptNumber + " failed:"); 45 | System.err.println(t.toString()); 46 | } 47 | } 48 | 49 | System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures."); 50 | 51 | throw caughtThrowable; 52 | } 53 | }; 54 | } 55 | 56 | public int getAttemptNumber() { 57 | return attemptNumber; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/toplogies/TopologiesTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.toplogies; 2 | 3 | import com.google.common.collect.Sets; 4 | import netflix.ocelli.CloseableInstance; 5 | import netflix.ocelli.Host; 6 | import netflix.ocelli.Instance; 7 | import netflix.ocelli.InstanceCollector; 8 | import netflix.ocelli.functions.Functions; 9 | import netflix.ocelli.topologies.RingTopology; 10 | import netflix.ocelli.util.RxUtil; 11 | import org.junit.Assert; 12 | import org.junit.Test; 13 | import rx.schedulers.TestScheduler; 14 | import rx.subjects.PublishSubject; 15 | 16 | import java.util.List; 17 | import java.util.concurrent.atomic.AtomicReference; 18 | 19 | public class TopologiesTest { 20 | public static class HostWithId extends Host { 21 | private final Integer id; 22 | 23 | public HostWithId(String hostName, int port, Integer id) { 24 | super(hostName, port); 25 | this.id = id; 26 | } 27 | 28 | public Integer getId() { 29 | return this.id; 30 | } 31 | 32 | public String toString() { 33 | return id.toString(); 34 | } 35 | } 36 | 37 | @Test 38 | public void test() { 39 | CloseableInstance m1 = CloseableInstance.from(1); 40 | CloseableInstance m2 = CloseableInstance.from(2); 41 | CloseableInstance m3 = CloseableInstance.from(3); 42 | CloseableInstance m4 = CloseableInstance.from(4); 43 | CloseableInstance m6 = CloseableInstance.from(6); 44 | CloseableInstance m7 = CloseableInstance.from(7); 45 | CloseableInstance m8 = CloseableInstance.from(8); 46 | CloseableInstance m9 = CloseableInstance.from(9); 47 | CloseableInstance m10 = CloseableInstance.from(10); 48 | CloseableInstance m11 = CloseableInstance.from(11); 49 | 50 | PublishSubject> members = PublishSubject.create(); 51 | 52 | TestScheduler scheduler = new TestScheduler(); 53 | 54 | RingTopology mapper = RingTopology.create(5, Functions.identity(), Functions.memoize(3), scheduler); 55 | 56 | AtomicReference> current = new AtomicReference>(); 57 | 58 | members 59 | .doOnNext(RxUtil.info("add")) 60 | .compose(mapper) 61 | .compose(InstanceCollector.create()) 62 | .doOnNext(RxUtil.info("current")) 63 | .subscribe(RxUtil.set(current)); 64 | 65 | members.onNext(m11); 66 | Assert.assertEquals(Sets.newHashSet(11) , Sets.newHashSet(current.get())); 67 | members.onNext(m7); 68 | Assert.assertEquals(Sets.newHashSet(7, 11) , Sets.newHashSet(current.get())); 69 | members.onNext(m1); 70 | Assert.assertEquals(Sets.newHashSet(1, 7, 11) , Sets.newHashSet(current.get())); 71 | members.onNext(m2); 72 | Assert.assertEquals(Sets.newHashSet(1, 7, 11) , Sets.newHashSet(current.get())); 73 | members.onNext(m4); 74 | Assert.assertEquals(Sets.newHashSet(1, 7, 11) , Sets.newHashSet(current.get())); 75 | members.onNext(m3); 76 | Assert.assertEquals(Sets.newHashSet(1, 7, 11) , Sets.newHashSet(current.get())); 77 | members.onNext(m8); 78 | Assert.assertEquals(Sets.newHashSet(7, 8, 11) , Sets.newHashSet(current.get())); 79 | members.onNext(m10); 80 | Assert.assertEquals(Sets.newHashSet(7, 8, 10), Sets.newHashSet(current.get())); 81 | members.onNext(m9); 82 | Assert.assertEquals(Sets.newHashSet(7, 8, 9), Sets.newHashSet(current.get())); 83 | members.onNext(m6); 84 | Assert.assertEquals(Sets.newHashSet(6, 7, 8), Sets.newHashSet(current.get())); 85 | 86 | m6.close(); 87 | Assert.assertEquals(Sets.newHashSet(7, 8, 9), Sets.newHashSet(current.get())); 88 | m9.close(); 89 | Assert.assertEquals(Sets.newHashSet(7, 8, 10), Sets.newHashSet(current.get())); 90 | m8.close(); 91 | Assert.assertEquals(Sets.newHashSet(7, 10, 11), Sets.newHashSet(current.get())); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/util/CountDownAction.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.util; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CopyOnWriteArrayList; 5 | import java.util.concurrent.CountDownLatch; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import rx.functions.Action1; 9 | 10 | public class CountDownAction implements Action1 { 11 | private CountDownLatch latch; 12 | private CopyOnWriteArrayList list = new CopyOnWriteArrayList(); 13 | 14 | public CountDownAction(int count) { 15 | latch = new CountDownLatch(count); 16 | } 17 | 18 | @Override 19 | public void call(T t1) { 20 | list.add(t1); 21 | latch.countDown(); 22 | } 23 | 24 | public void await(long timeout, TimeUnit units) throws Exception { 25 | latch.await(timeout, units); 26 | } 27 | 28 | public List get() { 29 | return list; 30 | } 31 | 32 | public void reset(int count) { 33 | latch = new CountDownLatch(count); 34 | list = new CopyOnWriteArrayList(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ocelli-core/src/test/java/netflix/ocelli/util/RandomQueueTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.util; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Ignore; 5 | import org.junit.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.NoSuchElementException; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | public class RandomQueueTest { 14 | @Test 15 | public void shouldBeInitiallyEmpty() { 16 | RandomBlockingQueue queue = new RandomBlockingQueue(); 17 | Assert.assertTrue(queue.isEmpty()); 18 | Assert.assertNull(queue.peek()); 19 | Assert.assertTrue(queue.isEmpty()); 20 | try { 21 | queue.remove(); 22 | Assert.fail(); 23 | } 24 | catch (NoSuchElementException e) { 25 | } 26 | } 27 | 28 | @Test 29 | public void shouldBlockEmptyQueue() throws InterruptedException { 30 | RandomBlockingQueue queue = new RandomBlockingQueue(); 31 | Assert.assertNull(queue.poll(100, TimeUnit.MILLISECONDS)); 32 | } 33 | 34 | @Test 35 | @Ignore 36 | public void addRemoveAndShouldBlock() throws InterruptedException { 37 | RandomBlockingQueue queue = new RandomBlockingQueue(); 38 | queue.add(123); 39 | Integer item = queue.take(); 40 | Assert.assertEquals((Integer)123, item); 41 | Assert.assertNull(queue.poll(100, TimeUnit.MILLISECONDS)); 42 | } 43 | 44 | @Test 45 | public void addOne() { 46 | RandomBlockingQueue queue = new RandomBlockingQueue(); 47 | queue.add(123); 48 | 49 | Assert.assertTrue(!queue.isEmpty()); 50 | Assert.assertEquals(1, queue.size()); 51 | Assert.assertEquals((Integer)123, queue.peek()); 52 | Assert.assertEquals((Integer)123, queue.poll()); 53 | Assert.assertTrue(queue.isEmpty()); 54 | Assert.assertEquals(0, queue.size()); 55 | Assert.assertNull(queue.peek()); 56 | 57 | try { 58 | queue.remove(); 59 | Assert.fail(); 60 | } 61 | catch (NoSuchElementException e) { 62 | } 63 | } 64 | 65 | @Test(timeout=1000) 66 | public void removeIsRandom() { 67 | RandomBlockingQueue queue = new RandomBlockingQueue(); 68 | List items = new ArrayList(); 69 | for (int i = 0; i < 100; i++) { 70 | items.add(i); 71 | queue.add(i); 72 | } 73 | 74 | List actual = new ArrayList(); 75 | Integer item; 76 | while (null != (item = queue.poll())) { 77 | actual.add(item); 78 | } 79 | Assert.assertTrue(queue.isEmpty()); 80 | Assert.assertEquals(100, actual.size()); 81 | Assert.assertNotSame(items, actual); 82 | Collections.sort(actual); 83 | Assert.assertEquals(items, actual); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ocelli-core/src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ocelli-eureka/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Netflix, Inc. 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 | dependencies { 17 | compile project(':ocelli-core') 18 | compile 'com.netflix.eureka:eureka-client:1.1.159' 19 | } 20 | -------------------------------------------------------------------------------- /ocelli-eureka/src/test/java/netflix/ocelli/eureka/EurekaInterestManagerTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.eureka; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | import junit.framework.Assert; 9 | import netflix.ocelli.InstanceCollector; 10 | import netflix.ocelli.util.RxUtil; 11 | 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.Mockito; 16 | import org.mockito.runners.MockitoJUnitRunner; 17 | 18 | import rx.schedulers.TestScheduler; 19 | 20 | import com.google.common.collect.Sets; 21 | import com.netflix.appinfo.InstanceInfo; 22 | import com.netflix.appinfo.InstanceInfo.InstanceStatus; 23 | import com.netflix.discovery.DiscoveryClient; 24 | import com.netflix.discovery.shared.Application; 25 | 26 | @RunWith(MockitoJUnitRunner.class) 27 | public class EurekaInterestManagerTest { 28 | @Mock 29 | private DiscoveryClient client; 30 | 31 | @Mock 32 | private Application application; 33 | 34 | @Test 35 | public void testAddRemoveInstances() { 36 | InstanceInfo i1 = createInstance(1); 37 | InstanceInfo i2 = createInstance(2); 38 | InstanceInfo i3 = createInstance(3); 39 | InstanceInfo i4 = createInstance(4); 40 | 41 | Mockito.when(client.getApplication("foo")).thenReturn(application); 42 | 43 | AtomicReference> result = new AtomicReference>(); 44 | 45 | TestScheduler scheduler = new TestScheduler(); 46 | EurekaInterestManager eureka = new EurekaInterestManager(client); 47 | eureka.newInterest() 48 | .forApplication("foo") 49 | .withRefreshInterval(1, TimeUnit.SECONDS) 50 | .withScheduler(scheduler) 51 | .asObservable() 52 | .compose(InstanceCollector.create()) 53 | .subscribe(RxUtil.set(result)); 54 | 55 | Mockito.when(application.getInstances()).thenReturn(Arrays.asList(i1, i2)); 56 | scheduler.advanceTimeBy(10, TimeUnit.SECONDS); 57 | Assert.assertEquals(Sets.newHashSet(i2, i1), Sets.newHashSet(result.get())); 58 | 59 | Mockito.when(application.getInstances()).thenReturn(Arrays.asList(i1, i2, i3)); 60 | scheduler.advanceTimeBy(10, TimeUnit.SECONDS); 61 | Assert.assertEquals(Sets.newHashSet(i3, i2, i1), Sets.newHashSet(result.get())); 62 | 63 | Mockito.when(application.getInstances()).thenReturn(Arrays.asList(i3, i4)); 64 | scheduler.advanceTimeBy(10, TimeUnit.SECONDS); 65 | Assert.assertEquals(Sets.newHashSet(i3, i4), Sets.newHashSet(result.get())); 66 | 67 | Mockito.when(application.getInstances()).thenReturn(Arrays.asList()); 68 | scheduler.advanceTimeBy(10, TimeUnit.SECONDS); 69 | Assert.assertEquals(Sets.newHashSet(), Sets.newHashSet(result.get())); 70 | } 71 | 72 | InstanceInfo createInstance(int id) { 73 | return InstanceInfo.Builder.newBuilder() 74 | .setHostName("localhost:800" + id) 75 | .setAppName("foo") 76 | .setStatus(InstanceStatus.UP) 77 | .build(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ocelli-eureka2/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Netflix, Inc. 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 | dependencies { 18 | compile project(':ocelli-core') 19 | compile "com.netflix.eureka:eureka2-client-shaded:2.0.0-rc.2" 20 | } 21 | -------------------------------------------------------------------------------- /ocelli-eureka2/src/main/java/netflix/ocelli/eureka2/Eureka2InterestManager.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.eureka2; 2 | 3 | import com.netflix.eureka2.client.EurekaInterestClient; 4 | import com.netflix.eureka2.client.Eurekas; 5 | import com.netflix.eureka2.client.resolver.ServerResolver; 6 | import com.netflix.eureka2.interests.ChangeNotification; 7 | import com.netflix.eureka2.interests.Interest; 8 | import com.netflix.eureka2.interests.Interests; 9 | import com.netflix.eureka2.registry.instance.InstanceInfo; 10 | import com.netflix.eureka2.registry.instance.ServicePort; 11 | import netflix.ocelli.Instance; 12 | import netflix.ocelli.InstanceManager; 13 | import rx.Observable; 14 | import rx.Observable.OnSubscribe; 15 | import rx.Subscriber; 16 | import rx.functions.Action1; 17 | import rx.functions.Func1; 18 | 19 | import javax.inject.Inject; 20 | import java.net.InetSocketAddress; 21 | import java.net.SocketAddress; 22 | import java.util.HashSet; 23 | 24 | /** 25 | * @author Nitesh Kant 26 | */ 27 | public class Eureka2InterestManager { 28 | 29 | private final EurekaInterestClient client; 30 | private static final DefaultMapper defaultMapper = new DefaultMapper(); 31 | 32 | public Eureka2InterestManager(ServerResolver eurekaResolver) { 33 | client = Eurekas.newInterestClientBuilder().withServerResolver(eurekaResolver).build(); 34 | } 35 | 36 | @Inject 37 | public Eureka2InterestManager(EurekaInterestClient client) { 38 | this.client = client; 39 | } 40 | 41 | public Observable> forVip(String... vips) { 42 | return forInterest(Interests.forVips(vips)); 43 | } 44 | 45 | public Observable> forInterest(Interest interest) { 46 | return forInterest(interest, defaultMapper); 47 | } 48 | 49 | public Observable> forInterest(final Interest interest, 50 | final Func1 instanceInfoToHost) { 51 | return Observable.create(new OnSubscribe>() { 52 | @Override 53 | public void call(Subscriber> s) { 54 | final InstanceManager subject = InstanceManager.create(); 55 | s.add(client 56 | .forInterest(interest) 57 | .subscribe(new Action1>() { 58 | @Override 59 | public void call(ChangeNotification notification) { 60 | SocketAddress host = instanceInfoToHost.call(notification.getData()); 61 | switch (notification.getKind()) { 62 | case Add: 63 | subject.add(host); 64 | break; 65 | case Delete: 66 | subject.remove(host); 67 | break; 68 | case Modify: 69 | subject.remove(host); 70 | subject.add(host); 71 | break; 72 | default: 73 | break; 74 | } 75 | } 76 | })); 77 | 78 | subject.subscribe(s); 79 | } 80 | }); 81 | } 82 | 83 | protected static class DefaultMapper implements Func1 { 84 | 85 | @Override 86 | public SocketAddress call(InstanceInfo instanceInfo) { 87 | String ipAddress = instanceInfo.getDataCenterInfo().getDefaultAddress().getIpAddress(); 88 | HashSet servicePorts = instanceInfo.getPorts(); 89 | ServicePort portToUse = servicePorts.iterator().next(); 90 | return new InetSocketAddress(ipAddress, portToUse.getPort()); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ocelli-eureka2/src/test/java/netflix/ocelli/eureka2/Eureka2InterestManagerTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.eureka2; 2 | 3 | import com.netflix.eureka2.client.EurekaInterestClient; 4 | import com.netflix.eureka2.interests.ChangeNotification; 5 | import com.netflix.eureka2.interests.Interest; 6 | import com.netflix.eureka2.interests.Interests; 7 | import com.netflix.eureka2.registry.datacenter.BasicDataCenterInfo; 8 | import com.netflix.eureka2.registry.instance.InstanceInfo; 9 | import com.netflix.eureka2.registry.instance.ServicePort; 10 | import netflix.ocelli.Instance; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.Mock; 16 | import org.mockito.Mockito; 17 | import org.mockito.runners.MockitoJUnitRunner; 18 | import rx.Observable; 19 | 20 | import java.net.SocketAddress; 21 | import java.util.Collections; 22 | import java.util.HashSet; 23 | import java.util.List; 24 | import java.util.concurrent.TimeUnit; 25 | 26 | @RunWith(MockitoJUnitRunner.class) 27 | public class Eureka2InterestManagerTest { 28 | @Mock 29 | private EurekaInterestClient clientMock; 30 | private Eureka2InterestManager membershipSource; 31 | 32 | public static final InstanceInfo INSTANCE_1 = new InstanceInfo.Builder() 33 | .withId("id_serviceA") 34 | .withApp("ServiceA") 35 | .withAppGroup("ServiceA_1") 36 | .withStatus(InstanceInfo.Status.UP) 37 | .withPorts(new HashSet(Collections.singletonList(new ServicePort(8000, false)))) 38 | .withDataCenterInfo(BasicDataCenterInfo.fromSystemData()) 39 | .build(); 40 | 41 | public static final InstanceInfo INSTANCE_2 = new InstanceInfo.Builder() 42 | .withId("id_serviceA_2") 43 | .withApp("ServiceA") 44 | .withAppGroup("ServiceA_1") 45 | .withStatus(InstanceInfo.Status.UP) 46 | .withPorts(new HashSet(Collections.singletonList(new ServicePort(8001, false)))) 47 | .withDataCenterInfo(BasicDataCenterInfo.fromSystemData()) 48 | .build(); 49 | 50 | public static final ChangeNotification ADD_INSTANCE_1 = 51 | new ChangeNotification(ChangeNotification.Kind.Add, INSTANCE_1); 52 | 53 | public static final ChangeNotification ADD_INSTANCE_2 = 54 | new ChangeNotification(ChangeNotification.Kind.Add, INSTANCE_2); 55 | 56 | @Before 57 | public void setUp() throws Exception { 58 | membershipSource = new Eureka2InterestManager(clientMock); 59 | } 60 | 61 | @Test 62 | public void testVipBasedInterest() throws Exception { 63 | Interest interest = Interests.forVips("test-vip"); 64 | Mockito.when(clientMock.forInterest(interest)).thenReturn(Observable.just(ADD_INSTANCE_1, ADD_INSTANCE_2)); 65 | 66 | List> instances = membershipSource 67 | .forInterest(interest) 68 | .take(2) 69 | .toList().toBlocking() 70 | .toFuture() 71 | .get(1, TimeUnit.SECONDS); 72 | 73 | Assert.assertEquals(2, instances.size()); 74 | System.out.println("instances = " + instances); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ocelli-examples/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Netflix, Inc. 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 | // examples are in Java 8 while rest of projects is Java 7 18 | sourceCompatibility = JavaVersion.VERSION_1_8 19 | targetCompatibility = JavaVersion.VERSION_1_8 20 | 21 | dependencies { 22 | compile project(':ocelli-rxnetty') 23 | compile project(':ocelli-eureka') 24 | compile project(':ocelli-eureka2') 25 | } 26 | 27 | -------------------------------------------------------------------------------- /ocelli-examples/src/main/java/netflix/ocelli/examples/rxnetty/http/ChoiceOfTwo.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.examples.rxnetty.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.reactivex.netty.protocol.http.client.HttpClient; 5 | import netflix.ocelli.Instance; 6 | import netflix.ocelli.rxnetty.protocol.http.HttpLoadBalancer; 7 | import netflix.ocelli.rxnetty.protocol.http.WeightedHttpClientListener; 8 | import rx.Observable; 9 | 10 | import java.net.ConnectException; 11 | import java.net.SocketAddress; 12 | import java.net.SocketException; 13 | import java.nio.charset.Charset; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | import static netflix.ocelli.examples.rxnetty.http.HttpExampleUtils.*; 17 | 18 | public final class ChoiceOfTwo { 19 | 20 | private ChoiceOfTwo() { 21 | } 22 | 23 | public static void main(String[] args) { 24 | 25 | Observable> hosts = newHostStreamWithCannedLatencies(5L, 1L, 2L, 1L, 0L); 26 | 27 | HttpLoadBalancer lb = 28 | HttpLoadBalancer.choiceOfTwo(hosts, failureListener -> { 29 | return new WeightedHttpClientListener() { 30 | 31 | private volatile int lastSeenLatencyInverse; 32 | 33 | @Override 34 | public int getWeight() { 35 | return lastSeenLatencyInverse; 36 | } 37 | 38 | @Override 39 | public void onResponseHeadersReceived(int responseCode, long duration, TimeUnit timeUnit) { 40 | /* This is just a demo for how to wire the weight of an instance to the load balancer, it 41 | * certainly is not the algorithm to be used in real production applications. 42 | */ 43 | lastSeenLatencyInverse = Integer.MAX_VALUE - (int)duration; // High latency => low weight 44 | if (responseCode == 503) { 45 | // When throttled, quarantine. 46 | failureListener.quarantine(1, TimeUnit.MINUTES); 47 | } 48 | } 49 | 50 | @Override 51 | public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) { 52 | // Connect failed, remove 53 | failureListener.remove(); 54 | } 55 | }; 56 | }); 57 | 58 | HttpClient.newClient(lb.toConnectionProvider()) 59 | .createGet("/hello") 60 | .doOnNext(System.out::println) 61 | .flatMap(resp -> { 62 | if (resp.getStatus().code() != 200) { 63 | return Observable.error(new InvalidResponseException()); 64 | } 65 | return resp.getContent(); 66 | }) 67 | .retry((integer, throwable) -> throwable instanceof SocketException 68 | || throwable instanceof ConnectException 69 | || throwable instanceof InvalidResponseException) 70 | .repeat(10) 71 | .toBlocking() 72 | .forEach(bb -> bb.toString(Charset.defaultCharset())); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ocelli-examples/src/main/java/netflix/ocelli/examples/rxnetty/http/HttpExampleUtils.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.examples.rxnetty.http; 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus; 4 | import io.reactivex.netty.protocol.http.server.HttpServer; 5 | import netflix.ocelli.Instance; 6 | import rx.Observable; 7 | import rx.functions.Func1; 8 | 9 | import java.net.InetSocketAddress; 10 | import java.net.SocketAddress; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | final class HttpExampleUtils { 14 | 15 | private HttpExampleUtils() { 16 | } 17 | 18 | protected static Observable> newHostStreamWithCannedLatencies(Long... latencies) { 19 | return Observable.from(latencies) 20 | .map(latency -> { 21 | return startServer(latency); 22 | }) 23 | .map(new SockAddrToInstance()); 24 | } 25 | 26 | protected static Observable> newHostStreamWithCannedStatus( 27 | HttpResponseStatus... cannedStatuses) { 28 | return Observable.from(cannedStatuses) 29 | .map(cannedStatus -> { 30 | if (null != cannedStatus) { 31 | return startServer(cannedStatus); 32 | } 33 | return new InetSocketAddress(0); 34 | }) 35 | .map(new SockAddrToInstance()); 36 | } 37 | 38 | protected static SocketAddress startServer(long latencyMillis) { 39 | return HttpServer.newServer() 40 | .start((request, response) -> { 41 | return Observable.timer(latencyMillis, TimeUnit.MILLISECONDS) 42 | .flatMap(aTick -> response.addHeader("X-Instance", 43 | response.unsafeNettyChannel() 44 | .localAddress()) 45 | .setStatus(HttpResponseStatus.OK)); 46 | }) 47 | .getServerAddress(); 48 | } 49 | 50 | protected static SocketAddress startServer(HttpResponseStatus cannedStatus) { 51 | return HttpServer.newServer() 52 | .start((request, response) -> { 53 | return response.addHeader("X-Instance", response.unsafeNettyChannel().localAddress()) 54 | .setStatus(cannedStatus); 55 | }) 56 | .getServerAddress(); 57 | } 58 | 59 | protected static class InvalidResponseException extends RuntimeException { 60 | 61 | private static final long serialVersionUID = -712946630951320233L; 62 | 63 | public InvalidResponseException() { 64 | } 65 | } 66 | 67 | private static class SockAddrToInstance implements Func1> { 68 | @Override 69 | public Instance call(SocketAddress socketAddr) { 70 | return new Instance() { 71 | 72 | @Override 73 | public Observable getLifecycle() { 74 | return Observable.never(); 75 | } 76 | 77 | @Override 78 | public SocketAddress getValue() { 79 | return socketAddr; 80 | } 81 | }; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ocelli-examples/src/main/java/netflix/ocelli/examples/rxnetty/http/RandomWeighted.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.examples.rxnetty.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.reactivex.netty.protocol.http.client.HttpClient; 5 | import netflix.ocelli.Instance; 6 | import netflix.ocelli.examples.rxnetty.http.HttpExampleUtils.*; 7 | import netflix.ocelli.rxnetty.protocol.http.HttpLoadBalancer; 8 | import netflix.ocelli.rxnetty.protocol.http.WeightedHttpClientListener; 9 | import rx.Observable; 10 | 11 | import java.net.ConnectException; 12 | import java.net.SocketAddress; 13 | import java.net.SocketException; 14 | import java.nio.charset.Charset; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import static netflix.ocelli.examples.rxnetty.http.HttpExampleUtils.*; 18 | 19 | public final class RandomWeighted { 20 | 21 | private RandomWeighted() { 22 | } 23 | 24 | public static void main(String[] args) { 25 | 26 | Observable> hosts = newHostStreamWithCannedLatencies(1L, 2L); 27 | 28 | HttpLoadBalancer lb = 29 | HttpLoadBalancer.weigthedRandom(hosts, failureListener -> { 30 | return new WeightedHttpClientListener() { 31 | 32 | private volatile int weight; 33 | 34 | @Override 35 | public int getWeight() { 36 | return weight; 37 | } 38 | 39 | @Override 40 | public void onResponseHeadersReceived(int responseCode, long duration, TimeUnit timeUnit) { 41 | /* This is just a demo for how to wire the weight of an instance to the load balancer, it 42 | * certainly is not the algorithm to be used in real production applications. 43 | */ 44 | weight = (int) (Long.MAX_VALUE - duration); // High latency => low weight 45 | 46 | if (responseCode == 503) { 47 | // When throttled, quarantine. 48 | failureListener.quarantine(1, TimeUnit.MINUTES); 49 | } 50 | } 51 | 52 | @Override 53 | public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) { 54 | // Connect failed, remove 55 | failureListener.remove(); 56 | } 57 | }; 58 | }); 59 | 60 | HttpClient.newClient(lb.toConnectionProvider()) 61 | .createGet("/hello") 62 | .doOnNext(System.out::println) 63 | .flatMap(resp -> { 64 | if (resp.getStatus().code() != 200) { 65 | return Observable.error(new InvalidResponseException()); 66 | } 67 | return resp.getContent(); 68 | }) 69 | .retry((integer, throwable) -> throwable instanceof SocketException 70 | || throwable instanceof ConnectException 71 | || throwable instanceof InvalidResponseException) 72 | .repeat(10) 73 | .toBlocking() 74 | .forEach(bb -> bb.toString(Charset.defaultCharset())); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ocelli-examples/src/main/java/netflix/ocelli/examples/rxnetty/http/RoundRobin.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.examples.rxnetty.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.reactivex.netty.protocol.http.client.HttpClient; 5 | import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; 6 | import netflix.ocelli.Instance; 7 | import netflix.ocelli.examples.rxnetty.http.HttpExampleUtils.*; 8 | import netflix.ocelli.rxnetty.protocol.http.HttpLoadBalancer; 9 | import rx.Observable; 10 | 11 | import java.net.ConnectException; 12 | import java.net.SocketAddress; 13 | import java.net.SocketException; 14 | import java.nio.charset.Charset; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import static io.netty.handler.codec.http.HttpResponseStatus.*; 18 | import static netflix.ocelli.examples.rxnetty.http.HttpExampleUtils.*; 19 | 20 | public final class RoundRobin { 21 | 22 | private RoundRobin() { 23 | } 24 | 25 | public static void main(String[] args) { 26 | 27 | Observable> hosts = newHostStreamWithCannedStatus(OK, SERVICE_UNAVAILABLE, 28 | null/*Unavailable socket address*/); 29 | 30 | HttpLoadBalancer lb = 31 | HttpLoadBalancer.roundRobin(hosts, failureListener -> { 32 | return new HttpClientEventsListener() { 33 | @Override 34 | public void onResponseHeadersReceived(int responseCode, long duration, TimeUnit timeUnit) { 35 | if (responseCode == 503) { 36 | // When throttled, quarantine. 37 | failureListener.quarantine(1, TimeUnit.MINUTES); 38 | } 39 | } 40 | 41 | @Override 42 | public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) { 43 | // Connect failed, remove 44 | failureListener.remove(); 45 | } 46 | }; 47 | }); 48 | 49 | HttpClient.newClient(lb.toConnectionProvider()) 50 | .createGet("/hello") 51 | .doOnNext(System.out::println) 52 | .flatMap(resp -> { 53 | if (resp.getStatus().code() != 200) { 54 | return Observable.error(new InvalidResponseException()); 55 | } 56 | return resp.getContent(); 57 | }) 58 | .retry((integer, throwable) -> throwable instanceof SocketException 59 | || throwable instanceof ConnectException 60 | || throwable instanceof InvalidResponseException) 61 | .repeat(10) 62 | .toBlocking() 63 | .forEach(bb -> bb.toString(Charset.defaultCharset())); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ocelli-examples/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Netflix, Inc. 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 | log4j.rootLogger=INFO, stdout 17 | 18 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 19 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 20 | log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] (%F:%L) - %m%n -------------------------------------------------------------------------------- /ocelli-rxnetty/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Netflix, Inc. 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 | dependencies { 18 | compile project(':ocelli-core') 19 | compile 'io.reactivex:rxnetty-http:0.5.0-SNAPSHOT' 20 | 21 | testCompile 'org.uncommons:uncommons-maths:1.2' 22 | testCompile 'com.google.guava:guava:14.0.1' 23 | testCompile "org.hamcrest:hamcrest-library:1.3" 24 | testCompile "org.mockito:mockito-core:1.+" 25 | testCompile 'junit:junit:4.12' 26 | } 27 | 28 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/FailureListener.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty; 2 | 3 | import rx.Scheduler; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** 8 | * A contract for taking actions upon detecting an unhealthy host. A failure listener instance is always associated with 9 | * a unique host, so any action taken on this listener will directly be applied to the associated host. 10 | */ 11 | public interface FailureListener { 12 | 13 | /** 14 | * This action will remove the host associated with this listener from the load balancing pool. 15 | */ 16 | void remove(); 17 | 18 | /** 19 | * This action quarantines the host associated with this listener from the load balancing pool, for the passed 20 | * {@code quarantineDuration}. The host will be added back to the load balancing pool after the quarantine duration 21 | * is elapsed. 22 | * 23 | * @param quarantineDuration Duration for keeping the host quarantined. 24 | * @param timeUnit Time unit for the duration. 25 | */ 26 | void quarantine(long quarantineDuration, TimeUnit timeUnit); 27 | 28 | /** 29 | * This action quarantines the host associated with this listener from the load balancing pool, for the passed 30 | * {@code quarantineDuration}. The host will be added back to the load balancing pool after the quarantine duration 31 | * is elapsed. 32 | * 33 | * @param quarantineDuration Duration for keeping the host quarantined. 34 | * @param timeUnit Time unit for the duration. 35 | * @param timerScheduler Scheduler to be used for the quarantine duration timer. 36 | */ 37 | void quarantine(long quarantineDuration, TimeUnit timeUnit, Scheduler timerScheduler); 38 | } 39 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/internal/AbstractLoadBalancer.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.internal; 2 | 3 | import io.reactivex.netty.channel.Connection; 4 | import io.reactivex.netty.client.ConnectionFactory; 5 | import io.reactivex.netty.client.ConnectionObservable; 6 | import io.reactivex.netty.client.ConnectionObservable.AbstractOnSubscribeFunc; 7 | import io.reactivex.netty.client.ConnectionProvider; 8 | import io.reactivex.netty.client.pool.PooledConnectionProvider; 9 | import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; 10 | import netflix.ocelli.Instance; 11 | import netflix.ocelli.LoadBalancerStrategy; 12 | import netflix.ocelli.rxnetty.FailureListener; 13 | import rx.Observable; 14 | import rx.Subscriber; 15 | import rx.functions.Action1; 16 | import rx.functions.Func1; 17 | 18 | import java.net.SocketAddress; 19 | import java.util.List; 20 | import java.util.NoSuchElementException; 21 | 22 | /** 23 | * An abstract load balancer for all TCP based protocols. 24 | * 25 | *

Failure detection

26 | * 27 | * For every host that this load balancer connects, it provides a way to register a {@link TcpClientEventListener} 28 | * instance that can detect failures based on the various events received. Upon detecting the failure, an appropriate 29 | * action can be taken for the host, using the provided {@link FailureListener}. 30 | * 31 | *

Use with RxNetty clients

32 | * 33 | * In order to use this load balancer with RxNetty clients, one has to convert it to an instance of 34 | * {@link ConnectionProvider} by calling {@link #toConnectionProvider()} 35 | * 36 | * @param Type of Objects written on the connections created by this load balancer. 37 | * @param Type of Objects read from the connections created by this load balancer. 38 | */ 39 | public abstract class AbstractLoadBalancer { 40 | 41 | protected final Observable> hosts; 42 | protected final LoadBalancerStrategy> loadBalancer; 43 | protected final Func1 eventListenerFactory; 44 | 45 | protected AbstractLoadBalancer(Observable> hosts, 46 | Func1 eventListenerFactory, 47 | LoadBalancerStrategy> loadBalancer) { 48 | this.hosts = hosts; 49 | this.eventListenerFactory = eventListenerFactory; 50 | this.loadBalancer = loadBalancer; 51 | } 52 | 53 | /** 54 | * Converts this load balancer to a {@link ConnectionProvider} to be used with RxNetty clients. 55 | * 56 | * @return {@link ConnectionProvider} for this load balancer. 57 | */ 58 | public ConnectionProvider toConnectionProvider() { 59 | return ConnectionProvider.create(new Func1, ConnectionProvider>() { 60 | @Override 61 | public ConnectionProvider call(final ConnectionFactory connectionFactory) { 62 | return toConnectionProvider(connectionFactory); 63 | } 64 | }); 65 | } 66 | 67 | /*Visible for testing*/ ConnectionProvider toConnectionProvider(final ConnectionFactory factory) { 68 | 69 | final Observable>> providerStream = 70 | hosts.map(new Func1, Instance>>() { 71 | @Override 72 | public Instance> call(final Instance host) { 73 | final ConnectionProvider pcp = newConnectionProviderForHost(host, factory); 74 | 75 | return new Instance>() { 76 | @Override 77 | public Observable getLifecycle() { 78 | return host.getLifecycle(); 79 | } 80 | 81 | @Override 82 | public ConnectionProvider getValue() { 83 | return pcp; 84 | } 85 | }; 86 | } 87 | }); 88 | 89 | return new LoadBalancingProvider(factory, providerStream); 90 | } 91 | 92 | protected ConnectionProvider newConnectionProviderForHost(Instance host, 93 | ConnectionFactory connectionFactory) { 94 | /* 95 | * Bounds on the concurrency (concurrent connections) should be enforced at the request 96 | * processing level, providing a bound on number of connections is a difficult number 97 | * to determine. 98 | */ 99 | return PooledConnectionProvider.createUnbounded(connectionFactory, host.getValue()); 100 | } 101 | 102 | /*Visible for testing*/class LoadBalancingProvider extends ConnectionProvider { 103 | 104 | private final HostHolder hostHolder; 105 | 106 | public LoadBalancingProvider(ConnectionFactory connectionFactory, 107 | Observable>> providerStream) { 108 | super(connectionFactory); 109 | hostHolder = new HostHolder<>(providerStream, eventListenerFactory); 110 | } 111 | 112 | @Override 113 | public ConnectionObservable nextConnection() { 114 | 115 | return ConnectionObservable.createNew(new AbstractOnSubscribeFunc() { 116 | @Override 117 | protected void doSubscribe(Subscriber> sub, 118 | Action1> subscribeAllListenersAction) { 119 | final List> providers = hostHolder.getProviders(); 120 | 121 | if (null == providers || providers.isEmpty()) { 122 | sub.onError(new NoSuchElementException("No hosts available.")); 123 | } 124 | 125 | HostConnectionProvider hcp = loadBalancer.choose(providers); 126 | 127 | ConnectionObservable nextConnection = hcp.getProvider().nextConnection(); 128 | if (hcp.getEventsListener() != null) { 129 | nextConnection.subscribeForEvents(hcp.getEventsListener()); 130 | } 131 | subscribeAllListenersAction.call(nextConnection); 132 | nextConnection.unsafeSubscribe(sub); 133 | } 134 | }); 135 | } 136 | 137 | @Override 138 | protected Observable doShutdown() { 139 | return hostHolder.shutdown(); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/internal/HostCollector.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.internal; 2 | 3 | import io.reactivex.netty.client.ConnectionProvider; 4 | import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; 5 | import netflix.ocelli.Instance; 6 | import netflix.ocelli.rxnetty.FailureListener; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import rx.Observable; 10 | import rx.Observable.Operator; 11 | import rx.Scheduler; 12 | import rx.Subscriber; 13 | import rx.functions.Action0; 14 | import rx.functions.Action1; 15 | import rx.functions.Actions; 16 | import rx.functions.Func1; 17 | import rx.schedulers.Schedulers; 18 | 19 | import java.util.List; 20 | import java.util.concurrent.CopyOnWriteArrayList; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | class HostCollector implements Operator>, Instance>> { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(HostCollector.class); 26 | 27 | protected final CopyOnWriteArrayList> currentHosts = new CopyOnWriteArrayList<>(); 28 | private final Func1 eventListenerFactory; 29 | 30 | HostCollector(Func1 eventListenerFactory) { 31 | this.eventListenerFactory = eventListenerFactory; 32 | } 33 | 34 | @Override 35 | public Subscriber>> 36 | call(final Subscriber>> o) { 37 | 38 | return new Subscriber>>() { 39 | @Override 40 | public void onCompleted() { 41 | o.onCompleted(); 42 | } 43 | 44 | @Override 45 | public void onError(Throwable e) { 46 | o.onError(e); 47 | } 48 | 49 | @Override 50 | public void onNext(Instance> i) { 51 | 52 | final ConnectionProvider provider = i.getValue(); 53 | 54 | final TcpClientEventListener listener = eventListenerFactory.call(newFailureListener(provider, o)); 55 | 56 | final HostConnectionProvider hcp = new HostConnectionProvider<>(i.getValue(), listener); 57 | 58 | addHost(hcp, o); 59 | 60 | bindToInstanceLifecycle(i, hcp, o); 61 | } 62 | }; 63 | } 64 | 65 | protected void removeHost(HostConnectionProvider toRemove, 66 | Subscriber>> hostListListener) { 67 | /*It's a copy-on-write list, so removal makes a copy with no interference to reads*/ 68 | currentHosts.remove(toRemove); 69 | hostListListener.onNext(currentHosts); 70 | } 71 | 72 | protected void addHost(HostConnectionProvider toAdd, 73 | Subscriber>> hostListListener) { 74 | /*It's a copy-on-write list, so addition makes a copy with no interference to reads*/ 75 | currentHosts.add(toAdd); 76 | hostListListener.onNext(currentHosts); 77 | } 78 | 79 | protected FailureListener newFailureListener(final ConnectionProvider provider, 80 | final Subscriber>> hostListListener) { 81 | 82 | return new FailureListener() { 83 | @Override 84 | public void remove() { 85 | HostConnectionProvider.removeFrom(currentHosts, provider); 86 | hostListListener.onNext(currentHosts); 87 | } 88 | 89 | @Override 90 | public void quarantine(long quarantineDuration, TimeUnit timeUnit) { 91 | quarantine(quarantineDuration, timeUnit, Schedulers.computation()); 92 | } 93 | 94 | @Override 95 | public void quarantine(long quarantineDuration, TimeUnit timeUnit, Scheduler timerScheduler) { 96 | final FailureListener fl = this; 97 | remove(); 98 | Observable.timer(quarantineDuration, timeUnit, timerScheduler) 99 | .subscribe(new Action1() { 100 | @Override 101 | public void call(Long aLong) { 102 | TcpClientEventListener listener = eventListenerFactory.call(fl); 103 | addHost(new HostConnectionProvider(provider, listener), hostListListener); 104 | } 105 | }, new Action1() { 106 | @Override 107 | public void call(Throwable throwable) { 108 | logger.error("Error while adding back a quarantine instance to the load balancer.", 109 | throwable); 110 | } 111 | }); 112 | } 113 | }; 114 | } 115 | 116 | protected void bindToInstanceLifecycle(Instance> i, 117 | final HostConnectionProvider hcp, 118 | final Subscriber>> o) { 119 | i.getLifecycle() 120 | .finallyDo(new Action0() { 121 | @Override 122 | public void call() { 123 | removeHost(hcp, o); 124 | } 125 | }) 126 | .subscribe(Actions.empty(), new Action1() { 127 | @Override 128 | public void call(Throwable throwable) { 129 | // Do nothing as finallyDo takes care of both complete and error. 130 | } 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/internal/HostConnectionProvider.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.internal; 2 | 3 | import io.reactivex.netty.client.ConnectionProvider; 4 | import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; 5 | 6 | import java.util.Collection; 7 | 8 | public class HostConnectionProvider { 9 | 10 | private final ConnectionProvider provider; 11 | private final TcpClientEventListener eventsListener; 12 | 13 | private HostConnectionProvider(ConnectionProvider provider) { 14 | this(provider, null); 15 | } 16 | 17 | HostConnectionProvider(ConnectionProvider provider, TcpClientEventListener eventsListener) { 18 | this.provider = provider; 19 | this.eventsListener = eventsListener; 20 | } 21 | 22 | public static boolean removeFrom(Collection> c, 23 | ConnectionProvider toRemove) { 24 | return c.remove(new HostConnectionProvider(toRemove)); 25 | } 26 | 27 | public ConnectionProvider getProvider() { 28 | return provider; 29 | } 30 | 31 | public TcpClientEventListener getEventsListener() { 32 | return eventsListener; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object o) { 37 | if (this == o) { 38 | return true; 39 | } 40 | if (!(o instanceof HostConnectionProvider)) { 41 | return false; 42 | } 43 | 44 | @SuppressWarnings("unchecked") 45 | HostConnectionProvider that = (HostConnectionProvider) o; 46 | 47 | if (provider != null? !provider.equals(that.provider) : that.provider != null) { 48 | return false; 49 | } 50 | 51 | return true; 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return provider != null? provider.hashCode() : 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/internal/HostHolder.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.internal; 2 | 3 | import io.reactivex.netty.client.ConnectionProvider; 4 | import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; 5 | import netflix.ocelli.Instance; 6 | import netflix.ocelli.rxnetty.FailureListener; 7 | import rx.Observable; 8 | import rx.Observable.OnSubscribe; 9 | import rx.Subscriber; 10 | import rx.Subscription; 11 | import rx.functions.Action1; 12 | import rx.functions.Func1; 13 | 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | class HostHolder { 18 | 19 | private final Observable>> providerStream; 20 | private volatile List> providers; 21 | private Subscription streamSubscription; 22 | 23 | HostHolder(Observable>> providerStream, 24 | final Func1 eventListenerFactory) { 25 | this.providerStream = providerStream.lift(new HostCollector(eventListenerFactory)) 26 | .serialize()/*Host collector emits concurrently*/; 27 | providers = Collections.emptyList(); 28 | subscribeToHostStream(); 29 | } 30 | 31 | List> getProviders() { 32 | return providers; 33 | } 34 | 35 | private void subscribeToHostStream() { 36 | streamSubscription = providerStream.subscribe(new Action1>>() { 37 | @Override 38 | public void call(List> hostConnectionProviders) { 39 | providers = hostConnectionProviders; 40 | } 41 | }); 42 | } 43 | 44 | public Observable shutdown() { 45 | return Observable.create(new OnSubscribe() { 46 | @Override 47 | public void call(Subscriber subscriber) { 48 | if (null != streamSubscription) { 49 | streamSubscription.unsubscribe(); 50 | } 51 | 52 | subscriber.onCompleted(); 53 | } 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/protocol/WeightAware.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.protocol; 2 | 3 | import io.reactivex.netty.client.ConnectionProvider; 4 | import netflix.ocelli.loadbalancer.RandomWeightedLoadBalancer; 5 | 6 | /** 7 | * A property for RxNetty listeners that are used to also define weights for a particular host, typically in a 8 | * {@link RandomWeightedLoadBalancer} 9 | */ 10 | public interface WeightAware { 11 | 12 | /** 13 | * Returns the current weight of the associated host with this object. 14 | * This method will be called every time {@link ConnectionProvider#nextConnection()} is called for every active 15 | * hosts, so it is recommended to not do any costly processing in this method, it should typically be a lookup of 16 | * an already calculated value. 17 | * 18 | * @return The current weight. 19 | */ 20 | int getWeight(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/protocol/WeightComparator.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.protocol; 2 | 3 | import netflix.ocelli.rxnetty.internal.HostConnectionProvider; 4 | 5 | import java.util.Comparator; 6 | 7 | /** 8 | * A comparator for {@link WeightAware} 9 | */ 10 | public class WeightComparator implements Comparator> { 11 | @Override 12 | public int compare(HostConnectionProvider cp1, HostConnectionProvider cp2) { 13 | WeightAware wa1 = (WeightAware) cp1.getEventsListener(); 14 | WeightAware wa2 = (WeightAware) cp2.getEventsListener(); 15 | return wa1.getWeight() > wa2.getWeight() ? 1 : -1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/protocol/http/WeightedHttpClientListener.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.protocol.http; 2 | 3 | import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; 4 | import netflix.ocelli.rxnetty.protocol.WeightAware; 5 | 6 | /** 7 | * An {@link HttpClientEventsListener} contract with an additional property defined by {@link WeightAware} 8 | */ 9 | public abstract class WeightedHttpClientListener extends HttpClientEventsListener implements WeightAware { 10 | } 11 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/protocol/tcp/ComparableTcpClientListener.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.protocol.tcp; 2 | 3 | import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; 4 | import netflix.ocelli.loadbalancer.ChoiceOfTwoLoadBalancer; 5 | 6 | /** 7 | * An {@link TcpClientEventListener} contract which is also a {@link Comparable}. These listeners are typically used 8 | * with a load balancer that chooses the best among two servers, eg: {@link ChoiceOfTwoLoadBalancer} 9 | */ 10 | public abstract class ComparableTcpClientListener extends TcpClientEventListener 11 | implements Comparable { 12 | } 13 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/main/java/netflix/ocelli/rxnetty/protocol/tcp/WeightedTcpClientListener.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.protocol.tcp; 2 | 3 | import io.reactivex.netty.protocol.http.client.events.HttpClientEventsListener; 4 | import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; 5 | import netflix.ocelli.rxnetty.protocol.WeightAware; 6 | 7 | /** 8 | * An {@link TcpClientEventListener} contract with an additional property defined by {@link WeightAware} 9 | */ 10 | public abstract class WeightedTcpClientListener extends HttpClientEventsListener implements WeightAware { 11 | } 12 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/test/conf/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ocelli-rxnetty/src/test/java/netflix/ocelli/rxnetty/internal/AbstractLoadBalancerTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.internal; 2 | 3 | import io.reactivex.netty.client.ConnectionFactory; 4 | import io.reactivex.netty.client.ConnectionObservable; 5 | import io.reactivex.netty.client.ConnectionProvider; 6 | import netflix.ocelli.Instance; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.mockito.Mockito; 10 | 11 | import java.net.SocketAddress; 12 | import java.util.List; 13 | 14 | public class AbstractLoadBalancerTest { 15 | 16 | @Rule 17 | public final LoadBalancerRule lbRule = new LoadBalancerRule(); 18 | 19 | @Test(timeout = 60000) 20 | public void testRoundRobin() throws Exception { 21 | List> hosts = lbRule.setupDefault(); 22 | AbstractLoadBalancer loadBalancer = lbRule.getLoadBalancer(); 23 | ConnectionFactory cfMock = lbRule.newConnectionFactoryMock(); 24 | ConnectionProvider cp = loadBalancer.toConnectionProvider(cfMock); 25 | 26 | ConnectionObservable co = cp.nextConnection(); 27 | 28 | lbRule.connect(co); 29 | Mockito.verify(cfMock).newConnection(hosts.get(0).getValue()); 30 | Mockito.verifyNoMoreInteractions(cfMock); 31 | 32 | cp = loadBalancer.toConnectionProvider(cfMock); 33 | co = cp.nextConnection(); 34 | lbRule.connect(co); 35 | Mockito.verify(cfMock).newConnection(hosts.get(1).getValue()); 36 | Mockito.verifyNoMoreInteractions(cfMock); 37 | } 38 | } -------------------------------------------------------------------------------- /ocelli-rxnetty/src/test/java/netflix/ocelli/rxnetty/internal/LoadBalancingProviderTest.java: -------------------------------------------------------------------------------- 1 | package netflix.ocelli.rxnetty.internal; 2 | 3 | import io.reactivex.netty.channel.Connection; 4 | import io.reactivex.netty.client.ConnectionFactory; 5 | import io.reactivex.netty.client.ConnectionObservable; 6 | import io.reactivex.netty.client.ConnectionProvider; 7 | import io.reactivex.netty.protocol.tcp.client.events.TcpClientEventListener; 8 | import netflix.ocelli.Instance; 9 | import netflix.ocelli.rxnetty.FailureListener; 10 | import netflix.ocelli.rxnetty.internal.AbstractLoadBalancer.LoadBalancingProvider; 11 | import org.junit.Rule; 12 | import org.junit.Test; 13 | import org.mockito.verification.VerificationMode; 14 | import rx.Observable; 15 | import rx.functions.Func1; 16 | 17 | import java.net.InetSocketAddress; 18 | import java.net.SocketAddress; 19 | import java.util.List; 20 | import java.util.concurrent.atomic.AtomicBoolean; 21 | 22 | import static org.hamcrest.MatcherAssert.*; 23 | import static org.hamcrest.Matchers.*; 24 | import static org.mockito.Mockito.*; 25 | 26 | public class LoadBalancingProviderTest { 27 | 28 | @Rule 29 | public final LoadBalancerRule loadBalancerRule = new LoadBalancerRule(); 30 | 31 | @Test(timeout = 60000) 32 | public void testRoundRobin() throws Exception { 33 | List> hosts = loadBalancerRule.setupDefault(); 34 | 35 | assertThat("Unexpected hosts found.", hosts, hasSize(2)); 36 | 37 | AbstractLoadBalancer loadBalancer = loadBalancerRule.getLoadBalancer(); 38 | ConnectionFactory cfMock = loadBalancerRule.newConnectionFactoryMock(); 39 | 40 | Observable>> providers = 41 | loadBalancerRule.getHostsAsConnectionProviders(cfMock); 42 | 43 | LoadBalancingProvider lbProvider = newLoadBalancingProvider(loadBalancer, cfMock, providers); 44 | 45 | @SuppressWarnings("unchecked") 46 | ConnectionObservable connectionObservable = lbProvider.nextConnection(); 47 | 48 | assertNextConnection(hosts.get(0).getValue(), cfMock, connectionObservable, times(1)); 49 | 50 | assertNextConnection(hosts.get(1).getValue(), cfMock, connectionObservable, times(1)); 51 | 52 | assertNextConnection(hosts.get(0).getValue(), cfMock, connectionObservable, 53 | times(2) /*Invoked once above with same host*/); 54 | } 55 | 56 | @Test(timeout = 60000) 57 | public void testListenerSubscription() throws Exception { 58 | final AtomicBoolean listenerCalled = new AtomicBoolean(); 59 | final TcpClientEventListener listener = new TcpClientEventListener() { 60 | @Override 61 | public void onConnectionCloseStart() { 62 | listenerCalled.set(true); 63 | } 64 | }; 65 | InetSocketAddress host = new InetSocketAddress(0); 66 | loadBalancerRule.setup(new Func1() { 67 | @Override 68 | public TcpClientEventListener call(FailureListener failureListener) { 69 | return listener; 70 | } 71 | }, host); 72 | 73 | AbstractLoadBalancer loadBalancer = loadBalancerRule.getLoadBalancer(); 74 | ConnectionFactory cfMock = loadBalancerRule.newConnectionFactoryMock(); 75 | 76 | Observable>> providers = 77 | loadBalancerRule.getHostsAsConnectionProviders(cfMock); 78 | 79 | LoadBalancingProvider lbProvider = newLoadBalancingProvider(loadBalancer, cfMock, providers); 80 | 81 | @SuppressWarnings("unchecked") 82 | ConnectionObservable connectionObservable = lbProvider.nextConnection(); 83 | 84 | Connection c = assertNextConnection(host, cfMock, connectionObservable, times(1)); 85 | c.closeNow(); 86 | 87 | assertThat("Listener not called.", listenerCalled.get(), is(true)); 88 | } 89 | 90 | protected Connection assertNextConnection(SocketAddress host, 91 | ConnectionFactory cfMock, 92 | ConnectionObservable connectionObservable, 93 | VerificationMode verificationMode) { 94 | Connection c = loadBalancerRule.connect(connectionObservable); 95 | verify(cfMock, verificationMode).newConnection(host); 96 | return c; 97 | } 98 | 99 | protected LoadBalancingProvider newLoadBalancingProvider(AbstractLoadBalancer loadBalancer, 100 | ConnectionFactory cfMock, 101 | Observable>> providers) { 102 | LoadBalancingProvider lbProvider = loadBalancer.new LoadBalancingProvider(cfMock, providers); 103 | return lbProvider; 104 | } 105 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='ocelli' 2 | include 'ocelli-core' 3 | include 'ocelli-eureka' 4 | include 'ocelli-eureka2' 5 | include 'ocelli-rxnetty' 6 | include 'ocelli-examples' --------------------------------------------------------------------------------