├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── cd ├── deploy.sh ├── mvnsettings.xml ├── tag.sh └── version.sh ├── gpg.asc.enc ├── images └── result.png ├── pom.xml └── src ├── main ├── java │ └── technology │ │ └── dice │ │ └── dicefairlink │ │ ├── AuroraReadonlyEndpoint.java │ │ ├── ParsedUrl.java │ │ ├── config │ │ ├── AwsApiDiscoveryAuthMode.java │ │ ├── FairlinkConfiguration.java │ │ └── ReplicasDiscoveryMode.java │ │ ├── discovery │ │ ├── members │ │ │ ├── ClusterInfo.java │ │ │ ├── FairlinkMemberFinder.java │ │ │ ├── JdbcConnectionValidator.java │ │ │ ├── MemberFinder.java │ │ │ ├── MemberFinderMethod.java │ │ │ ├── ReplicaValidator.java │ │ │ ├── awsapi │ │ │ │ └── AwsApiReplicasFinder.java │ │ │ └── sql │ │ │ │ ├── DatabaseInstance.java │ │ │ │ ├── DatabaseInstanceRole.java │ │ │ │ ├── MySQLReplicasFinder.java │ │ │ │ └── PostgresSQLReplicasFinder.java │ │ └── tags │ │ │ ├── ExclusionTag.java │ │ │ ├── TagFilter.java │ │ │ └── awsapi │ │ │ └── ResourceGroupApiTagDiscovery.java │ │ ├── driver │ │ ├── AuroraReadReplicasDriver.java │ │ ├── Constants.java │ │ └── FairlinkConnectionString.java │ │ └── iterators │ │ ├── CyclicIterator.java │ │ ├── RandomisedCyclicIterator.java │ │ └── SizedIterator.java └── resources │ └── META-INF │ └── services │ └── java.sql.Driver └── test ├── bash └── readReplicasOnLoop.bash ├── java └── technology │ └── dice │ └── dicefairlink │ ├── AuroraReadonlyEndpointTest.java │ ├── StepByStepExecutor.java │ ├── config │ └── FairlinkConfigurationTest.java │ ├── discovery │ ├── members │ │ ├── ClusterInfoTest.java │ │ ├── FairlinkMemberFinderTest.java │ │ ├── JdbcConnectionValidatorTest.java │ │ ├── awsapi │ │ │ ├── AwsApiReplicasFinderTest.java │ │ │ └── AwsApiReplicasFinderTestIT.java │ │ └── sql │ │ │ ├── MySqlReplicasFinderTest.java │ │ │ └── PostgresSqlReplicaFinderTest.java │ └── tags │ │ └── awsapi │ │ ├── ResourceGroupApiResponse.java │ │ ├── ResourceGroupApiTagDiscoveryTest.java │ │ └── ResourceGroupApiTagDiscoveryTestIT.java │ ├── driver │ ├── AuroraReadReplicasDriverEndToEndTest.java │ ├── AuroraReadReplicasDriverTest.java │ ├── Data.java │ └── FairlinkConnectionStringTest.java │ ├── iterators │ └── RandomisedCyclicIteratorTest.java │ └── support │ ├── discovery │ ├── members │ │ ├── FailingReplicasFinder.java │ │ ├── FixedMemberFinder.java │ │ └── FixedSetReplicasFinder.java │ └── tags │ │ ├── FailingExcludedReplicasFinder.java │ │ └── FixedSetExcludedReplicasFinder.java │ ├── driver │ └── TestDriver.java │ └── iterators │ └── TestCyclicIterator.java └── resources ├── logback.xml └── technology └── dice └── dicefairlink └── discovery └── members ├── awsapi ├── allAvailableDbInstances.xml ├── emptyResponse.xml ├── oneReplicaDeletingInstances.xml ├── withMembers.xml └── withoutMembers.xml └── sql ├── mysql ├── 2replicas.sql ├── 3replicasPorts.sql ├── masterData.sql ├── schema.sql ├── slave1Data.sql └── slave2Data.sql └── postgresql ├── 2replicas.sql ├── drop_function.sql └── schema.sql /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Git repo (with tags) 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - name: Cache Maven packages 19 | uses: actions/cache@v3 20 | with: 21 | key: ${{ runner.os }}-1 22 | path: | 23 | ~/.m2/repository 24 | - name: Set up JDK 8 25 | uses: actions/setup-java@v3 26 | with: 27 | java-version: '8' 28 | distribution: 'temurin' 29 | - name: Build with Maven 30 | run: mvn --batch-mode --update-snapshots install 31 | - name: Run Codecov 32 | uses: codecov/codecov-action@v3 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | - name: Set version, sign artifacts and deploy to Maven Central 36 | if: ${{ github.ref == 'refs/heads/master' && github.repository == 'DiceTechnology/dice-fairlink' }} 37 | run: cd/deploy.sh 38 | env: 39 | GPG_KEY: ${{ secrets.GPG_KEY }} 40 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 41 | CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} 42 | CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN }} 43 | SLACK_URL: ${{ secrets.SLACK_URL }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Package Files # 2 | *.jar 3 | *.war 4 | *.nar 5 | *.ear 6 | *.zip 7 | *.tar.gz 8 | *.rar 9 | 10 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 11 | hs_err_pid* 12 | *.class 13 | *.og 14 | target/ 15 | .idea/ 16 | *.iml 17 | .DS_Store 18 | *.mmdb 19 | /nbproject/ 20 | /mavenproject1/nbproject/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dice Technology 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cd/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set +x 4 | echo $GPG_KEY | base64 --decode | gpg --batch --fast-import 5 | set -x 6 | 7 | ./cd/version.sh && \ 8 | ./cd/tag.sh && \ 9 | mvn deploy -P publish -DskipTests=true --settings cd/mvnsettings.xml 10 | -------------------------------------------------------------------------------- /cd/mvnsettings.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | central 5 | ${env.CENTRAL_USERNAME} 6 | ${env.CENTRAL_TOKEN} 7 | 8 | 9 | 10 | 11 | central 12 | 13 | true 14 | 15 | 16 | gpg 17 | 690636EA9C9CEBE2 18 | ${env.GPG_PASSPHRASE} 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /cd/tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to tag the GIT repository with a specific version taken from the POM file 3 | 4 | set -x 5 | 6 | function slack { 7 | local PAYLOAD="{\"text\":\"$1\"}" 8 | echo Sending message to slack 9 | set +x 10 | curl -o /dev/null -s -w "%{http_code}\n" -X POST -H 'Content-type: application/json' --data "$PAYLOAD" $SLACK_URL 11 | set -x 12 | } 13 | 14 | # Get VERSION from top level POM 15 | VERSION_POM=$( mvn help:evaluate -Dexpression=project.version | grep -v '\[.*' | tail -n1 ) 16 | 17 | # Get ARTIFACT_ID from top level POM 18 | ARTIFACT_ID_POM=$( mvn help:evaluate -Dexpression=project.artifactId | grep -v '\[.*' | tail -n1 ) 19 | 20 | # Setup Git Configuration 21 | git config --global user.email "build@dice.technology" 22 | git config --global user.name "DiceTech CI" 23 | 24 | git tag "${VERSION_POM}" -m "[GH] Released ${VERSION_POM}" 2>/dev/null && \ 25 | git push origin --tags 2>/dev/null && \ 26 | echo "Tagged $ARTIFACT_ID_POM with version $VERSION_POM" && \ 27 | slack "Tagged $ARTIFACT_ID_POM with version $VERSION_POM" 28 | -------------------------------------------------------------------------------- /cd/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to create a semver version as MAJOR.MINOR.PATCH (https://semver.org/) where: 3 | # - MAJOR.MINOR is taken from top level POM 4 | # - PATCH is different for each branch: 5 | # - master branch, will be based on latest tag like MAJOR.MINOR.PREVIOUS_PATCH and return PREVIOUS_PATCH + 1 6 | # - other branches, will be the Git Hash 7 | 8 | set -x 9 | 10 | # Get PATCH from git latest tag starting with major.minor, otherwise 0 11 | function getPatchFromGitTag { 12 | local VERSION_TAG=$( git tag -l "${MAJOR}.${MINOR}*" | sort -t . -k 3 -g | tail -n1 ) 13 | local VERSION_TAG_BITS=(${VERSION_TAG//./ }) 14 | 15 | local PATCH=0 16 | if [ ! -z "${VERSION_TAG_BITS[2]}" ] 17 | then 18 | PATCH=$((${VERSION_TAG_BITS[2]} + 1)) 19 | fi 20 | 21 | echo "$PATCH" 22 | } 23 | 24 | function getPatchFromGitHash { 25 | local GIT_HASH=$( git rev-parse HEAD | cut -c 1-7 ) 26 | local PATCH="0-$GIT_HASH" 27 | echo "$PATCH" 28 | } 29 | 30 | # Get MAJOR and MINOR from top level POM 31 | VERSION_POM=$( mvn help:evaluate -Dexpression=project.version | grep -v '\[.*' | tail -n1 ) 32 | VERSION_POM_BITS=(${VERSION_POM//./ }) 33 | 34 | MAJOR=${VERSION_POM_BITS[0]} 35 | MINOR=${VERSION_POM_BITS[1]} 36 | PATCH=$(getPatchFromGitTag) 37 | VERSION="${MAJOR}.${MINOR}.${PATCH}" 38 | 39 | echo "Setting version to ${VERSION}" 40 | echo "NEW_VERSION=${VERSION}" >> $GITHUB_ENV 41 | 42 | # Set the new version in POM 43 | mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${VERSION} 44 | -------------------------------------------------------------------------------- /gpg.asc.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiceTechnology/dice-fairlink/147eb658581766876dba9d47d44757a5553d7f1a/gpg.asc.enc -------------------------------------------------------------------------------- /images/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiceTechnology/dice-fairlink/147eb658581766876dba9d47d44757a5553d7f1a/images/result.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | 8 | technology.dice.open 9 | dice-fairlink 10 | 2.1.0 11 | dice-fairlink 12 | 13 | jar 14 | 15 | dice-fairlink is a JDBC driver design to evenly connect to the read replicas of an 16 | AWS Aurora cluster 17 | 18 | http://open.dice.technology/dice-fairlink 19 | 20 | 21 | scm:git:git@github.com:DiceTechnology/dice-fairlink.git 22 | scm:git:ssh://github.com:DiceTechnology/dice-fairlink.git 23 | 24 | http://github.com/DiceTechnology/dice-fairlink/tree/master 25 | 26 | 27 | 28 | 29 | MIT License 30 | http://www.opensource.org/licenses/mit-license.php 31 | repo 32 | 33 | 34 | 35 | 36 | 37 | Goncalo Luiz 38 | gedl 39 | gluiz at endeavorco dot com 40 | DiceTechnology 41 | http://open.dice.technology 42 | 43 | 44 | Andrey Lebedenko 45 | AndreyLebedenko 46 | andrey dot lebedenko at gmail dot com 47 | DiceTechnology 48 | http://open.dice.technology 49 | 50 | 51 | 52 | 53 | UTF-8 54 | 1.8 55 | 1.8 56 | 57 | 58 | 59 | 60 | software.amazon.awssdk 61 | rds 62 | 2.20.3 63 | provided 64 | 65 | 66 | software.amazon.awssdk 67 | resourcegroupstaggingapi 68 | 2.20.3 69 | provided 70 | 71 | 72 | junit 73 | junit 74 | 4.13.1 75 | test 76 | 77 | 78 | org.assertj 79 | assertj-core 80 | 3.9.1 81 | test 82 | 83 | 84 | org.powermock 85 | powermock-module-junit4 86 | 1.7.3 87 | test 88 | 89 | 90 | org.powermock 91 | powermock-api-mockito2 92 | 1.7.3 93 | test 94 | 95 | 96 | org.easymock 97 | easymock 98 | 3.6 99 | test 100 | 101 | 102 | org.powermock 103 | powermock-api-easymock 104 | 1.7.3 105 | test 106 | 107 | 108 | com.google.guava 109 | guava 110 | 33.2.1-jre 111 | test 112 | 113 | 114 | org.jacoco 115 | jacoco-maven-plugin 116 | 0.8.8 117 | test 118 | 119 | 120 | org.testcontainers 121 | testcontainers 122 | 1.21.0 123 | test 124 | 125 | 126 | org.testcontainers 127 | mysql 128 | 1.21.0 129 | test 130 | 131 | 132 | mysql 133 | mysql-connector-java 134 | 8.0.28 135 | test 136 | 137 | 138 | org.testcontainers 139 | postgresql 140 | 1.21.0 141 | test 142 | 143 | 144 | org.postgresql 145 | postgresql 146 | 42.7.2 147 | test 148 | 149 | 150 | org.slf4j 151 | jul-to-slf4j 152 | 1.7.28 153 | test 154 | 155 | 156 | org.slf4j 157 | slf4j-api 158 | 1.7.28 159 | test 160 | 161 | 162 | ch.qos.logback 163 | logback-classic 164 | 1.2.13 165 | test 166 | 167 | 168 | com.github.tomakehurst 169 | wiremock-jre8 170 | 2.35.1 171 | test 172 | 173 | 174 | 175 | 176 | 177 | 178 | org.kuali.maven.wagons 179 | maven-s3-wagon 180 | 1.2.1 181 | 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-source-plugin 187 | 2.4 188 | 189 | 190 | attach-sources 191 | 192 | jar-no-fork 193 | 194 | 195 | 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-javadoc-plugin 200 | 2.10.3 201 | 202 | 203 | **/protobuf/*.java 204 | 205 | 206 | 207 | 208 | attach-javadocs 209 | 210 | jar 211 | 212 | 213 | 214 | 215 | 216 | org.jacoco 217 | jacoco-maven-plugin 218 | 0.8.4 219 | 220 | file 221 | true 222 | 223 | 224 | 225 | default-prepare-agent 226 | 227 | prepare-agent 228 | 229 | 230 | 231 | default-report 232 | prepare-package 233 | 234 | report 235 | 236 | 237 | 238 | default-check 239 | test 240 | 241 | check 242 | report 243 | 244 | 245 | 246 | 247 | 248 | BUNDLE 249 | 250 | 251 | 252 | COMPLEXITY 253 | COVEREDRATIO 254 | 0.80 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | maven-scm-plugin 265 | 2.0.0-M3 266 | 267 | ${project.version} 268 | 269 | 270 | 271 | org.apache.maven.plugins 272 | maven-compiler-plugin 273 | 3.6.0 274 | 275 | 1.8 276 | 1.8 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | publish 285 | 286 | 287 | 288 | org.apache.maven.plugins 289 | maven-gpg-plugin 290 | 3.0.1 291 | 292 | 293 | sign-artifacts 294 | verify 295 | 296 | sign 297 | 298 | 299 | 300 | 301 | 302 | org.sonatype.central 303 | central-publishing-maven-plugin 304 | 0.7.0 305 | true 306 | 307 | central 308 | true 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/AuroraReadonlyEndpoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink; 7 | 8 | import technology.dice.dicefairlink.config.FairlinkConfiguration; 9 | import technology.dice.dicefairlink.discovery.members.MemberFinder; 10 | import technology.dice.dicefairlink.iterators.SizedIterator; 11 | 12 | import java.time.Duration; 13 | import java.util.concurrent.ScheduledExecutorService; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.atomic.AtomicReference; 16 | import java.util.logging.Level; 17 | import java.util.logging.Logger; 18 | 19 | public class AuroraReadonlyEndpoint { 20 | private static final Logger LOGGER = Logger.getLogger(AuroraReadonlyEndpoint.class.getName()); 21 | private final MemberFinder fairlinkMemberFinder; 22 | private volatile SizedIterator replicas; 23 | private final AtomicReference lastReplica = new AtomicReference<>(); 24 | 25 | public AuroraReadonlyEndpoint( 26 | FairlinkConfiguration fairlinkConfiguration, 27 | MemberFinder fairlinkMemberFinder, 28 | ScheduledExecutorService replicaDiscoveryExecutor) { 29 | 30 | this.fairlinkMemberFinder = fairlinkMemberFinder; 31 | replicas = fairlinkMemberFinder.init(); 32 | final Duration startJitter = fairlinkConfiguration.randomBoundDelay(); 33 | LOGGER.log(Level.INFO, "Starting cluster member discovery with {0} delay.", startJitter); 34 | replicaDiscoveryExecutor.scheduleAtFixedRate( 35 | () -> replicas = fairlinkMemberFinder.discoverReplicas(), 36 | fairlinkConfiguration.getReplicaPollInterval().plus(startJitter).getSeconds(), 37 | fairlinkConfiguration.getReplicaPollInterval().getSeconds(), 38 | TimeUnit.SECONDS); 39 | } 40 | 41 | public String getNextReplica() { 42 | String nextReplica = replicas.next(); 43 | LOGGER.finer("Obtained replica: " + nextReplica); 44 | if (nextReplica != null && nextReplica.equals(lastReplica.get()) && replicas.size() > 1) { 45 | nextReplica = replicas.next(); 46 | } 47 | lastReplica.set(nextReplica); 48 | return nextReplica; 49 | } 50 | 51 | public void refresh() { 52 | replicas = this.fairlinkMemberFinder.discoverReplicas(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/ParsedUrl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink; 7 | 8 | public class ParsedUrl { 9 | private final String delegateProtocol; 10 | private final String delegateUrl; 11 | 12 | public ParsedUrl(String delegateProtocol, String delegateUrl) { 13 | this.delegateProtocol = delegateProtocol; 14 | this.delegateUrl = delegateUrl; 15 | } 16 | 17 | public String getDelegateProtocol() { 18 | return delegateProtocol; 19 | } 20 | 21 | public String getDelegateUrl() { 22 | return delegateUrl; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/config/AwsApiDiscoveryAuthMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.config; 7 | 8 | import java.util.Arrays; 9 | import java.util.Optional; 10 | 11 | public enum AwsApiDiscoveryAuthMode { 12 | BASIC, 13 | ENVIRONMENT, 14 | DEFAULT_CHAIN; 15 | 16 | public static Optional fromStringInsensitive(String candidate) { 17 | return Arrays.stream(AwsApiDiscoveryAuthMode.values()) 18 | .filter(mode -> mode.toString().equalsIgnoreCase(candidate)) 19 | .findAny(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/config/FairlinkConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.config; 7 | 8 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 9 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 10 | import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; 11 | import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; 12 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 13 | import software.amazon.awssdk.regions.Region; 14 | 15 | import java.time.Duration; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | import java.util.Properties; 19 | import java.util.concurrent.ThreadLocalRandom; 20 | import java.util.logging.Level; 21 | import java.util.logging.Logger; 22 | 23 | public class FairlinkConfiguration { 24 | private static final Logger LOGGER = Logger.getLogger(FairlinkConfiguration.class.getName()); 25 | private static final int MAX_START_DELAY = 10; 26 | public static final String AWS_AUTH_MODE_PROPERTY_NAME = "auroraDiscoveryAuthMode"; 27 | public static final String AWS_BASIC_CREDENTIALS_KEY = "auroraDiscoveryKeyId"; 28 | public static final String AWS_BASIC_CREDENTIALS_SECRET = "auroraDiscoverKeySecret"; 29 | public static final String REPLICA_POLL_INTERVAL_PROPERTY_NAME = "replicaPollInterval"; 30 | public static final String TAGS_INTERVAL_PROPERTY_NAME = "tagsPollInterval"; 31 | public static final String REPLICA_ENDPOINT_TEMPLATE = "replicaEndpointTemplate"; 32 | public static final String DISCOVERY_MODE_PROPERTY_NAME = "discoveryMode"; 33 | public static final String VALIDATE_CONNECTION = "validateConnection"; 34 | public static final String CLUSTER_REGION = "auroraClusterRegion"; 35 | public static final String FALLBACK_ENDPOINT = "fallbackEndpoint"; 36 | private static final Duration DEFAULT_POLLER_INTERVAL = Duration.ofSeconds(30); 37 | private static final Duration DEFAULT_TAG_POLL_INTERVAL = Duration.ofMinutes(2); 38 | private static final String MYSQL = "mysql"; 39 | private static final String POSTGRESQL = "postgresql"; 40 | private static final String AWS_ENDPOINT_OVERRIDE = "awsEndpointOverride"; 41 | private final Region auroraClusterRegion; 42 | private final Optional replicaEndpointTemplate; 43 | private final Optional fallbackEndpoint; 44 | private final Optional awsEndpointOverride; 45 | private final AwsCredentialsProvider awsCredentialsProvider; 46 | private final Duration replicaPollInterval; 47 | private final ReplicasDiscoveryMode replicasDiscoveryMode; 48 | private final Map env; 49 | private final boolean validateConnection; 50 | private final Duration tagsPollerInterval; 51 | 52 | public FairlinkConfiguration(Properties properties, Map env) { 53 | this.env = env; 54 | this.auroraClusterRegion = this.resolveRegion(properties); 55 | this.awsCredentialsProvider = this.awsAuth(properties); 56 | this.tagsPollerInterval = this.resolveTagPollerInterval(properties); 57 | this.replicaPollInterval = this.resolvePollerInterval(properties); 58 | this.replicasDiscoveryMode = this.resolveDiscoveryMode(properties); 59 | this.replicaEndpointTemplate = this.resolveReplicaEndpointTemplate(properties); 60 | this.validateConnection = this.resolveValidationConnection(properties); 61 | this.fallbackEndpoint = this.resolveFallbackEndpoint(properties); 62 | this.awsEndpointOverride = this.resolveAwsEndpointOverride(properties); 63 | this.validateConfiguration(); 64 | } 65 | 66 | private Optional resolveAwsEndpointOverride(Properties properties) { 67 | return Optional.ofNullable(properties.getProperty(AWS_ENDPOINT_OVERRIDE)); 68 | } 69 | 70 | private Optional resolveFallbackEndpoint(Properties properties) { 71 | return Optional.ofNullable(properties.getProperty(FALLBACK_ENDPOINT)); 72 | } 73 | 74 | private Optional resolveReplicaEndpointTemplate(Properties properties) { 75 | return Optional.ofNullable(properties.getProperty(REPLICA_ENDPOINT_TEMPLATE)); 76 | } 77 | 78 | private boolean resolveValidationConnection(Properties properties) { 79 | return Optional.ofNullable(Boolean.parseBoolean(properties.getProperty(VALIDATE_CONNECTION))) 80 | .orElse(Boolean.TRUE); 81 | } 82 | 83 | private void validateConfiguration() { 84 | if (this.replicasDiscoveryMode == ReplicasDiscoveryMode.AWS_API) { 85 | this.validateAwsApiDiscovery(); 86 | } else { 87 | this.validateSqlDiscovery(); 88 | } 89 | 90 | this.replicaEndpointTemplate.orElseThrow( 91 | () -> 92 | new IllegalStateException( 93 | "Replica endpoint template mandatory. It is used for tag exclusion discovery and if an SQL discovery mode is selected")); 94 | } 95 | 96 | private void validateSqlDiscovery() {} 97 | 98 | private void validateAwsApiDiscovery() {} 99 | 100 | private ReplicasDiscoveryMode resolveDiscoveryMode(Properties properties) { 101 | return ReplicasDiscoveryMode.fromStringInsensitive( 102 | properties.getProperty( 103 | DISCOVERY_MODE_PROPERTY_NAME, ReplicasDiscoveryMode.AWS_API.name())) 104 | .orElse(ReplicasDiscoveryMode.AWS_API); 105 | } 106 | 107 | private AwsCredentialsProvider awsAuth(Properties properties) { 108 | AwsApiDiscoveryAuthMode authMode = 109 | AwsApiDiscoveryAuthMode.fromStringInsensitive( 110 | properties.getProperty( 111 | AWS_AUTH_MODE_PROPERTY_NAME, AwsApiDiscoveryAuthMode.DEFAULT_CHAIN.name())) 112 | .orElse(AwsApiDiscoveryAuthMode.DEFAULT_CHAIN); 113 | LOGGER.log(Level.FINE, "authMode: {0}", authMode); 114 | switch (authMode) { 115 | case BASIC: 116 | String key = properties.getProperty(AWS_BASIC_CREDENTIALS_KEY); 117 | String secret = properties.getProperty(AWS_BASIC_CREDENTIALS_SECRET); 118 | if (key == null || secret == null) { 119 | throw new IllegalStateException( 120 | String.format( 121 | "For basic authentication both [%s] and [%s] must both be set", 122 | AWS_BASIC_CREDENTIALS_KEY, AWS_BASIC_CREDENTIALS_SECRET)); 123 | } 124 | return StaticCredentialsProvider.create(AwsBasicCredentials.create(key, secret)); 125 | case ENVIRONMENT: 126 | return EnvironmentVariableCredentialsProvider.create(); 127 | default: 128 | // DEFAULT_CHAIN 129 | return DefaultCredentialsProvider.create(); 130 | } 131 | } 132 | 133 | private Region resolveRegion(final Properties properties) { 134 | final Optional propertyRegion = 135 | Optional.ofNullable(properties.getProperty(CLUSTER_REGION)); 136 | LOGGER.log(Level.FINE, "Region from property: {0}", propertyRegion); 137 | return propertyRegion 138 | .map(r -> Region.of(r)) 139 | .orElseGet( 140 | () -> { 141 | final String envRegion = env.get("AWS_DEFAULT_REGION"); 142 | LOGGER.log(Level.FINE, "Region from environment: {0}", envRegion); 143 | if (envRegion != null) { 144 | return Region.of(envRegion); 145 | } 146 | throw new IllegalStateException( 147 | "Region is mandatory for exclusion tag discovery and replica discovery on AWS_API mode"); 148 | }); 149 | } 150 | 151 | private Duration resolvePollerInterval(Properties properties) { 152 | try { 153 | return Duration.ofSeconds( 154 | Integer.parseInt(properties.getProperty(REPLICA_POLL_INTERVAL_PROPERTY_NAME))); 155 | } catch (IllegalArgumentException | NullPointerException e) { 156 | LOGGER.warning( 157 | String.format( 158 | "No or invalid polling interval specified. Using default replica poll interval of %s", 159 | DEFAULT_POLLER_INTERVAL)); 160 | return DEFAULT_POLLER_INTERVAL; 161 | } 162 | } 163 | 164 | private Duration resolveTagPollerInterval(Properties properties) { 165 | try { 166 | return Duration.ofSeconds( 167 | Integer.parseInt(properties.getProperty(TAGS_INTERVAL_PROPERTY_NAME))); 168 | } catch (IllegalArgumentException | NullPointerException e) { 169 | LOGGER.warning( 170 | String.format( 171 | "No or invalid tags polling interval specified. Using default tags poll interval of %s", 172 | DEFAULT_TAG_POLL_INTERVAL)); 173 | return DEFAULT_TAG_POLL_INTERVAL; 174 | } 175 | } 176 | 177 | public Duration randomBoundDelay() { 178 | return Duration.ofMillis( 179 | new Float(ThreadLocalRandom.current().nextFloat() * MAX_START_DELAY * 1000).longValue()); 180 | } 181 | 182 | public Duration getReplicaPollInterval() { 183 | return replicaPollInterval; 184 | } 185 | 186 | public Duration getTagsPollerInterval() { 187 | return tagsPollerInterval; 188 | } 189 | 190 | public Optional getAwsEndpointOverride() { 191 | return awsEndpointOverride; 192 | } 193 | 194 | public AwsCredentialsProvider getAwsCredentialsProvider() { 195 | return awsCredentialsProvider; 196 | } 197 | 198 | public Optional getFallbackEndpoint() { 199 | return fallbackEndpoint; 200 | } 201 | 202 | public Region getAuroraClusterRegion() { 203 | return auroraClusterRegion; 204 | } 205 | 206 | public String hostname(String fromDbIdentifier) { 207 | return String.format(replicaEndpointTemplate.get(), fromDbIdentifier); 208 | } 209 | 210 | public boolean isValidateConnection() { 211 | return validateConnection; 212 | } 213 | 214 | public ReplicasDiscoveryMode getReplicasDiscoveryMode() { 215 | return replicasDiscoveryMode; 216 | } 217 | 218 | public boolean isDiscoveryModeValidForDelegate(String delegateProtocol) { 219 | return (this.getReplicasDiscoveryMode() != ReplicasDiscoveryMode.SQL_MYSQL || delegateProtocol.equalsIgnoreCase(MYSQL)) 220 | && (this.getReplicasDiscoveryMode() != ReplicasDiscoveryMode.SQL_POSTGRES || delegateProtocol.equalsIgnoreCase(POSTGRESQL)); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/config/ReplicasDiscoveryMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.config; 7 | 8 | import java.util.Arrays; 9 | import java.util.Optional; 10 | 11 | public enum ReplicasDiscoveryMode { 12 | AWS_API, 13 | SQL_MYSQL, 14 | SQL_POSTGRES; 15 | 16 | public static Optional fromStringInsensitive(String candidate) { 17 | return Arrays.stream(ReplicasDiscoveryMode.values()) 18 | .filter(mode -> mode.toString().equalsIgnoreCase(candidate)) 19 | .findAny(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/ClusterInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members; 7 | 8 | import java.util.Objects; 9 | import java.util.Set; 10 | 11 | public final class ClusterInfo { 12 | private final String readonlyEndpoint; 13 | private final Set replicas; 14 | 15 | public ClusterInfo(String readonlyEndpoint, Set replicas) { 16 | if ("".equals(readonlyEndpoint) || readonlyEndpoint == null) { 17 | throw new IllegalArgumentException("Read only endpoint must not be null"); 18 | } 19 | if (replicas == null) { 20 | throw new IllegalArgumentException("Set of replicas must not be mull"); 21 | } 22 | this.readonlyEndpoint = readonlyEndpoint; 23 | this.replicas = replicas; 24 | } 25 | 26 | public String getReadonlyEndpoint() { 27 | return readonlyEndpoint; 28 | } 29 | 30 | public Set getReplicas() { 31 | return replicas; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) { 37 | return true; 38 | } 39 | if (!(o instanceof ClusterInfo)) { 40 | return false; 41 | } 42 | ClusterInfo that = (ClusterInfo) o; 43 | return Objects.equals(getReadonlyEndpoint(), that.getReadonlyEndpoint()) 44 | && Objects.equals(getReplicas(), that.getReplicas()); 45 | } 46 | 47 | @Override 48 | public int hashCode() { 49 | return Objects.hash(getReadonlyEndpoint(), getReplicas()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/FairlinkMemberFinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members; 7 | 8 | import technology.dice.dicefairlink.config.FairlinkConfiguration; 9 | import technology.dice.dicefairlink.discovery.tags.ExclusionTag; 10 | import technology.dice.dicefairlink.discovery.tags.TagFilter; 11 | import technology.dice.dicefairlink.driver.FairlinkConnectionString; 12 | import technology.dice.dicefairlink.iterators.SizedIterator; 13 | 14 | import java.net.URISyntaxException; 15 | import java.time.Duration; 16 | import java.util.Collection; 17 | import java.util.Collections; 18 | import java.util.HashSet; 19 | import java.util.Optional; 20 | import java.util.Set; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.function.Function; 24 | import java.util.logging.Level; 25 | import java.util.logging.Logger; 26 | import java.util.stream.Collectors; 27 | 28 | public class FairlinkMemberFinder implements MemberFinder { 29 | private static final Logger LOGGER = Logger.getLogger(FairlinkMemberFinder.class.getName()); 30 | private static final ExclusionTag EXCLUSION_TAG = new ExclusionTag("Fairlink-Exclude", "true"); 31 | private static final Set EMPTY_SET = Collections.unmodifiableSet(new HashSet<>(0)); 32 | 33 | private final FairlinkConfiguration fairlinkConfiguration; 34 | private final MemberFinderMethod memberFinder; 35 | private final ReplicaValidator replicaValidator; 36 | private final Function, SizedIterator> iteratorBuilder; 37 | private volatile Optional fallbackEndpoint = Optional.empty(); 38 | private volatile Collection excludedInstanceIds = 39 | Collections.unmodifiableCollection(new HashSet<>(0)); 40 | protected final FairlinkConnectionString fairlinkConnectionString; 41 | protected final TagFilter tagFilter; 42 | 43 | public FairlinkMemberFinder( 44 | FairlinkConfiguration fairlinkConfiguration, 45 | FairlinkConnectionString fairlinkConnectionString, 46 | ScheduledExecutorService tagsPollingExecutor, 47 | TagFilter excludedInstancesFinder, 48 | MemberFinderMethod memberFinder, 49 | Function, SizedIterator> stringSizedIteratorBuilder, 50 | ReplicaValidator replicaValidator) { 51 | this.fairlinkConnectionString = fairlinkConnectionString; 52 | this.fairlinkConfiguration = fairlinkConfiguration; 53 | this.tagFilter = excludedInstancesFinder; 54 | this.memberFinder = memberFinder; 55 | this.replicaValidator = replicaValidator; 56 | this.iteratorBuilder = stringSizedIteratorBuilder; 57 | this.fallbackEndpoint = fairlinkConfiguration.getFallbackEndpoint(); 58 | final Duration startJitter = fairlinkConfiguration.randomBoundDelay(); 59 | LOGGER.info("Starting excluded members discovery with " + startJitter + " delay."); 60 | tagsPollingExecutor.scheduleAtFixedRate( 61 | () -> excludedInstanceIds = safeExclusionsDiscovery(), 62 | startJitter.getSeconds(), 63 | fairlinkConfiguration.getTagsPollerInterval().getSeconds(), 64 | TimeUnit.SECONDS); 65 | } 66 | 67 | private Set safeExclusionsDiscovery() { 68 | try { 69 | return tagFilter.listExcludedInstances(EXCLUSION_TAG); 70 | } catch (Exception e) { 71 | LOGGER.log( 72 | Level.SEVERE, "Could not discover exclusions; including all discovered instances", e); 73 | return EMPTY_SET; 74 | } 75 | } 76 | 77 | public final SizedIterator discoverReplicas() { 78 | try { 79 | long before = System.currentTimeMillis(); 80 | final ClusterInfo clusterInfo = this.memberFinder.discoverCluster(); 81 | this.fallbackEndpoint = 82 | Optional.of( 83 | fairlinkConfiguration 84 | .getFallbackEndpoint() 85 | .orElse(clusterInfo.getReadonlyEndpoint())); 86 | final Set filteredReplicas = 87 | clusterInfo.getReplicas().stream() 88 | .filter(db -> !excludedInstanceIds.contains(db)) 89 | .filter( 90 | dbIdentifier -> 91 | (!this.fairlinkConfiguration.isValidateConnection()) 92 | || this.validate(fairlinkConfiguration.hostname(dbIdentifier))) 93 | .map(fairlinkConfiguration::hostname) 94 | .collect(Collectors.toSet()); 95 | final SizedIterator result = 96 | filteredReplicas.isEmpty() 97 | ? this.iteratorBuilder.apply( 98 | this.setOf(this.fallbackEndpoint.orElse(clusterInfo.getReadonlyEndpoint()))) 99 | : this.iteratorBuilder.apply(filteredReplicas); 100 | long after = System.currentTimeMillis(); 101 | LOGGER.info( 102 | "Updated list of replicas in " 103 | + (after - before) 104 | + " ms. Found " 105 | + filteredReplicas.size() 106 | + " good, active, non-excluded replica" 107 | + (filteredReplicas.size() != 1 ? "s" : "") 108 | + " (validation " 109 | + (fairlinkConfiguration.isValidateConnection() ? "" : "NOT ") 110 | + "done). Excluded " 111 | + this.excludedInstanceIds.size() 112 | + " instance" 113 | + (excludedInstanceIds.size() != 1 ? "s" : "") 114 | + (this.excludedInstanceIds.size() != 1 ? "" : "s") 115 | + ". Next update in " 116 | + this.fairlinkConfiguration.getReplicaPollInterval()); 117 | return result; 118 | } catch (Exception e) { 119 | LOGGER.log( 120 | Level.WARNING, 121 | "Error discovering cluster identified by [" 122 | + this.fairlinkConnectionString.getFairlinkUri() 123 | + "]. Will return fallback endpoint " 124 | + this.fallbackEndpoint.orElse("N/A") 125 | + " if available", 126 | e); 127 | if (!this.fallbackEndpoint.isPresent()) { 128 | LOGGER.log( 129 | Level.SEVERE, 130 | "Fallback endpoint not available. This means the cluster has never been successfully discovered. This is probably a permanent error condition"); 131 | } 132 | return fallbackEndpoint 133 | .map(fallbackEndpoint -> this.iteratorBuilder.apply(this.setOf(fallbackEndpoint))) 134 | .orElseThrow( 135 | () -> new RuntimeException( 136 | "Could not discover cluster identified by [" 137 | + fairlinkConnectionString.getFairlinkUri() 138 | + "] and a fallback reader endpoint is not available")); 139 | } 140 | } 141 | 142 | private Set setOf(String entry) { 143 | Set set = new HashSet<>(1); 144 | set.add(entry); 145 | return Collections.unmodifiableSet(set); 146 | } 147 | 148 | private boolean validate(String host) { 149 | try { 150 | return this.replicaValidator.isValid( 151 | fairlinkConnectionString.delegateConnectionString(host), 152 | fairlinkConnectionString.getProperties()); 153 | } catch (URISyntaxException e) { 154 | return false; 155 | } 156 | } 157 | 158 | public final SizedIterator init() { 159 | this.excludedInstanceIds = safeExclusionsDiscovery(); 160 | final SizedIterator replicasIterator = this.discoverReplicas(); 161 | LOGGER.log( 162 | Level.INFO, 163 | String.format( 164 | "Initialised driver for cluster identified by [%s with [%d] replicas]. List will be refreshed every [%s]", 165 | fairlinkConnectionString.getHost(), 166 | replicasIterator.size(), 167 | fairlinkConfiguration.getReplicaPollInterval())); 168 | return replicasIterator; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/JdbcConnectionValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members; 7 | 8 | import java.sql.Connection; 9 | import java.sql.Driver; 10 | import java.util.Properties; 11 | 12 | public class JdbcConnectionValidator implements ReplicaValidator { 13 | private final Driver driver; 14 | 15 | public JdbcConnectionValidator(Driver driver) { 16 | this.driver = driver; 17 | } 18 | 19 | @Override 20 | public boolean isValid(String host, Properties properties) { 21 | try (Connection c = driver.connect(host, properties)) { 22 | c.createStatement().executeQuery("SELECT 1"); 23 | } catch (Exception e) { 24 | return false; 25 | } 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/MemberFinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members; 7 | 8 | import technology.dice.dicefairlink.iterators.SizedIterator; 9 | 10 | public interface MemberFinder { 11 | SizedIterator discoverReplicas(); 12 | 13 | default SizedIterator init() { 14 | return this.discoverReplicas(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/MemberFinderMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members; 7 | 8 | @FunctionalInterface 9 | public interface MemberFinderMethod { 10 | ClusterInfo discoverCluster(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/ReplicaValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members; 7 | 8 | import java.util.Properties; 9 | 10 | @FunctionalInterface 11 | public interface ReplicaValidator { 12 | boolean isValid(String host, Properties properties); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/awsapi/AwsApiReplicasFinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members.awsapi; 7 | 8 | import java.net.URI; 9 | import java.util.Collections; 10 | import java.util.HashSet; 11 | import java.util.Optional; 12 | import java.util.Set; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | import java.util.stream.Collectors; 16 | import software.amazon.awssdk.services.rds.RdsClient; 17 | import software.amazon.awssdk.services.rds.RdsClientBuilder; 18 | import software.amazon.awssdk.services.rds.model.DBCluster; 19 | import software.amazon.awssdk.services.rds.model.DBClusterMember; 20 | import software.amazon.awssdk.services.rds.model.DBInstance; 21 | import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; 22 | import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse; 23 | import software.amazon.awssdk.services.rds.model.DescribeDbInstancesRequest; 24 | import software.amazon.awssdk.services.rds.model.Filter; 25 | import software.amazon.awssdk.services.rds.paginators.DescribeDBInstancesIterable; 26 | import technology.dice.dicefairlink.config.FairlinkConfiguration; 27 | import technology.dice.dicefairlink.discovery.members.ClusterInfo; 28 | import technology.dice.dicefairlink.discovery.members.MemberFinderMethod; 29 | import technology.dice.dicefairlink.driver.FairlinkConnectionString; 30 | 31 | public class AwsApiReplicasFinder implements MemberFinderMethod { 32 | private static final Logger LOGGER = Logger.getLogger(AwsApiReplicasFinder.class.getName()); 33 | private static final String ACTIVE_STATUS = "available"; 34 | public static final String DB_CLUSTER_ID_FILTER = "db-cluster-id"; 35 | private static final Set EMPTY_SET = Collections.unmodifiableSet(new HashSet<>(0)); 36 | private final String clusterId; 37 | private final RdsClient client; 38 | 39 | public AwsApiReplicasFinder( 40 | FairlinkConfiguration fairlinkConfiguration, 41 | FairlinkConnectionString fairlinkConnectionString) { 42 | 43 | this.clusterId = fairlinkConnectionString.getHost(); 44 | LOGGER.log(Level.INFO, "Cluster ID: {0}", fairlinkConnectionString.getHost()); 45 | LOGGER.log(Level.INFO, "AWS Region: {0}", fairlinkConfiguration.getAuroraClusterRegion()); 46 | final RdsClientBuilder clientBuilder = 47 | RdsClient.builder() 48 | .region(fairlinkConfiguration.getAuroraClusterRegion()) 49 | .credentialsProvider(fairlinkConfiguration.getAwsCredentialsProvider()); 50 | fairlinkConfiguration 51 | .getAwsEndpointOverride() 52 | .ifPresent(o -> clientBuilder.endpointOverride(URI.create(o))); 53 | this.client = clientBuilder.build(); 54 | } 55 | 56 | private DBCluster describeCluster(String clusterId) { 57 | final DescribeDbClustersResponse describeDbClustersResponse = 58 | client.describeDBClusters( 59 | DescribeDbClustersRequest.builder().dbClusterIdentifier(clusterId).build()); 60 | return describeDbClustersResponse.dbClusters().stream() 61 | .findFirst() 62 | .orElseThrow( 63 | () -> 64 | new RuntimeException( 65 | String.format( 66 | "Could not find exactly one cluster with cluster id [%s]", clusterId))); 67 | } 68 | 69 | private Set replicaMembersOf(DBCluster cluster) { 70 | try { 71 | DescribeDbInstancesRequest request = 72 | DescribeDbInstancesRequest.builder() 73 | .filters( 74 | Filter.builder() 75 | .name(DB_CLUSTER_ID_FILTER) 76 | .values(cluster.dbClusterIdentifier()) 77 | .build()) 78 | .build(); 79 | final Optional writer = 80 | cluster.dbClusterMembers().stream().filter(member -> member.isClusterWriter()).findAny(); 81 | final DescribeDBInstancesIterable describeInstancesPaginator = 82 | client.describeDBInstancesPaginator(request); 83 | final Set replicaIds = 84 | describeInstancesPaginator.stream() 85 | .flatMap( 86 | pageOfInstances -> 87 | pageOfInstances.dbInstances().stream() 88 | .filter(AwsApiReplicasFinder::isActive) 89 | .filter(dbInstance -> isDbReader(writer, dbInstance)) 90 | .map(dbInstance -> dbInstance.dbInstanceIdentifier())) 91 | .collect(Collectors.toSet()); 92 | 93 | return replicaIds; 94 | 95 | } catch (Exception e) { 96 | LOGGER.log( 97 | Level.SEVERE, "Failed to list cluster replicas. Returning an empty set of replicas", e); 98 | return EMPTY_SET; 99 | } 100 | } 101 | 102 | private static boolean isDbReader(Optional writer, DBInstance dbInstance) { 103 | return !writer 104 | .map(w -> w.dbInstanceIdentifier().equalsIgnoreCase(dbInstance.dbInstanceIdentifier())) 105 | .orElse(false); 106 | } 107 | 108 | private static boolean isActive(DBInstance dbInstance) { 109 | return dbInstance.dbInstanceStatus().equalsIgnoreCase(ACTIVE_STATUS); 110 | } 111 | 112 | @Override 113 | public ClusterInfo discoverCluster() { 114 | final DBCluster cluster = this.describeCluster(this.clusterId); 115 | return new ClusterInfo(cluster.readerEndpoint(), replicaMembersOf(cluster)); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/sql/DatabaseInstance.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members.sql; 7 | 8 | public class DatabaseInstance { 9 | private final DatabaseInstanceRole role; 10 | private final String id; 11 | 12 | public DatabaseInstance(DatabaseInstanceRole role, String id) { 13 | this.role = role; 14 | this.id = id; 15 | } 16 | 17 | public DatabaseInstanceRole getRole() { 18 | return role; 19 | } 20 | 21 | public String getId() { 22 | return id; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/sql/DatabaseInstanceRole.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members.sql; 7 | 8 | public enum DatabaseInstanceRole { 9 | READER, 10 | WRITER; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/sql/MySQLReplicasFinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.members.sql; 7 | 8 | import technology.dice.dicefairlink.discovery.members.ClusterInfo; 9 | import technology.dice.dicefairlink.discovery.members.MemberFinderMethod; 10 | import technology.dice.dicefairlink.driver.FairlinkConnectionString; 11 | 12 | import java.sql.Connection; 13 | import java.sql.Driver; 14 | import java.sql.ResultSet; 15 | import java.util.Collections; 16 | import java.util.HashSet; 17 | import java.util.Optional; 18 | import java.util.Set; 19 | import java.util.logging.Level; 20 | import java.util.logging.Logger; 21 | import java.util.stream.Collectors; 22 | 23 | public class MySQLReplicasFinder implements MemberFinderMethod { 24 | private static final Logger LOGGER = Logger.getLogger(MySQLReplicasFinder.class.getName()); 25 | private static final Set EMPTY_SET = 26 | Collections.unmodifiableSet(new HashSet<>(0)); 27 | private static final String DEFAULT_INFORMATION_SCHEMA_NAME = "information_schema"; 28 | private static final String FIND_NODES_QUERY_TEMPLATE = 29 | "select server_id, if(session_id = 'MASTER_SESSION_ID'," 30 | + "'WRITER', 'READER') as role from " 31 | + "%s.replica_host_status;"; 32 | private final Driver driverForDelegate; 33 | private final FairlinkConnectionString fairlinkConnectionString; 34 | private final String informationSchemaName; 35 | 36 | public MySQLReplicasFinder( 37 | FairlinkConnectionString fairlinkConnectionString, 38 | Driver driverForDelegate, 39 | String informationSchemaName) { 40 | this.driverForDelegate = driverForDelegate; 41 | this.fairlinkConnectionString = fairlinkConnectionString; 42 | this.informationSchemaName = 43 | Optional.ofNullable(informationSchemaName).orElse(DEFAULT_INFORMATION_SCHEMA_NAME); 44 | } 45 | 46 | protected Set findReplicas() { 47 | Set instances = new HashSet<>(); 48 | try (final Connection c = 49 | this.driverForDelegate.connect( 50 | fairlinkConnectionString.delegateConnectionString(), 51 | fairlinkConnectionString.getProperties()); 52 | final ResultSet resultSet = 53 | c.createStatement() 54 | .executeQuery( 55 | String.format(FIND_NODES_QUERY_TEMPLATE, this.informationSchemaName))) { 56 | while (resultSet.next()) { 57 | instances.add( 58 | new DatabaseInstance( 59 | DatabaseInstanceRole.valueOf(resultSet.getString("role")), 60 | resultSet.getString("server_id"))); 61 | } 62 | } catch (Exception e) { 63 | LOGGER.log( 64 | Level.SEVERE, 65 | "Failed to obtain cluster members due to exception. Returning empty set", 66 | e); 67 | return EMPTY_SET; 68 | } 69 | return Collections.unmodifiableSet(instances); 70 | } 71 | 72 | @Override 73 | public ClusterInfo discoverCluster() { 74 | return new ClusterInfo( 75 | this.fairlinkConnectionString.delegateConnectionString(), 76 | this.findReplicas().stream() 77 | .filter(databaseInstance -> databaseInstance.getRole() == DatabaseInstanceRole.READER) 78 | .map(DatabaseInstance::getId) 79 | .collect(Collectors.toSet())); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/members/sql/PostgresSQLReplicasFinder.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.members.sql; 2 | 3 | import technology.dice.dicefairlink.discovery.members.ClusterInfo; 4 | import technology.dice.dicefairlink.discovery.members.MemberFinderMethod; 5 | import technology.dice.dicefairlink.driver.FairlinkConnectionString; 6 | 7 | import java.sql.Connection; 8 | import java.sql.Driver; 9 | import java.sql.ResultSet; 10 | import java.util.Collections; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | import java.util.stream.Collectors; 16 | 17 | public class PostgresSQLReplicasFinder implements MemberFinderMethod { 18 | private static final Logger LOGGER = Logger.getLogger(PostgresSQLReplicasFinder.class.getName()); 19 | private static final Set EMPTY_SET = 20 | Collections.unmodifiableSet(new HashSet<>(0)); 21 | 22 | private static final String FIND_NODES_QUERY = 23 | "select server_id, " 24 | + "case when session_id = 'MASTER_SESSION_ID' then 'WRITER' else 'READER' end " 25 | + "as role from aurora_replica_status();"; 26 | 27 | private final Driver driverForDelegate; 28 | private final FairlinkConnectionString fairlinkConnectionString; 29 | 30 | public PostgresSQLReplicasFinder(FairlinkConnectionString fairlinkConnectionString, Driver driverForDelegate) { 31 | this.fairlinkConnectionString = fairlinkConnectionString; 32 | this.driverForDelegate = driverForDelegate; 33 | } 34 | 35 | protected Set findReplicas() { 36 | Set instances = new HashSet<>(); 37 | try (final Connection c = 38 | this.driverForDelegate.connect( 39 | fairlinkConnectionString.delegateConnectionString(), 40 | fairlinkConnectionString.getProperties()); 41 | final ResultSet resultSet = 42 | c.createStatement() 43 | .executeQuery(FIND_NODES_QUERY)) { 44 | while (resultSet.next()) { 45 | instances.add( 46 | new DatabaseInstance( 47 | DatabaseInstanceRole.valueOf(resultSet.getString("role")), 48 | resultSet.getString("server_id"))); 49 | } 50 | } catch (Exception e) { 51 | LOGGER.log( 52 | Level.SEVERE, 53 | "Failed to obtain cluster members due to exception. Returning empty set", 54 | e); 55 | return EMPTY_SET; 56 | } 57 | return Collections.unmodifiableSet(instances); 58 | } 59 | 60 | @Override 61 | public ClusterInfo discoverCluster() { 62 | return new ClusterInfo( 63 | this.fairlinkConnectionString.delegateConnectionString(), 64 | this.findReplicas().stream() 65 | .filter(databaseInstance -> databaseInstance.getRole() == DatabaseInstanceRole.READER) 66 | .map(DatabaseInstance::getId) 67 | .collect(Collectors.toSet())); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/tags/ExclusionTag.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.tags; 7 | 8 | public class ExclusionTag { 9 | private final String key; 10 | private final String value; 11 | 12 | public ExclusionTag(String key, String value) { 13 | this.key = key; 14 | this.value = value; 15 | } 16 | 17 | public String getKey() { 18 | return key; 19 | } 20 | 21 | public String getValue() { 22 | return value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/tags/TagFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.tags; 7 | 8 | import java.util.Set; 9 | 10 | @FunctionalInterface 11 | public interface TagFilter { 12 | Set listExcludedInstances(ExclusionTag tag); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/discovery/tags/awsapi/ResourceGroupApiTagDiscovery.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.discovery.tags.awsapi; 7 | 8 | import java.net.URI; 9 | import software.amazon.awssdk.services.resourcegroupstaggingapi.ResourceGroupsTaggingApiClient; 10 | import software.amazon.awssdk.services.resourcegroupstaggingapi.ResourceGroupsTaggingApiClientBuilder; 11 | import software.amazon.awssdk.services.resourcegroupstaggingapi.model.GetResourcesRequest; 12 | import software.amazon.awssdk.services.resourcegroupstaggingapi.paginators.GetResourcesIterable; 13 | import technology.dice.dicefairlink.config.FairlinkConfiguration; 14 | import technology.dice.dicefairlink.discovery.tags.ExclusionTag; 15 | import technology.dice.dicefairlink.discovery.tags.TagFilter; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | import java.util.Collections; 20 | import java.util.HashSet; 21 | import java.util.Set; 22 | import java.util.logging.Level; 23 | import java.util.logging.Logger; 24 | import java.util.stream.Collectors; 25 | 26 | public class ResourceGroupApiTagDiscovery implements TagFilter { 27 | private static final Logger LOGGER = 28 | Logger.getLogger(ResourceGroupApiTagDiscovery.class.getName()); 29 | private static final String RDS_DB_INSTANCE_FILTER = "rds:db"; 30 | 31 | private final ResourceGroupsTaggingApiClient client; 32 | private final Collection typeFilter; 33 | 34 | public ResourceGroupApiTagDiscovery(FairlinkConfiguration fairlinkConfiguration) { 35 | 36 | final ResourceGroupsTaggingApiClientBuilder clientBuilder = 37 | ResourceGroupsTaggingApiClient.builder() 38 | .region(fairlinkConfiguration.getAuroraClusterRegion()) 39 | .credentialsProvider(fairlinkConfiguration.getAwsCredentialsProvider()); 40 | fairlinkConfiguration 41 | .getAwsEndpointOverride() 42 | .ifPresent(o -> clientBuilder.endpointOverride(URI.create(o))); 43 | this.client = clientBuilder.build(); 44 | Collection temporaryTypeFilter = new ArrayList<>(1); 45 | temporaryTypeFilter.add(RDS_DB_INSTANCE_FILTER); 46 | this.typeFilter = Collections.unmodifiableCollection(temporaryTypeFilter); 47 | } 48 | 49 | @Override 50 | public Set listExcludedInstances(ExclusionTag tags) { 51 | try { 52 | GetResourcesRequest request = 53 | GetResourcesRequest.builder() 54 | .resourceTypeFilters(this.typeFilter) 55 | .tagFilters( 56 | software.amazon.awssdk.services.resourcegroupstaggingapi.model.TagFilter.builder() 57 | .key(tags.getKey()) 58 | .values(tags.getValue()) 59 | .build()) 60 | .build(); 61 | final GetResourcesIterable resourcesPaginator = client.getResourcesPaginator(request); 62 | final Set excludedDbInstances = 63 | resourcesPaginator.stream() 64 | .flatMap( 65 | p -> 66 | p.resourceTagMappingList().stream() 67 | .map( 68 | e -> e.resourceARN().substring(e.resourceARN().lastIndexOf(":") + 1))) 69 | .collect(Collectors.toSet()); 70 | LOGGER.fine( 71 | "Found " 72 | + excludedDbInstances.size() 73 | + " excluded replica" 74 | + (excludedDbInstances.size() != 1 ? "s" : "") 75 | + " in the account, across all clusters"); 76 | return excludedDbInstances; 77 | } catch (Exception e) { 78 | LOGGER.log( 79 | Level.SEVERE, 80 | "Failed to obtain excluded instances. All instances assumed not to be excluded", 81 | e); 82 | return Collections.unmodifiableSet(new HashSet<>(0)); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/driver/AuroraReadReplicasDriver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.driver; 7 | 8 | import java.net.URISyntaxException; 9 | import java.sql.Connection; 10 | import java.sql.Driver; 11 | import java.sql.DriverManager; 12 | import java.sql.DriverPropertyInfo; 13 | import java.sql.SQLException; 14 | import java.util.Collection; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.NoSuchElementException; 18 | import java.util.Optional; 19 | import java.util.Properties; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | import java.util.function.Function; 23 | import java.util.function.Supplier; 24 | import java.util.logging.Level; 25 | import java.util.logging.Logger; 26 | import software.amazon.awssdk.regions.Region; 27 | import technology.dice.dicefairlink.AuroraReadonlyEndpoint; 28 | import technology.dice.dicefairlink.ParsedUrl; 29 | import technology.dice.dicefairlink.config.FairlinkConfiguration; 30 | import technology.dice.dicefairlink.discovery.members.FairlinkMemberFinder; 31 | import technology.dice.dicefairlink.discovery.members.JdbcConnectionValidator; 32 | import technology.dice.dicefairlink.discovery.members.MemberFinderMethod; 33 | import technology.dice.dicefairlink.discovery.members.ReplicaValidator; 34 | import technology.dice.dicefairlink.discovery.members.awsapi.AwsApiReplicasFinder; 35 | import technology.dice.dicefairlink.discovery.members.sql.MySQLReplicasFinder; 36 | import technology.dice.dicefairlink.discovery.members.sql.PostgresSQLReplicasFinder; 37 | import technology.dice.dicefairlink.discovery.tags.TagFilter; 38 | import technology.dice.dicefairlink.discovery.tags.awsapi.ResourceGroupApiTagDiscovery; 39 | import technology.dice.dicefairlink.iterators.RandomisedCyclicIterator; 40 | import technology.dice.dicefairlink.iterators.SizedIterator; 41 | 42 | public class AuroraReadReplicasDriver implements Driver { 43 | private static final Logger LOGGER = Logger.getLogger(AuroraReadReplicasDriver.class.getName()); 44 | private final Map delegates = new HashMap<>(); 45 | private final Map auroraClusters = new HashMap<>(); 46 | 47 | private final Supplier discoveryExecutor; 48 | private final Supplier tagPollExecutor; 49 | private final Optional> tagFilter; 50 | private final Optional> fairlinkMemberFinder; 51 | private final Optional, SizedIterator>> sizedIteratorBuilder; 52 | private final Optional> replicaValidator; 53 | 54 | static { 55 | try { 56 | DriverManager.registerDriver(new AuroraReadReplicasDriver()); 57 | LOGGER.fine("AuroraReadReplicasDriver is now registered."); 58 | } catch (Exception e) { 59 | throw new RuntimeException("Can't register driver!", e); 60 | } 61 | } 62 | 63 | public AuroraReadReplicasDriver() { 64 | this( 65 | () -> Executors.newScheduledThreadPool(1), 66 | () -> Executors.newScheduledThreadPool(1), 67 | null, 68 | null, 69 | null, 70 | null); 71 | } 72 | 73 | public AuroraReadReplicasDriver( 74 | final Supplier discoveryExecutor, 75 | final Supplier tagPollExecutor, 76 | final Supplier tagFilter, 77 | final Supplier memberFinder, 78 | final Supplier replicaValidator, 79 | final Function, SizedIterator> iteratorBuilder) { 80 | LOGGER.fine("Starting..."); 81 | this.discoveryExecutor = discoveryExecutor; 82 | this.tagPollExecutor = tagPollExecutor; 83 | this.tagFilter = Optional.ofNullable(tagFilter); 84 | this.replicaValidator = Optional.ofNullable(replicaValidator); 85 | this.fairlinkMemberFinder = Optional.ofNullable(memberFinder); 86 | this.sizedIteratorBuilder = Optional.ofNullable(iteratorBuilder); 87 | } 88 | 89 | @Override 90 | public boolean acceptsURL(String url) throws SQLException { 91 | if (url == null) { 92 | throw new SQLException("Url must not be null"); 93 | } 94 | boolean matches = FairlinkConnectionString.accepts(url); 95 | LOGGER.info(String.format("Accepting URL: [%s] : %s", url, matches)); 96 | return matches; 97 | } 98 | 99 | /** {@inheritDoc} */ 100 | @Override 101 | public Connection connect(final String url, final Properties properties) throws SQLException { 102 | final Optional parsedUrlOptional = parseUrlAndCacheDriver(url, properties); 103 | 104 | if (!parsedUrlOptional.isPresent()) { 105 | return null; 106 | } 107 | return delegates 108 | .get(parsedUrlOptional.get().getDelegateProtocol()) 109 | .connect(parsedUrlOptional.get().getDelegateUrl(), properties); 110 | } 111 | 112 | /** {@inheritDoc} */ 113 | @Override 114 | public int getMajorVersion() { 115 | return Constants.AURORA_RO_MAJOR_VERSION; 116 | } 117 | 118 | /** {@inheritDoc} */ 119 | @Override 120 | public int getMinorVersion() { 121 | return Constants.AURORA_RO_MINOR_VERSION; 122 | } 123 | 124 | /** {@inheritDoc} */ 125 | @Override 126 | public Logger getParentLogger() { 127 | return LOGGER.getParent(); 128 | } 129 | 130 | /** {@inheritDoc} */ 131 | @Override 132 | public DriverPropertyInfo[] getPropertyInfo(String url, Properties properties) { 133 | return new DriverPropertyInfo[0]; 134 | } 135 | 136 | /** {@inheritDoc} */ 137 | @Override 138 | public boolean jdbcCompliant() { 139 | return false; 140 | } 141 | 142 | private Optional parseUrlAndCacheDriver(final String url, final Properties properties) 143 | throws SQLException { 144 | LOGGER.log(Level.FINE, "URI: {0}", url); 145 | FairlinkConnectionString fairlinkConnectionString; 146 | 147 | try { 148 | try { 149 | fairlinkConnectionString = 150 | new FairlinkConnectionString(url, properties); 151 | } catch (IllegalArgumentException e) { 152 | return Optional.empty(); 153 | } 154 | 155 | if (!this.auroraClusters.containsKey(fairlinkConnectionString.getFairlinkUri())) { 156 | FairlinkConfiguration fairlinkConfiguration = 157 | new FairlinkConfiguration(properties, System.getenv()); 158 | if (!fairlinkConfiguration.isDiscoveryModeValidForDelegate( 159 | fairlinkConnectionString.getDelegateProtocol())) { 160 | return Optional.empty(); 161 | } 162 | LOGGER.log( 163 | Level.FINE, "Delegate driver: {0}", fairlinkConnectionString.getDelegateProtocol()); 164 | LOGGER.log(Level.FINE, "Driver URI: {0}", fairlinkConnectionString.getFairlinkUri()); 165 | final Region region = fairlinkConfiguration.getAuroraClusterRegion(); 166 | LOGGER.log(Level.FINE, "Region: {0}", region); 167 | // because AWS credentials, region and poll interval properties 168 | // are only processed once per uri, the driver does not support dynamically changing them 169 | 170 | this.addDriverForDelegate( 171 | fairlinkConnectionString.getDelegateProtocol(), 172 | fairlinkConnectionString.delegateConnectionString()); 173 | 174 | final AuroraReadonlyEndpoint roEndpoint = 175 | new AuroraReadonlyEndpoint( 176 | fairlinkConfiguration, 177 | this.fairlinkMemberFinder 178 | .map(Supplier::get) 179 | .orElseGet( 180 | () -> 181 | newMemberFinder( 182 | fairlinkConnectionString, fairlinkConfiguration, properties)), 183 | this.discoveryExecutor.get()); 184 | 185 | LOGGER.log(Level.FINE, "RO url: {0}", fairlinkConnectionString.getHost()); 186 | this.auroraClusters.put(fairlinkConnectionString.getFairlinkUri(), roEndpoint); 187 | } 188 | 189 | final String nextReplica = 190 | auroraClusters.get(fairlinkConnectionString.getFairlinkUri()).getNextReplica(); 191 | LOGGER.fine( 192 | String.format( 193 | "Obtained [%s] for the next replica to use for cluster [%s]", 194 | nextReplica, fairlinkConnectionString.getHost())); 195 | final String delegatedReplicaUri = 196 | fairlinkConnectionString.delegateConnectionString(nextReplica); 197 | 198 | LOGGER.log(Level.FINE, "URI to connect to: {0}", delegatedReplicaUri); 199 | 200 | return Optional.of( 201 | new ParsedUrl(fairlinkConnectionString.getDelegateProtocol(), delegatedReplicaUri)); 202 | 203 | } catch (URISyntaxException e) { 204 | LOGGER.log(Level.FINE, "Can not get replicas for cluster URI: " + url, e); 205 | return Optional.empty(); 206 | } catch (NoSuchElementException e) { 207 | return Optional.empty(); 208 | } 209 | } 210 | 211 | public void refreshReplicas(String url) { 212 | final AuroraReadonlyEndpoint auroraReadonlyEndpoint = this.auroraClusters.get(url); 213 | if (auroraReadonlyEndpoint != null) { 214 | auroraReadonlyEndpoint.refresh(); 215 | } 216 | } 217 | 218 | private FairlinkMemberFinder newMemberFinder( 219 | FairlinkConnectionString fairlinkConnectionString, 220 | FairlinkConfiguration fairlinkConfiguration, 221 | Properties properties) { 222 | return new FairlinkMemberFinder( 223 | fairlinkConfiguration, 224 | fairlinkConnectionString, 225 | this.tagPollExecutor.get(), 226 | this.tagFilter 227 | .map(Supplier::get) 228 | .orElseGet(() -> new ResourceGroupApiTagDiscovery(fairlinkConfiguration)), 229 | this.newMemberFinderMethod( 230 | fairlinkConfiguration, 231 | fairlinkConnectionString, 232 | this.delegates.get(fairlinkConnectionString.getDelegateProtocol()), 233 | properties), 234 | this.sizedIteratorBuilder.orElse(strings -> RandomisedCyclicIterator.of(strings)), 235 | this.replicaValidator 236 | .map(Supplier::get) 237 | .orElse( 238 | new JdbcConnectionValidator( 239 | this.delegates.get(fairlinkConnectionString.getDelegateProtocol())))); 240 | } 241 | 242 | private MemberFinderMethod newMemberFinderMethod( 243 | FairlinkConfiguration fairlinkConfiguration, 244 | FairlinkConnectionString fairlinkConnectionString, 245 | Driver driver, 246 | Properties properties) { 247 | switch (fairlinkConfiguration.getReplicasDiscoveryMode()) { 248 | case AWS_API: 249 | return new AwsApiReplicasFinder(fairlinkConfiguration, fairlinkConnectionString); 250 | case SQL_MYSQL: 251 | return new MySQLReplicasFinder( 252 | fairlinkConnectionString, 253 | driver, 254 | properties.getProperty("_fairlinkMySQLSchemaOverride")); 255 | case SQL_POSTGRES: 256 | return new PostgresSQLReplicasFinder( 257 | fairlinkConnectionString, 258 | driver); 259 | default: 260 | throw new IllegalArgumentException( 261 | fairlinkConfiguration.getReplicasDiscoveryMode().name() 262 | + "is not a valid discovery mode"); 263 | } 264 | } 265 | 266 | private void addDriverForDelegate(String delegate, final String stringURI) throws SQLException { 267 | this.delegates.putIfAbsent(delegate, DriverManager.getDriver(stringURI)); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/driver/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.driver; 7 | 8 | public class Constants { 9 | public static final int AURORA_RO_MAJOR_VERSION = 2; 10 | public static final int AURORA_RO_MINOR_VERSION = 1; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/driver/FairlinkConnectionString.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.driver; 7 | 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.util.Properties; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | public class FairlinkConnectionString { 15 | private static final String DRIVER_PROTOCOL = "fairlink"; 16 | private static final String DRIVER_PROTOCOL_BACKWARD_COMPATIBILITY = "auroraro"; 17 | private static final Pattern DRIVER_PATTERN = 18 | Pattern.compile("jdbc:" + DRIVER_PROTOCOL + ":(?[^:]*):(?.*\\/\\/.+)"); 19 | private static final Pattern DRIVER_PATTERN_BACKWARD_COMPATIBILITY = 20 | Pattern.compile( 21 | "jdbc:" 22 | + DRIVER_PROTOCOL_BACKWARD_COMPATIBILITY 23 | + ":(?[^:]*):(?.*\\/\\/.+)"); 24 | private static final String JDBC_PREFIX = "jdbc"; 25 | private final String delegateProtocol; 26 | private final String fairlinKUri; 27 | private final URI delegateUri; 28 | private final Properties properties; 29 | 30 | public FairlinkConnectionString(String connectionString, Properties properties) 31 | throws URISyntaxException { 32 | this.fairlinKUri = connectionString; 33 | this.properties = properties; 34 | 35 | Matcher matcher = DRIVER_PATTERN.matcher(connectionString); 36 | Matcher matcherBackwardCompatibility = 37 | DRIVER_PATTERN_BACKWARD_COMPATIBILITY.matcher(connectionString); 38 | if (matcher.matches()) { 39 | this.delegateProtocol = matcher.group("delegate"); 40 | this.delegateUri = new URI(this.delegateProtocol + ":" + matcher.group("uri")); 41 | } else if (matcherBackwardCompatibility.matches()) { 42 | this.delegateProtocol = matcherBackwardCompatibility.group("delegate"); 43 | this.delegateUri = 44 | new URI(this.delegateProtocol + ":" + matcherBackwardCompatibility.group("uri")); 45 | } else { 46 | throw new IllegalArgumentException( 47 | connectionString + " is not a valid fairlink connection string"); 48 | } 49 | } 50 | 51 | public String getDelegateProtocol() { 52 | return delegateProtocol; 53 | } 54 | 55 | public String delegateConnectionString() { 56 | return JDBC_PREFIX + ":" + this.delegateUri.toASCIIString(); 57 | } 58 | 59 | public String delegateConnectionString(String forHost) throws URISyntaxException { 60 | final URI portExtractor = new URI(JDBC_PREFIX + "://" + forHost); 61 | return new URI( 62 | JDBC_PREFIX + ":" + delegateUri.getScheme(), 63 | delegateUri.getUserInfo(), 64 | portExtractor.getHost(), 65 | portExtractor.getPort() == -1 ? delegateUri.getPort() : portExtractor.getPort(), 66 | delegateUri.getPath(), 67 | delegateUri.getQuery(), 68 | delegateUri.getFragment()) 69 | .toASCIIString(); 70 | } 71 | 72 | public String getHost() { 73 | return delegateUri.getHost(); 74 | } 75 | 76 | public String getFairlinkUri() { 77 | return this.fairlinKUri; 78 | } 79 | 80 | public Properties getProperties() { 81 | return properties; 82 | } 83 | 84 | public static boolean accepts(String url) { 85 | Matcher matcher = DRIVER_PATTERN.matcher(url); 86 | Matcher matcherBackwardCompatibility = DRIVER_PATTERN_BACKWARD_COMPATIBILITY.matcher(url); 87 | return matcher.matches() || matcherBackwardCompatibility.matches(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/iterators/CyclicIterator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.iterators; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.Collections; 11 | import java.util.Iterator; 12 | import java.util.List; 13 | import java.util.NoSuchElementException; 14 | 15 | public class CyclicIterator implements SizedIterator { 16 | private final List elements; 17 | private volatile Iterator iterator; 18 | 19 | protected CyclicIterator(Collection collection) { 20 | this.elements = Collections.unmodifiableList(new ArrayList(collection)); 21 | this.iterator = this.elements.iterator(); 22 | } 23 | 24 | @Override 25 | public boolean hasNext() { 26 | return !elements.isEmpty(); 27 | } 28 | 29 | @Override 30 | public int size() { 31 | return elements.size(); 32 | } 33 | 34 | @Override 35 | public synchronized T next() { 36 | if (!iterator.hasNext()) { 37 | iterator = elements.iterator(); 38 | if (!iterator.hasNext()) { 39 | throw new NoSuchElementException(); 40 | } 41 | } 42 | T next = iterator.next(); 43 | return next; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/iterators/RandomisedCyclicIterator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.iterators; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.Collections; 11 | import java.util.concurrent.ThreadLocalRandom; 12 | 13 | public class RandomisedCyclicIterator extends CyclicIterator { 14 | 15 | protected RandomisedCyclicIterator(Collection collection) { 16 | super(shuffle(collection)); 17 | } 18 | 19 | private static Collection shuffle(Collection collection) { 20 | final ArrayList copy = new ArrayList<>(collection); 21 | Collections.shuffle(copy, ThreadLocalRandom.current()); 22 | return Collections.unmodifiableCollection(copy); 23 | } 24 | 25 | public static RandomisedCyclicIterator of(Collection collection) { 26 | return new RandomisedCyclicIterator<>(collection); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/technology/dice/dicefairlink/iterators/SizedIterator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.iterators; 7 | 8 | import java.util.Iterator; 9 | 10 | public interface SizedIterator extends Iterator { 11 | int size(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/java.sql.Driver: -------------------------------------------------------------------------------- 1 | technology.dice.dicefairlink.driver.AuroraReadReplicasDriver -------------------------------------------------------------------------------- /src/test/bash/readReplicasOnLoop.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if (( $# < 5 )); then 4 | echo "usage: $0 host port username password iterations" 5 | exit 1; 6 | fi 7 | 8 | i=1 9 | host=$1 10 | port=$2 11 | username=$3 12 | password=$4 13 | end=$5 14 | 15 | while [ $i -le $end ]; do 16 | echo $(date) - iteration $i 17 | mysql -h $host -P $port -u $username -p$password -e "select server_id, if(session_id = 'MASTER_SESSION_ID','WRITER', 'READER') as role from information_schema.replica_host_status;" 18 | i=$(($i+1)) 19 | done 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/AuroraReadonlyEndpointTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.testcontainers.shaded.com.google.common.collect.ImmutableList; 6 | import org.testcontainers.shaded.com.google.common.collect.ImmutableSet; 7 | import technology.dice.dicefairlink.config.FairlinkConfiguration; 8 | import technology.dice.dicefairlink.support.discovery.members.FixedMemberFinder; 9 | 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.Properties; 13 | 14 | public class AuroraReadonlyEndpointTest { 15 | private Properties baseTestProperties() { 16 | Properties p = new Properties(); 17 | p.setProperty("auroraClusterRegion", "eu-west-1"); 18 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 19 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 20 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 21 | p.setProperty("discoveryMode", "SQL_MYSQL"); 22 | p.setProperty("replicaPollInterval", "5"); 23 | p.setProperty("tagsPollInterval", "10"); 24 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 25 | p.setProperty("validateConnection", "true"); 26 | return p; 27 | } 28 | 29 | @Test 30 | public void multipleReplicas() { 31 | AuroraReadonlyEndpoint underTest = 32 | new AuroraReadonlyEndpoint( 33 | new FairlinkConfiguration(this.baseTestProperties(), new HashMap<>()), 34 | new FixedMemberFinder(ImmutableSet.of("r1", "r2", "r3")), 35 | new StepByStepExecutor(1)); 36 | Assert.assertEquals("r1", underTest.getNextReplica()); 37 | Assert.assertEquals("r2", underTest.getNextReplica()); 38 | Assert.assertEquals("r3", underTest.getNextReplica()); 39 | Assert.assertEquals("r1", underTest.getNextReplica()); 40 | } 41 | 42 | @Test 43 | public void oneReplica() { 44 | AuroraReadonlyEndpoint underTest = 45 | new AuroraReadonlyEndpoint( 46 | new FairlinkConfiguration(this.baseTestProperties(), new HashMap<>()), 47 | new FixedMemberFinder(ImmutableSet.of("r1")), 48 | new StepByStepExecutor(1)); 49 | Assert.assertEquals("r1", underTest.getNextReplica()); 50 | Assert.assertEquals("r1", underTest.getNextReplica()); 51 | Assert.assertEquals("r1", underTest.getNextReplica()); 52 | } 53 | 54 | @Test 55 | public void refresh() { 56 | final FixedMemberFinder finder = new FixedMemberFinder(ImmutableSet.of("r1")); 57 | AuroraReadonlyEndpoint underTest = 58 | new AuroraReadonlyEndpoint( 59 | new FairlinkConfiguration(this.baseTestProperties(), new HashMap<>()), 60 | finder, 61 | new StepByStepExecutor(1)); 62 | Assert.assertEquals("r1", underTest.getNextReplica()); 63 | Assert.assertEquals("r1", underTest.getNextReplica()); 64 | Assert.assertEquals("r1", underTest.getNextReplica()); 65 | finder.updateMembers(ImmutableList.of("r3", "r4")); 66 | underTest.refresh(); 67 | Assert.assertEquals("r3", underTest.getNextReplica()); 68 | Assert.assertEquals("r4", underTest.getNextReplica()); 69 | Assert.assertEquals("r3", underTest.getNextReplica()); 70 | } 71 | 72 | @Test 73 | public void triesToSkipRepeated() { 74 | AuroraReadonlyEndpoint underTest = 75 | new AuroraReadonlyEndpoint( 76 | new FairlinkConfiguration(this.baseTestProperties(), new HashMap<>()), 77 | new FixedMemberFinder(ImmutableList.of("r1", "r1", "r2")), 78 | new StepByStepExecutor(1)); 79 | Assert.assertEquals("r1", underTest.getNextReplica()); 80 | Assert.assertEquals("r2", underTest.getNextReplica()); 81 | } 82 | 83 | @Test 84 | public void nullEntry() { 85 | ArrayList acceptsNulls = new ArrayList(3); 86 | acceptsNulls.add(null); 87 | acceptsNulls.add(null); 88 | acceptsNulls.add("r3"); 89 | AuroraReadonlyEndpoint underTest = 90 | new AuroraReadonlyEndpoint( 91 | new FairlinkConfiguration(this.baseTestProperties(), new HashMap<>()), 92 | new FixedMemberFinder(acceptsNulls), 93 | new StepByStepExecutor(1)); 94 | Assert.assertNull(underTest.getNextReplica()); 95 | Assert.assertNull(underTest.getNextReplica()); 96 | Assert.assertEquals("r3", underTest.getNextReplica()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/StepByStepExecutor.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink; 2 | 3 | import java.util.concurrent.ScheduledFuture; 4 | import java.util.concurrent.ScheduledThreadPoolExecutor; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | public class StepByStepExecutor extends ScheduledThreadPoolExecutor { 8 | 9 | private volatile Runnable task; 10 | 11 | public StepByStepExecutor(int corePoolSize) { 12 | super(corePoolSize); 13 | } 14 | 15 | @Override 16 | public ScheduledFuture scheduleAtFixedRate( 17 | Runnable task, long ignoredDelay, long ignoredPeriod, TimeUnit ignoredTimeUnit) { 18 | this.task = task; 19 | return null; 20 | } 21 | 22 | public void step() { 23 | task.run(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/config/FairlinkConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.config; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.google.common.collect.Maps; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 8 | import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; 9 | import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; 10 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 11 | import software.amazon.awssdk.regions.Region; 12 | 13 | import java.time.Duration; 14 | import java.util.Properties; 15 | 16 | public class FairlinkConfigurationTest { 17 | @Test 18 | public void goodBasicCredentials() { 19 | Properties p = new Properties(); 20 | p.setProperty("auroraClusterRegion", "eu-west-1"); 21 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 22 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 23 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 24 | p.setProperty("discoveryMode", "SQL_MYSQL"); 25 | p.setProperty("replicaPollInterval", "5"); 26 | p.setProperty("tagsPollInterval", "10"); 27 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 28 | p.setProperty("validateConnection", "true"); 29 | FairlinkConfiguration underTest = new FairlinkConfiguration(p, Maps.newHashMap()); 30 | Assert.assertEquals(Region.EU_WEST_1, underTest.getAuroraClusterRegion()); 31 | final StaticCredentialsProvider expectedCredentials = 32 | StaticCredentialsProvider.create(AwsBasicCredentials.create("keyId", "keySecret")); 33 | Assert.assertTrue(underTest.getAwsCredentialsProvider() instanceof StaticCredentialsProvider); 34 | Assert.assertEquals("keyId", expectedCredentials.resolveCredentials().accessKeyId()); 35 | Assert.assertEquals("keySecret", expectedCredentials.resolveCredentials().secretAccessKey()); 36 | Assert.assertEquals(ReplicasDiscoveryMode.SQL_MYSQL, underTest.getReplicasDiscoveryMode()); 37 | Assert.assertEquals(Duration.ofSeconds(10), underTest.getTagsPollerInterval()); 38 | Assert.assertEquals(Duration.ofSeconds(5), underTest.getReplicaPollInterval()); 39 | Assert.assertTrue(underTest.isValidateConnection()); 40 | Assert.assertTrue(underTest.isDiscoveryModeValidForDelegate("mysql")); 41 | Assert.assertFalse(underTest.isDiscoveryModeValidForDelegate("postgresql")); 42 | Assert.assertEquals("replica1.rest-of-myhost.name", underTest.hostname("replica1")); 43 | final Duration jitter = underTest.randomBoundDelay(); 44 | Assert.assertFalse(jitter.isNegative()); 45 | Assert.assertTrue(jitter.compareTo(Duration.ofSeconds(10)) <= 0); 46 | } 47 | 48 | @Test 49 | public void goodBasicCredentialsRegionFromEnvironment() { 50 | Properties p = new Properties(); 51 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 52 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 53 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 54 | p.setProperty("discoveryMode", "SQL_MYSQL"); 55 | p.setProperty("replicaPollInterval", "5"); 56 | p.setProperty("tagsPollInterval", "10"); 57 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 58 | p.setProperty("validateConnection", "true"); 59 | FairlinkConfiguration underTest = 60 | new FairlinkConfiguration( 61 | p, ImmutableMap.of("AWS_DEFAULT_REGION", Region.AP_NORTHEAST_1.toString())); 62 | Assert.assertEquals(Region.AP_NORTHEAST_1, underTest.getAuroraClusterRegion()); 63 | final StaticCredentialsProvider expectedCredentials = 64 | StaticCredentialsProvider.create(AwsBasicCredentials.create("keyId", "keySecret")); 65 | Assert.assertTrue(underTest.getAwsCredentialsProvider() instanceof StaticCredentialsProvider); 66 | Assert.assertEquals("keyId", expectedCredentials.resolveCredentials().accessKeyId()); 67 | Assert.assertEquals("keySecret", expectedCredentials.resolveCredentials().secretAccessKey()); 68 | Assert.assertEquals(ReplicasDiscoveryMode.SQL_MYSQL, underTest.getReplicasDiscoveryMode()); 69 | Assert.assertEquals(Duration.ofSeconds(10), underTest.getTagsPollerInterval()); 70 | Assert.assertEquals(Duration.ofSeconds(5), underTest.getReplicaPollInterval()); 71 | Assert.assertTrue(underTest.isValidateConnection()); 72 | Assert.assertTrue(underTest.isDiscoveryModeValidForDelegate("mysql")); 73 | Assert.assertFalse(underTest.isDiscoveryModeValidForDelegate("postgresql")); 74 | Assert.assertEquals("replica1.rest-of-myhost.name", underTest.hostname("replica1")); 75 | final Duration jitter = underTest.randomBoundDelay(); 76 | Assert.assertFalse(jitter.isNegative()); 77 | Assert.assertTrue(jitter.compareTo(Duration.ofSeconds(10)) <= 0); 78 | } 79 | 80 | @Test 81 | public void goodEnvironmentCredentials() { 82 | Properties p = new Properties(); 83 | p.setProperty("auroraClusterRegion", "eu-west-1"); 84 | p.setProperty("auroraDiscoveryAuthMode", "environment"); 85 | p.setProperty("discoveryMode", "AWS_API"); 86 | p.setProperty("replicaPollInterval", "5"); 87 | p.setProperty("tagsPollInterval", "10"); 88 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 89 | p.setProperty("validateConnection", "true"); 90 | FairlinkConfiguration underTest = 91 | new FairlinkConfiguration( 92 | p, ImmutableMap.of("AWS_SECRET_KEY", "key", "AWS_SECRET_ACCESS_KEY", "secret")); 93 | Assert.assertEquals(Region.EU_WEST_1, underTest.getAuroraClusterRegion()); 94 | Assert.assertTrue( 95 | underTest.getAwsCredentialsProvider() instanceof EnvironmentVariableCredentialsProvider); 96 | Assert.assertEquals(ReplicasDiscoveryMode.AWS_API, underTest.getReplicasDiscoveryMode()); 97 | Assert.assertEquals(Duration.ofSeconds(10), underTest.getTagsPollerInterval()); 98 | Assert.assertEquals(Duration.ofSeconds(5), underTest.getReplicaPollInterval()); 99 | Assert.assertTrue(underTest.isValidateConnection()); 100 | Assert.assertTrue(underTest.isDiscoveryModeValidForDelegate("mysql")); 101 | Assert.assertTrue(underTest.isDiscoveryModeValidForDelegate("postgresql")); 102 | Assert.assertEquals("replica1.rest-of-myhost.name", underTest.hostname("replica1")); 103 | final Duration jitter = underTest.randomBoundDelay(); 104 | Assert.assertFalse(jitter.isNegative()); 105 | Assert.assertTrue(jitter.compareTo(Duration.ofSeconds(10)) <= 0); 106 | } 107 | 108 | @Test 109 | public void goodDefaultCredentials() { 110 | Properties p = new Properties(); 111 | p.setProperty("auroraClusterRegion", "eu-west-1"); 112 | p.setProperty("discoveryMode", "SQL_MYSQL"); 113 | p.setProperty("replicaPollInterval", "5"); 114 | p.setProperty("tagsPollInterval", "10"); 115 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 116 | p.setProperty("validateConnection", "true"); 117 | FairlinkConfiguration underTest = 118 | new FairlinkConfiguration( 119 | p, ImmutableMap.of("AWS_SECRET_KEY", "key", "AWS_SECRET_ACCESS_KEY", "secret")); 120 | Assert.assertEquals(Region.EU_WEST_1, underTest.getAuroraClusterRegion()); 121 | Assert.assertTrue(underTest.getAwsCredentialsProvider() instanceof DefaultCredentialsProvider); 122 | Assert.assertEquals(ReplicasDiscoveryMode.SQL_MYSQL, underTest.getReplicasDiscoveryMode()); 123 | Assert.assertEquals(Duration.ofSeconds(10), underTest.getTagsPollerInterval()); 124 | Assert.assertEquals(Duration.ofSeconds(5), underTest.getReplicaPollInterval()); 125 | Assert.assertTrue(underTest.isValidateConnection()); 126 | Assert.assertTrue(underTest.isDiscoveryModeValidForDelegate("mysql")); 127 | Assert.assertFalse(underTest.isDiscoveryModeValidForDelegate("postgresql")); 128 | Assert.assertEquals("replica1.rest-of-myhost.name", underTest.hostname("replica1")); 129 | final Duration jitter = underTest.randomBoundDelay(); 130 | Assert.assertFalse(jitter.isNegative()); 131 | Assert.assertTrue(jitter.compareTo(Duration.ofSeconds(10)) <= 0); 132 | } 133 | 134 | @Test(expected = IllegalStateException.class) 135 | public void basicWithoutKey() { 136 | Properties p = new Properties(); 137 | p.setProperty("auroraClusterRegion", "eu-west-1"); 138 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 139 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 140 | p.setProperty("discoveryMode", "SQL_MYSQL"); 141 | p.setProperty("replicaPollInterval", "5"); 142 | p.setProperty("tagsPollInterval", "10"); 143 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 144 | p.setProperty("validateConnection", "true"); 145 | new FairlinkConfiguration(p, Maps.newHashMap()); 146 | } 147 | 148 | @Test(expected = IllegalStateException.class) 149 | public void basicWithoutSecret() { 150 | Properties p = new Properties(); 151 | p.setProperty("auroraClusterRegion", "eu-west-1"); 152 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 153 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 154 | p.setProperty("discoveryMode", "SQL_MYSQL"); 155 | p.setProperty("replicaPollInterval", "5"); 156 | p.setProperty("tagsPollInterval", "10"); 157 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 158 | p.setProperty("validateConnection", "true"); 159 | new FairlinkConfiguration(p, Maps.newHashMap()); 160 | } 161 | 162 | @Test(expected = IllegalStateException.class) 163 | public void awsApiWithoutRegion() { 164 | Properties p = new Properties(); 165 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 166 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 167 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 168 | p.setProperty("discoveryMode", "AWS_API"); 169 | p.setProperty("replicaPollInterval", "5"); 170 | p.setProperty("tagsPollInterval", "10"); 171 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 172 | p.setProperty("validateConnection", "true"); 173 | new FairlinkConfiguration(p, Maps.newHashMap()); 174 | } 175 | 176 | @Test(expected = IllegalStateException.class) 177 | public void noReplicaTemplate() { 178 | Properties p = new Properties(); 179 | p.setProperty("auroraClusterRegion", "eu-west-1"); 180 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 181 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 182 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 183 | p.setProperty("discoveryMode", "SQL_MYSQL"); 184 | p.setProperty("replicaPollInterval", "5"); 185 | p.setProperty("tagsPollInterval", "10"); 186 | p.setProperty("validateConnection", "true"); 187 | new FairlinkConfiguration(p, Maps.newHashMap()); 188 | } 189 | 190 | @Test 191 | public void defaultPollerDurationsAbsent() { 192 | Properties p = new Properties(); 193 | p.setProperty("auroraClusterRegion", "eu-west-1"); 194 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 195 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 196 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 197 | p.setProperty("discoveryMode", "SQL_MYSQL"); 198 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 199 | p.setProperty("validateConnection", "true"); 200 | FairlinkConfiguration underTest = new FairlinkConfiguration(p, Maps.newHashMap()); 201 | Assert.assertEquals(Duration.ofMinutes(2), underTest.getTagsPollerInterval()); 202 | Assert.assertEquals(Duration.ofSeconds(30), underTest.getReplicaPollInterval()); 203 | } 204 | 205 | @Test 206 | public void defaultPollerDurationsInvalid() { 207 | Properties p = new Properties(); 208 | p.setProperty("auroraClusterRegion", "eu-west-1"); 209 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 210 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 211 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 212 | p.setProperty("discoveryMode", "SQL_MYSQL"); 213 | p.setProperty("replicaPollInterval", "abc"); 214 | p.setProperty("tagsPollInterval", "def"); 215 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 216 | p.setProperty("validateConnection", "true"); 217 | FairlinkConfiguration underTest = new FairlinkConfiguration(p, Maps.newHashMap()); 218 | Assert.assertEquals(Duration.ofMinutes(2), underTest.getTagsPollerInterval()); 219 | Assert.assertEquals(Duration.ofSeconds(30), underTest.getReplicaPollInterval()); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/members/ClusterInfoTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.members; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.testcontainers.shaded.com.google.common.collect.ImmutableSet; 6 | 7 | public class ClusterInfoTest { 8 | @Test 9 | public void hashcode() { 10 | ClusterInfo c1 = new ClusterInfo("a", ImmutableSet.of("r1", "r2")); 11 | ClusterInfo c2 = new ClusterInfo("b", ImmutableSet.of("r3", "r4")); 12 | ClusterInfo c3 = new ClusterInfo("a", ImmutableSet.of("r1", "r2")); 13 | Assert.assertEquals(c1.hashCode(), c3.hashCode()); 14 | Assert.assertNotEquals(c1.hashCode(), c2.hashCode()); 15 | } 16 | 17 | @Test 18 | public void equalz() { 19 | ClusterInfo c1 = new ClusterInfo("a", ImmutableSet.of("r1", "r2")); 20 | ClusterInfo c2 = new ClusterInfo("b", ImmutableSet.of("r3", "r4")); 21 | ClusterInfo c3 = new ClusterInfo("a", ImmutableSet.of("r1", "r2")); 22 | ClusterInfo c4 = new ClusterInfo("a", ImmutableSet.of("r1", "r4")); 23 | Assert.assertTrue(c1.equals(c3)); 24 | Assert.assertTrue(c1.equals(c1)); 25 | Assert.assertFalse(c1.equals(c2)); 26 | Assert.assertFalse(c1.equals(c4)); 27 | Assert.assertFalse(c1.equals("not eequals")); 28 | Assert.assertFalse(c1.equals(null)); 29 | } 30 | 31 | @Test(expected = IllegalArgumentException.class) 32 | public void ctorEmptyReadOnlyEndpoint() { 33 | new ClusterInfo("", ImmutableSet.of("r1", "r2")); 34 | } 35 | 36 | @Test(expected = IllegalArgumentException.class) 37 | public void ctorNullReadOnlyEndpoint() { 38 | new ClusterInfo(null, ImmutableSet.of("r1", "r2")); 39 | } 40 | 41 | @Test(expected = IllegalArgumentException.class) 42 | public void ctorNullListOfReplicas() { 43 | new ClusterInfo("a", null); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/members/JdbcConnectionValidatorTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.members; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.testcontainers.containers.MySQLContainer; 7 | 8 | import java.util.Properties; 9 | 10 | public class JdbcConnectionValidatorTest { 11 | @Rule public MySQLContainer mysql = new MySQLContainer(); 12 | 13 | private Properties baseTestProperties() { 14 | Properties p = new Properties(); 15 | p.setProperty("user", mysql.getUsername()); 16 | p.setProperty("password", mysql.getPassword()); 17 | return p; 18 | } 19 | 20 | @Test 21 | public void validConnection() { 22 | JdbcConnectionValidator underTest = new JdbcConnectionValidator(mysql.getJdbcDriverInstance()); 23 | Assert.assertTrue(underTest.isValid(mysql.getJdbcUrl(), baseTestProperties())); 24 | } 25 | 26 | @Test 27 | public void inValidConnection() { 28 | JdbcConnectionValidator underTest = new JdbcConnectionValidator(mysql.getJdbcDriverInstance()); 29 | Assert.assertFalse(underTest.isValid(mysql.getJdbcUrl(), new Properties())); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/members/awsapi/AwsApiReplicasFinderTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.members.awsapi; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 4 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 7 | 8 | import com.github.tomakehurst.wiremock.client.WireMock; 9 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 10 | import com.google.common.base.Charsets; 11 | import com.google.common.collect.ImmutableMap; 12 | import com.google.common.collect.ImmutableSet; 13 | import com.google.common.io.CharStreams; 14 | import java.io.IOException; 15 | import java.io.InputStreamReader; 16 | import java.net.URISyntaxException; 17 | import java.util.Properties; 18 | import org.junit.Assert; 19 | import org.junit.Rule; 20 | import org.junit.Test; 21 | import technology.dice.dicefairlink.config.FairlinkConfiguration; 22 | import technology.dice.dicefairlink.discovery.members.ClusterInfo; 23 | import technology.dice.dicefairlink.driver.FairlinkConnectionString; 24 | 25 | public class AwsApiReplicasFinderTest { 26 | @Rule public WireMockRule wireMockRule = new WireMockRule(11342); 27 | 28 | private Properties baseTestProperties() { 29 | Properties p = new Properties(); 30 | p.setProperty("auroraClusterRegion", "eu-west-1"); 31 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 32 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 33 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 34 | p.setProperty("discoveryMode", "AWS_API"); 35 | p.setProperty("replicaPollInterval", "5"); 36 | p.setProperty("tagsPollInterval", "10"); 37 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 38 | p.setProperty("validateConnection", "true"); 39 | p.setProperty("awsEndpointOverride", "http://localhost:11342"); 40 | return p; 41 | } 42 | 43 | @Test 44 | public void withMembersAllAvailable() throws URISyntaxException, IOException { 45 | AwsApiReplicasFinder underTest = 46 | new AwsApiReplicasFinder( 47 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of()), 48 | new FairlinkConnectionString( 49 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 50 | this.baseTestProperties())); 51 | 52 | String describeClusterResponse = 53 | CharStreams.toString( 54 | new InputStreamReader( 55 | AwsApiReplicasFinderTest.class 56 | .getClassLoader() 57 | .getResourceAsStream( 58 | "technology/dice/dicefairlink/discovery/members/awsapi/withMembers.xml"), 59 | Charsets.UTF_8)); 60 | 61 | String describeInstancesResponse = 62 | CharStreams.toString( 63 | new InputStreamReader( 64 | AwsApiReplicasFinderTest.class 65 | .getClassLoader() 66 | .getResourceAsStream( 67 | "technology/dice/dicefairlink/discovery/members/awsapi/allAvailableDbInstances.xml"), 68 | Charsets.UTF_8)); 69 | 70 | stubFor( 71 | post(urlEqualTo("/")) 72 | .withRequestBody(WireMock.containing("Action=DescribeDBClusters")) 73 | .willReturn(aResponse().withStatus(200).withBody(describeClusterResponse))); 74 | stubFor( 75 | post(urlEqualTo("/")) 76 | .withRequestBody(WireMock.containing("Action=DescribeDBInstances")) 77 | .willReturn(aResponse().withStatus(200).withBody(describeInstancesResponse))); 78 | 79 | final ClusterInfo actual = underTest.discoverCluster(); 80 | Assert.assertEquals( 81 | new ClusterInfo( 82 | "cluster-reader-endpoint", 83 | ImmutableSet.of("my-db-cluster-agd-3", "my-db-cluster-agd-2")), 84 | actual); 85 | } 86 | 87 | @Test 88 | public void withMembersOnReaderDeleting() throws URISyntaxException, IOException { 89 | AwsApiReplicasFinder underTest = 90 | new AwsApiReplicasFinder( 91 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of()), 92 | new FairlinkConnectionString( 93 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 94 | this.baseTestProperties())); 95 | 96 | String describeClusterResponse = 97 | CharStreams.toString( 98 | new InputStreamReader( 99 | AwsApiReplicasFinderTest.class 100 | .getClassLoader() 101 | .getResourceAsStream( 102 | "technology/dice/dicefairlink/discovery/members/awsapi/withMembers.xml"), 103 | Charsets.UTF_8)); 104 | 105 | String describeInstancesResponse = 106 | CharStreams.toString( 107 | new InputStreamReader( 108 | AwsApiReplicasFinderTest.class 109 | .getClassLoader() 110 | .getResourceAsStream( 111 | "technology/dice/dicefairlink/discovery/members/awsapi/oneReplicaDeletingInstances.xml"), 112 | Charsets.UTF_8)); 113 | 114 | stubFor( 115 | post(urlEqualTo("/")) 116 | .withRequestBody(WireMock.containing("Action=DescribeDBClusters")) 117 | .willReturn(aResponse().withStatus(200).withBody(describeClusterResponse))); 118 | stubFor( 119 | post(urlEqualTo("/")) 120 | .withRequestBody(WireMock.containing("Action=DescribeDBInstances")) 121 | .willReturn(aResponse().withStatus(200).withBody(describeInstancesResponse))); 122 | 123 | final ClusterInfo actual = underTest.discoverCluster(); 124 | Assert.assertEquals( 125 | new ClusterInfo("cluster-reader-endpoint", ImmutableSet.of("my-db-cluster-agd-2")), actual); 126 | } 127 | 128 | @Test 129 | public void withoutMembers() throws URISyntaxException, IOException { 130 | AwsApiReplicasFinder underTest = 131 | new AwsApiReplicasFinder( 132 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of()), 133 | new FairlinkConnectionString( 134 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 135 | this.baseTestProperties())); 136 | 137 | String response = 138 | CharStreams.toString( 139 | new InputStreamReader( 140 | AwsApiReplicasFinderTest.class 141 | .getClassLoader() 142 | .getResourceAsStream( 143 | "technology/dice/dicefairlink/discovery/members/awsapi/withoutMembers.xml"), 144 | Charsets.UTF_8)); 145 | 146 | stubFor(post(urlEqualTo("/")).willReturn(aResponse().withStatus(200).withBody(response))); 147 | 148 | final ClusterInfo actual = underTest.discoverCluster(); 149 | Assert.assertEquals( 150 | new ClusterInfo("sample-cluster.reader-endpoint", ImmutableSet.of()), actual); 151 | } 152 | 153 | @Test(expected = RuntimeException.class) 154 | public void serverErrorDescribingCluster() throws URISyntaxException { 155 | AwsApiReplicasFinder underTest = 156 | new AwsApiReplicasFinder( 157 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of()), 158 | new FairlinkConnectionString( 159 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 160 | this.baseTestProperties())); 161 | 162 | stubFor(post(urlEqualTo("/")).willReturn(aResponse().withStatus(500))); 163 | 164 | underTest.discoverCluster(); 165 | } 166 | 167 | @Test 168 | public void serverErrorDescribingInstances() throws URISyntaxException, IOException { 169 | AwsApiReplicasFinder underTest = 170 | new AwsApiReplicasFinder( 171 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of()), 172 | new FairlinkConnectionString( 173 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 174 | this.baseTestProperties())); 175 | 176 | String describeClusterResponse = 177 | CharStreams.toString( 178 | new InputStreamReader( 179 | AwsApiReplicasFinderTest.class 180 | .getClassLoader() 181 | .getResourceAsStream( 182 | "technology/dice/dicefairlink/discovery/members/awsapi/withMembers.xml"), 183 | Charsets.UTF_8)); 184 | 185 | stubFor( 186 | post(urlEqualTo("/")) 187 | .withRequestBody(WireMock.containing("Action=DescribeDBClusters")) 188 | .willReturn(aResponse().withStatus(200).withBody(describeClusterResponse))); 189 | 190 | stubFor( 191 | post(urlEqualTo("/")) 192 | .withRequestBody(WireMock.containing("Action=DescribeDBInstances")) 193 | .willReturn(aResponse().withStatus(500))); 194 | 195 | final ClusterInfo actual = underTest.discoverCluster(); 196 | Assert.assertEquals(new ClusterInfo("cluster-reader-endpoint", ImmutableSet.of()), actual); 197 | } 198 | 199 | @Test(expected = RuntimeException.class) 200 | public void clusterNotFound() throws URISyntaxException, IOException { 201 | AwsApiReplicasFinder underTest = 202 | new AwsApiReplicasFinder( 203 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of()), 204 | new FairlinkConnectionString( 205 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 206 | this.baseTestProperties())); 207 | 208 | String response = 209 | CharStreams.toString( 210 | new InputStreamReader( 211 | AwsApiReplicasFinderTest.class 212 | .getClassLoader() 213 | .getResourceAsStream( 214 | "technology/dice/dicefairlink/discovery/members/awsapi/emptyResponse.xml"), 215 | Charsets.UTF_8)); 216 | 217 | stubFor(post(urlEqualTo("/")).willReturn(aResponse().withStatus(200).withBody(response))); 218 | 219 | underTest.discoverCluster(); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/members/awsapi/AwsApiReplicasFinderTestIT.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.members.awsapi; 2 | 3 | import org.junit.Test; 4 | import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; 5 | import software.amazon.awssdk.regions.Region; 6 | import software.amazon.awssdk.services.rds.RdsClient; 7 | import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; 8 | import software.amazon.awssdk.services.rds.model.DescribeDbInstancesRequest; 9 | import software.amazon.awssdk.services.rds.model.Filter; 10 | 11 | public class AwsApiReplicasFinderTestIT { 12 | // Here to capture the http wire traffic. Not automatically ran as part of CI 13 | @Test 14 | public void makeDescribeApiCalls() { 15 | final RdsClient client = 16 | RdsClient.builder() 17 | .region(Region.EU_WEST_1) 18 | .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) 19 | .build(); 20 | 21 | client.describeDBClusters( 22 | DescribeDbClustersRequest.builder() 23 | .dbClusterIdentifier("cluster") 24 | .build()); 25 | 26 | client.describeDBInstances( 27 | DescribeDbInstancesRequest.builder() 28 | .filters( 29 | Filter.builder().name("db-cluster-id").values("cluster").build()) 30 | .build()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/members/sql/MySqlReplicasFinderTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.members.sql; 2 | 3 | import com.google.common.base.Charsets; 4 | import com.google.common.io.CharStreams; 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.testcontainers.containers.JdbcDatabaseContainer; 10 | import org.testcontainers.containers.MySQLContainer; 11 | import org.testcontainers.shaded.com.google.common.collect.ImmutableSet; 12 | import technology.dice.dicefairlink.discovery.members.ClusterInfo; 13 | import technology.dice.dicefairlink.driver.FairlinkConnectionString; 14 | 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.InputStreamReader; 18 | import java.net.URISyntaxException; 19 | import java.sql.Connection; 20 | import java.sql.DriverManager; 21 | import java.sql.SQLException; 22 | import java.util.Properties; 23 | 24 | public class MySqlReplicasFinderTest { 25 | @Rule public JdbcDatabaseContainer mysql = new MySQLContainer(); 26 | 27 | @Before 28 | public void before() throws IOException, SQLException { 29 | this.runScript("technology/dice/dicefairlink/discovery/members/sql/mysql/schema.sql"); 30 | } 31 | 32 | private void runScript(String path) throws IOException, SQLException { 33 | Properties p = new Properties(); 34 | p.setProperty("allowMultiQueries", "true"); 35 | p.setProperty("user", mysql.getUsername()); 36 | p.setProperty("password", mysql.getPassword()); 37 | final InputStream resourceAsStream = 38 | MySqlReplicasFinderTest.class.getClassLoader().getResourceAsStream(path); 39 | final String schemaSql = 40 | CharStreams.toString(new InputStreamReader(resourceAsStream, Charsets.UTF_8)); 41 | Connection conn = DriverManager.getConnection(mysql.getJdbcUrl(), p); 42 | 43 | conn.createStatement().executeUpdate(schemaSql); 44 | conn.close(); 45 | } 46 | 47 | private Properties baseTestProperties() { 48 | Properties p = new Properties(); 49 | p.setProperty("auroraClusterRegion", "eu-west-1"); 50 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 51 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 52 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 53 | p.setProperty("discoveryMode", "irrelevant"); 54 | p.setProperty("replicaPollInterval", "5"); 55 | p.setProperty("tagsPollInterval", "10"); 56 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 57 | p.setProperty("validateConnection", "true"); 58 | p.setProperty("user", mysql.getUsername()); 59 | p.setProperty("password", mysql.getPassword()); 60 | return p; 61 | } 62 | 63 | @Test 64 | public void noReplicas() throws URISyntaxException { 65 | MySQLReplicasFinder underTest = 66 | new MySQLReplicasFinder( 67 | new FairlinkConnectionString( 68 | mysql.getJdbcUrl().replace("mysql", "fairlink:mysql"), this.baseTestProperties()), 69 | mysql.getJdbcDriverInstance(), 70 | mysql.getDatabaseName()); 71 | final ClusterInfo clusterInfo = underTest.discoverCluster(); 72 | Assert.assertEquals(new ClusterInfo(mysql.getJdbcUrl(), ImmutableSet.of()), clusterInfo); 73 | } 74 | 75 | @Test 76 | public void withReplicas() throws URISyntaxException, IOException, SQLException { 77 | this.runScript("technology/dice/dicefairlink/discovery/members/sql/mysql/2replicas.sql"); 78 | MySQLReplicasFinder underTest = 79 | new MySQLReplicasFinder( 80 | new FairlinkConnectionString( 81 | mysql.getJdbcUrl().replace("mysql", "fairlink:mysql"), this.baseTestProperties()), 82 | mysql.getJdbcDriverInstance(), 83 | mysql.getDatabaseName()); 84 | final ClusterInfo actual = underTest.discoverCluster(); 85 | final ClusterInfo expected = new ClusterInfo(mysql.getJdbcUrl(), ImmutableSet.of("replica")); 86 | Assert.assertEquals(expected, actual); 87 | } 88 | 89 | @Test 90 | public void noConnectionsResultsInEmptySet() 91 | throws URISyntaxException, IOException, SQLException { 92 | this.runScript("technology/dice/dicefairlink/discovery/members/sql/mysql/2replicas.sql"); 93 | MySQLReplicasFinder underTest = 94 | new MySQLReplicasFinder( 95 | new FairlinkConnectionString( 96 | mysql.getJdbcUrl().replace("mysql", "fairlink:fairlinktestdriver"), 97 | this.baseTestProperties()), 98 | mysql.getJdbcDriverInstance(), 99 | mysql.getDatabaseName()); 100 | final ClusterInfo actual = underTest.discoverCluster(); 101 | final ClusterInfo expected = 102 | new ClusterInfo( 103 | mysql.getJdbcUrl().replace("mysql", "fairlinktestdriver"), ImmutableSet.of()); 104 | Assert.assertEquals(expected, actual); 105 | } 106 | 107 | @Test 108 | public void noTableResultsInEmptySet() throws URISyntaxException, IOException, SQLException { 109 | this.runScript("technology/dice/dicefairlink/discovery/members/sql/mysql/2replicas.sql"); 110 | MySQLReplicasFinder underTest = 111 | new MySQLReplicasFinder( 112 | new FairlinkConnectionString( 113 | mysql.getJdbcUrl().replace("mysql", "fairlink:mysql"), this.baseTestProperties()), 114 | mysql.getJdbcDriverInstance(), 115 | "i_do_not_exist"); 116 | final ClusterInfo actual = underTest.discoverCluster(); 117 | final ClusterInfo expected = new ClusterInfo(mysql.getJdbcUrl(), ImmutableSet.of()); 118 | Assert.assertEquals(expected, actual); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/members/sql/PostgresSqlReplicaFinderTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.members.sql; 2 | 3 | import com.google.common.base.Charsets; 4 | import com.google.common.io.CharStreams; 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.testcontainers.containers.PostgreSQLContainer; 10 | import org.testcontainers.shaded.com.google.common.collect.ImmutableSet; 11 | import technology.dice.dicefairlink.discovery.members.ClusterInfo; 12 | import technology.dice.dicefairlink.driver.FairlinkConnectionString; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.InputStreamReader; 17 | import java.net.URISyntaxException; 18 | import java.sql.Connection; 19 | import java.sql.DriverManager; 20 | import java.sql.SQLException; 21 | import java.util.Properties; 22 | 23 | /** 24 | * @author Ryan Gardner 25 | * @date 9/13/19 26 | */ 27 | public class PostgresSqlReplicaFinderTest { 28 | @Rule public PostgreSQLContainer postgres = new PostgreSQLContainer(); 29 | 30 | 31 | @Before 32 | public void before() throws IOException, SQLException { 33 | this.runScript("technology/dice/dicefairlink/discovery/members/sql/postgresql/schema.sql"); 34 | } 35 | 36 | private void runScript(String path) throws IOException, SQLException { 37 | Properties p = new Properties(); 38 | p.setProperty("allowMultiQueries", "true"); 39 | p.setProperty("user", postgres.getUsername()); 40 | p.setProperty("password", postgres.getPassword()); 41 | final InputStream resourceAsStream = 42 | PostgresSqlReplicaFinderTest.class.getClassLoader().getResourceAsStream(path); 43 | final String schemaSql = 44 | CharStreams.toString(new InputStreamReader(resourceAsStream, Charsets.UTF_8)); 45 | Connection conn = DriverManager.getConnection(postgres.getJdbcUrl(), p); 46 | 47 | conn.createStatement().executeUpdate(schemaSql); 48 | conn.close(); 49 | } 50 | 51 | private Properties baseTestProperties() { 52 | Properties p = new Properties(); 53 | p.setProperty("auroraClusterRegion", "eu-west-1"); 54 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 55 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 56 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 57 | p.setProperty("discoveryMode", "irrelevant"); 58 | p.setProperty("replicaPollInterval", "5"); 59 | p.setProperty("tagsPollInterval", "10"); 60 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 61 | p.setProperty("validateConnection", "true"); 62 | p.setProperty("user", postgres.getUsername()); 63 | p.setProperty("password", postgres.getPassword()); 64 | return p; 65 | } 66 | 67 | @Test 68 | public void noReplicas() throws URISyntaxException { 69 | PostgresSQLReplicasFinder underTest = 70 | new PostgresSQLReplicasFinder( 71 | new FairlinkConnectionString( 72 | postgres.getJdbcUrl().replace("postgresql", "fairlink:postgresql"), this.baseTestProperties()), 73 | postgres.getJdbcDriverInstance()); 74 | final ClusterInfo clusterInfo = underTest.discoverCluster(); 75 | Assert.assertEquals(new ClusterInfo(postgres.getJdbcUrl(), ImmutableSet.of()), clusterInfo); 76 | } 77 | 78 | @Test 79 | public void withReplicas() throws URISyntaxException, IOException, SQLException { 80 | this.runScript("technology/dice/dicefairlink/discovery/members/sql/postgresql/2replicas.sql"); 81 | PostgresSQLReplicasFinder underTest = 82 | new PostgresSQLReplicasFinder( 83 | new FairlinkConnectionString( 84 | postgres.getJdbcUrl().replace("postgresql", "fairlink:postgresql"), this.baseTestProperties()), 85 | postgres.getJdbcDriverInstance()); 86 | final ClusterInfo actual = underTest.discoverCluster(); 87 | final ClusterInfo expected = new ClusterInfo(postgres.getJdbcUrl(), ImmutableSet.of("replica")); 88 | Assert.assertEquals(expected, actual); 89 | } 90 | 91 | @Test 92 | public void noConnectionsResultsInEmptySet() 93 | throws URISyntaxException, IOException, SQLException { 94 | this.runScript("technology/dice/dicefairlink/discovery/members/sql/postgresql/2replicas.sql"); 95 | PostgresSQLReplicasFinder underTest = 96 | new PostgresSQLReplicasFinder( 97 | new FairlinkConnectionString( 98 | postgres.getJdbcUrl().replace("postgresql", "fairlink:fairlinktestdriver"), 99 | this.baseTestProperties()), 100 | postgres.getJdbcDriverInstance()); 101 | final ClusterInfo actual = underTest.discoverCluster(); 102 | final ClusterInfo expected = 103 | new ClusterInfo( 104 | postgres.getJdbcUrl().replace("postgresql", "fairlinktestdriver"), ImmutableSet.of()); 105 | Assert.assertEquals(expected, actual); 106 | } 107 | 108 | @Test 109 | public void noFunctionResultsInEmptySet() throws URISyntaxException, IOException, SQLException { 110 | this.runScript("technology/dice/dicefairlink/discovery/members/sql/postgresql/drop_function.sql"); 111 | PostgresSQLReplicasFinder underTest = 112 | new PostgresSQLReplicasFinder( 113 | new FairlinkConnectionString( 114 | postgres.getJdbcUrl().replace("postgresql", "fairlink:postgresql"), this.baseTestProperties()), 115 | postgres.getJdbcDriverInstance()); 116 | final ClusterInfo actual = underTest.discoverCluster(); 117 | final ClusterInfo expected = new ClusterInfo(postgres.getJdbcUrl(), ImmutableSet.of()); 118 | Assert.assertEquals(expected, actual); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/tags/awsapi/ResourceGroupApiResponse.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.tags.awsapi; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | 6 | public class ResourceGroupApiResponse { 7 | public static class Resource { 8 | public static class Tag { 9 | private final String key; 10 | private final String value; 11 | 12 | public Tag(String key, String value) { 13 | this.key = key; 14 | this.value = value; 15 | } 16 | 17 | @JsonProperty("Key") 18 | public String getKey() { 19 | return key; 20 | } 21 | 22 | @JsonProperty("Value") 23 | public String getValue() { 24 | return value; 25 | } 26 | } 27 | 28 | private final String resourceARN; 29 | private final List tags; 30 | 31 | public Resource(String resourceARN, List tags) { 32 | this.resourceARN = resourceARN; 33 | this.tags = tags; 34 | } 35 | 36 | @JsonProperty("ResourceARN") 37 | public String getResourceARN() { 38 | return resourceARN; 39 | } 40 | 41 | @JsonProperty("Tags") 42 | public List getTags() { 43 | return tags; 44 | } 45 | } 46 | 47 | private final List taggedResources; 48 | private final String taginationToken; 49 | 50 | public ResourceGroupApiResponse(List taggedResources, String taginationToken) { 51 | this.taggedResources = taggedResources; 52 | this.taginationToken = taginationToken; 53 | } 54 | 55 | @JsonProperty("ResourceTagMappingList") 56 | public List getTaggedResources() { 57 | return taggedResources; 58 | } 59 | 60 | @JsonProperty("PaginationToken") 61 | public String getTaginationToken() { 62 | return taginationToken; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/tags/awsapi/ResourceGroupApiTagDiscoveryTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.tags.awsapi; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 4 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 7 | 8 | import com.fasterxml.jackson.core.JsonProcessingException; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 11 | import com.google.common.collect.ImmutableList; 12 | import com.google.common.collect.ImmutableMap; 13 | import com.google.common.collect.ImmutableSet; 14 | import java.util.Properties; 15 | import java.util.Set; 16 | import org.junit.Assert; 17 | import org.junit.Rule; 18 | import org.junit.Test; 19 | import technology.dice.dicefairlink.config.FairlinkConfiguration; 20 | import technology.dice.dicefairlink.discovery.tags.ExclusionTag; 21 | import technology.dice.dicefairlink.discovery.tags.awsapi.ResourceGroupApiResponse.Resource; 22 | import technology.dice.dicefairlink.discovery.tags.awsapi.ResourceGroupApiResponse.Resource.Tag; 23 | 24 | public class ResourceGroupApiTagDiscoveryTest { 25 | @Rule public WireMockRule wireMockRule = new WireMockRule(11342); 26 | private final ObjectMapper mapper = new ObjectMapper(); 27 | 28 | private Properties baseTestProperties() { 29 | Properties p = new Properties(); 30 | p.setProperty("auroraClusterRegion", "eu-west-1"); 31 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 32 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 33 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 34 | p.setProperty("discoveryMode", "AWS_API"); 35 | p.setProperty("replicaPollInterval", "5"); 36 | p.setProperty("tagsPollInterval", "10"); 37 | p.setProperty("replicaEndpointTemplate", "%s.rest-of-myhost.name"); 38 | p.setProperty("validateConnection", "true"); 39 | p.setProperty("awsEndpointOverride", "http://localhost:11342"); 40 | return p; 41 | } 42 | 43 | @Test 44 | public void withExclusions() throws JsonProcessingException { 45 | ResourceGroupApiTagDiscovery underTest = 46 | new ResourceGroupApiTagDiscovery( 47 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of())); 48 | 49 | stubFor( 50 | post(urlEqualTo("/")) 51 | .willReturn( 52 | aResponse() 53 | .withStatus(200) 54 | .withBody( 55 | this.stringifyResponse( 56 | new ResourceGroupApiResponse( 57 | ImmutableList.of( 58 | new Resource( 59 | "arn1", 60 | ImmutableList.of( 61 | new Tag("k", "v"), 62 | new Tag("Fairlink-Exclude", "true")))), 63 | ""))))); 64 | 65 | final Set actual = 66 | underTest.listExcludedInstances(new ExclusionTag("FairlinkConfiguration", "true")); 67 | Assert.assertEquals(ImmutableSet.of("arn1"), actual); 68 | } 69 | 70 | @Test 71 | public void noExclusions() throws JsonProcessingException { 72 | ResourceGroupApiTagDiscovery underTest = 73 | new ResourceGroupApiTagDiscovery( 74 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of())); 75 | 76 | stubFor( 77 | post(urlEqualTo("/")) 78 | .willReturn( 79 | aResponse() 80 | .withStatus(200) 81 | .withBody( 82 | this.stringifyResponse( 83 | new ResourceGroupApiResponse(ImmutableList.of(), ""))))); 84 | 85 | final Set actual = 86 | underTest.listExcludedInstances(new ExclusionTag("FairlinkConfiguration", "true")); 87 | Assert.assertEquals(ImmutableSet.of(), actual); 88 | } 89 | 90 | @Test 91 | public void exceptionAssumesEmpty() { 92 | ResourceGroupApiTagDiscovery underTest = 93 | new ResourceGroupApiTagDiscovery( 94 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of())); 95 | 96 | stubFor(post(urlEqualTo("/")).willReturn(aResponse().withStatus(500))); 97 | 98 | final Set actual = 99 | underTest.listExcludedInstances(new ExclusionTag("FairlinkConfiguration", "true")); 100 | Assert.assertEquals(ImmutableSet.of(), actual); 101 | } 102 | 103 | @Test 104 | public void noInstancesFound() { 105 | ResourceGroupApiTagDiscovery underTest = 106 | new ResourceGroupApiTagDiscovery( 107 | new FairlinkConfiguration(this.baseTestProperties(), ImmutableMap.of())); 108 | 109 | stubFor(post(urlEqualTo("/")).willReturn(aResponse().withStatus(404))); 110 | 111 | final Set actual = 112 | underTest.listExcludedInstances(new ExclusionTag("FairlinkConfiguration", "true")); 113 | Assert.assertEquals(ImmutableSet.of(), actual); 114 | } 115 | 116 | private String stringifyResponse(ResourceGroupApiResponse response) 117 | throws JsonProcessingException { 118 | return this.mapper.writeValueAsString(response); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/discovery/tags/awsapi/ResourceGroupApiTagDiscoveryTestIT.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.discovery.tags.awsapi; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import java.util.stream.Collectors; 5 | import org.junit.Test; 6 | import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; 7 | import software.amazon.awssdk.regions.Region; 8 | import software.amazon.awssdk.services.resourcegroupstaggingapi.ResourceGroupsTaggingApiClient; 9 | import software.amazon.awssdk.services.resourcegroupstaggingapi.model.GetResourcesRequest; 10 | 11 | public class ResourceGroupApiTagDiscoveryTestIT { 12 | 13 | // Here to capture the http wire traffic. Not automatically ran as part of CI 14 | @Test 15 | public void makeAPicall() { 16 | final ResourceGroupsTaggingApiClient client = 17 | ResourceGroupsTaggingApiClient.builder() 18 | .region(Region.EU_WEST_1) 19 | .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) 20 | .build(); 21 | 22 | GetResourcesRequest request = 23 | GetResourcesRequest.builder() 24 | .resourceTypeFilters(ImmutableList.of("rds:db")) 25 | .tagFilters( 26 | software.amazon.awssdk.services.resourcegroupstaggingapi.model.TagFilter.builder() 27 | .key("Fairlink-Exclude") 28 | .values("true") 29 | .build()) 30 | .build(); 31 | 32 | client.getResourcesPaginator(request).stream().collect(Collectors.toSet()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/driver/AuroraReadReplicasDriverEndToEndTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.driver; 2 | 3 | import com.google.common.base.Charsets; 4 | import com.google.common.base.Function; 5 | import com.google.common.collect.ImmutableMap; 6 | import com.google.common.collect.ImmutableSet; 7 | import com.google.common.io.CharStreams; 8 | import java.net.URISyntaxException; 9 | import org.checkerframework.checker.nullness.qual.Nullable; 10 | import org.junit.Assert; 11 | import org.junit.Before; 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | import org.testcontainers.containers.MySQLContainer; 15 | import technology.dice.dicefairlink.StepByStepExecutor; 16 | import technology.dice.dicefairlink.discovery.members.sql.MySqlReplicasFinderTest; 17 | import technology.dice.dicefairlink.iterators.CyclicIterator; 18 | import technology.dice.dicefairlink.iterators.SizedIterator; 19 | import technology.dice.dicefairlink.support.discovery.tags.FixedSetExcludedReplicasFinder; 20 | import technology.dice.dicefairlink.support.iterators.TestCyclicIterator; 21 | 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.InputStreamReader; 25 | import java.sql.Connection; 26 | import java.sql.DriverManager; 27 | import java.sql.ResultSet; 28 | import java.sql.SQLException; 29 | import java.util.ArrayList; 30 | import java.util.Collection; 31 | import java.util.List; 32 | import java.util.Map; 33 | import java.util.Properties; 34 | 35 | public class AuroraReadReplicasDriverEndToEndTest { 36 | @Rule public MySQLContainer master = new MySQLContainer(); 37 | @Rule public MySQLContainer slave1 = new MySQLContainer(); 38 | @Rule public MySQLContainer slave2 = new MySQLContainer(); 39 | 40 | private void runScript(MySQLContainer container, String path) throws IOException, SQLException { 41 | this.runScript(container, path, ImmutableMap.of()); 42 | } 43 | 44 | private void runScript(MySQLContainer container, String path, Map replacing) 45 | throws IOException, SQLException { 46 | Properties p = new Properties(); 47 | p.setProperty("allowMultiQueries", "true"); 48 | p.setProperty("user", container.getUsername()); 49 | p.setProperty("password", container.getPassword()); 50 | final InputStream resourceAsStream = 51 | MySqlReplicasFinderTest.class.getClassLoader().getResourceAsStream(path); 52 | String schemaSql = 53 | CharStreams.toString(new InputStreamReader(resourceAsStream, Charsets.UTF_8)); 54 | for (Map.Entry replace : replacing.entrySet()) { 55 | schemaSql = schemaSql.replace(replace.getKey(), replace.getValue()); 56 | } 57 | Connection conn = DriverManager.getConnection(container.getJdbcUrl(), p); 58 | 59 | conn.createStatement().executeUpdate(schemaSql); 60 | conn.close(); 61 | } 62 | 63 | @Before 64 | public void before() throws IOException, SQLException { 65 | this.runScript(master, "technology/dice/dicefairlink/discovery/members/sql/mysql/schema.sql"); 66 | this.runScript(slave1, "technology/dice/dicefairlink/discovery/members/sql/mysql/schema.sql"); 67 | this.runScript(slave2, "technology/dice/dicefairlink/discovery/members/sql/mysql/schema.sql"); 68 | this.runScript( 69 | master, 70 | "technology/dice/dicefairlink/discovery/members/sql/mysql/3replicasPorts.sql", 71 | ImmutableMap.of( 72 | "__PORT_SLAVE_1__", 73 | slave1.getFirstMappedPort().toString(), 74 | "__PORT_SLAVE_2__", 75 | slave2.getFirstMappedPort().toString())); 76 | this.runScript(master, "technology/dice/dicefairlink/discovery/members/sql/mysql/masterData.sql"); 77 | this.runScript(slave1, "technology/dice/dicefairlink/discovery/members/sql/mysql/slave1Data.sql"); 78 | this.runScript(slave2, "technology/dice/dicefairlink/discovery/members/sql/mysql/slave2Data.sql"); 79 | } 80 | 81 | private StepByStepExecutor exclusionTagsExecutor; 82 | private StepByStepExecutor memberDiscoveryExecutor; 83 | 84 | private Properties baseTestProperties() { 85 | Properties p = new Properties(); 86 | p.setProperty("auroraClusterRegion", "eu-west-1"); 87 | p.setProperty("auroraDiscoveryAuthMode", "basic"); 88 | p.setProperty("auroraDiscoveryKeyId", "keyId"); 89 | p.setProperty("discoveryMode", "SQL_MYSQL"); 90 | p.setProperty("auroraDiscoverKeySecret", "keySecret"); 91 | p.setProperty("replicaPollInterval", "5"); 92 | p.setProperty("tagsPollInterval", "10"); 93 | p.setProperty("replicaEndpointTemplate", "%s"); 94 | p.setProperty("validateConnection", "true"); 95 | p.setProperty("_fairlinkMySQLSchemaOverride", "test"); 96 | p.setProperty("user", master.getUsername()); 97 | p.setProperty("password", master.getPassword()); 98 | return p; 99 | } 100 | 101 | @Before 102 | public void setup() { 103 | this.exclusionTagsExecutor = new StepByStepExecutor(1); 104 | this.memberDiscoveryExecutor = new StepByStepExecutor(1); 105 | } 106 | 107 | @Test 108 | public void readFromTwoReplicas() throws SQLException { 109 | CyclicIterator[] i = new CyclicIterator[1]; 110 | AuroraReadReplicasDriver underTest = 111 | new AuroraReadReplicasDriver( 112 | () -> memberDiscoveryExecutor, 113 | () -> exclusionTagsExecutor, 114 | () -> new FixedSetExcludedReplicasFinder(ImmutableSet.of("replica1", "replica3")), 115 | null, 116 | null, 117 | new Function, SizedIterator>() { 118 | @Nullable 119 | @Override 120 | public SizedIterator apply(@Nullable Collection strings) { 121 | final CyclicIterator iterator = TestCyclicIterator.of(strings); 122 | i[0] = iterator; 123 | return iterator; 124 | } 125 | }); 126 | 127 | List foundData = readTwice(underTest); 128 | Assert.assertEquals(2, i[0].size()); 129 | Assert.assertEquals(4, foundData.size()); 130 | Assert.assertTrue( 131 | foundData.stream() 132 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE1")) 133 | .findFirst() 134 | .isPresent()); 135 | Assert.assertTrue( 136 | foundData.stream() 137 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE2")) 138 | .findFirst() 139 | .isPresent()); 140 | 141 | final Connection masterConnection = 142 | DriverManager.getConnection(master.getJdbcUrl(), baseTestProperties()); 143 | masterConnection 144 | .createStatement() 145 | .executeUpdate("DELETE from replica_host_status WHERE SESSION_ID='another-sesion2'"); 146 | masterConnection.close(); 147 | 148 | memberDiscoveryExecutor.step(); 149 | foundData = readTwice(underTest); 150 | 151 | Assert.assertEquals(1, i[0].size()); 152 | Assert.assertEquals(4, foundData.size()); 153 | Assert.assertEquals( 154 | 2, foundData.stream().filter(d -> d.getB().equalsIgnoreCase("SLAVE1")).count()); 155 | Assert.assertFalse( 156 | foundData.stream() 157 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE2")) 158 | .findFirst() 159 | .isPresent()); 160 | } 161 | 162 | @Test 163 | public void refreshBeforeScheduledDiscovery() throws SQLException, URISyntaxException { 164 | CyclicIterator[] i = new CyclicIterator[1]; 165 | AuroraReadReplicasDriver underTest = 166 | new AuroraReadReplicasDriver( 167 | () -> memberDiscoveryExecutor, 168 | () -> exclusionTagsExecutor, 169 | () -> new FixedSetExcludedReplicasFinder(ImmutableSet.of("replica1", "replica3")), 170 | null, 171 | null, 172 | new Function, SizedIterator>() { 173 | @Nullable 174 | @Override 175 | public SizedIterator apply(@Nullable Collection strings) { 176 | final CyclicIterator iterator = TestCyclicIterator.of(strings); 177 | i[0] = iterator; 178 | return iterator; 179 | } 180 | }); 181 | 182 | List foundData = readTwice(underTest); 183 | Assert.assertEquals(2, i[0].size()); 184 | Assert.assertEquals(4, foundData.size()); 185 | Assert.assertTrue( 186 | foundData.stream() 187 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE1")) 188 | .findFirst() 189 | .isPresent()); 190 | Assert.assertTrue( 191 | foundData.stream() 192 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE2")) 193 | .findFirst() 194 | .isPresent()); 195 | 196 | final Connection masterConnection = 197 | DriverManager.getConnection(master.getJdbcUrl(), baseTestProperties()); 198 | masterConnection 199 | .createStatement() 200 | .executeUpdate("DELETE from replica_host_status WHERE SESSION_ID='another-sesion2'"); 201 | masterConnection.close(); 202 | 203 | underTest.refreshReplicas(master.getJdbcUrl().replace("mysql", "fairlink:mysql")); 204 | foundData = readTwice(underTest); 205 | 206 | Assert.assertEquals(1, i[0].size()); 207 | Assert.assertEquals(4, foundData.size()); 208 | Assert.assertEquals( 209 | 2, foundData.stream().filter(d -> d.getB().equalsIgnoreCase("SLAVE1")).count()); 210 | Assert.assertFalse( 211 | foundData.stream() 212 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE2")) 213 | .findFirst() 214 | .isPresent()); 215 | } 216 | 217 | @Test 218 | public void refreshUnexistingEndpoint() throws SQLException, URISyntaxException { 219 | CyclicIterator[] i = new CyclicIterator[1]; 220 | AuroraReadReplicasDriver underTest = 221 | new AuroraReadReplicasDriver( 222 | () -> memberDiscoveryExecutor, 223 | () -> exclusionTagsExecutor, 224 | () -> new FixedSetExcludedReplicasFinder(ImmutableSet.of("replica1", "replica3")), 225 | null, 226 | null, 227 | new Function, SizedIterator>() { 228 | @Nullable 229 | @Override 230 | public SizedIterator apply(@Nullable Collection strings) { 231 | final CyclicIterator iterator = TestCyclicIterator.of(strings); 232 | i[0] = iterator; 233 | return iterator; 234 | } 235 | }); 236 | 237 | List foundData = readTwice(underTest); 238 | Assert.assertEquals(2, i[0].size()); 239 | Assert.assertEquals(4, foundData.size()); 240 | Assert.assertTrue( 241 | foundData.stream() 242 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE1")) 243 | .findFirst() 244 | .isPresent()); 245 | Assert.assertTrue( 246 | foundData.stream() 247 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE2")) 248 | .findFirst() 249 | .isPresent()); 250 | 251 | final Connection masterConnection = 252 | DriverManager.getConnection(master.getJdbcUrl(), baseTestProperties()); 253 | masterConnection 254 | .createStatement() 255 | .executeUpdate("DELETE from replica_host_status WHERE SESSION_ID='another-sesion2'"); 256 | masterConnection.close(); 257 | 258 | underTest.refreshReplicas(slave1.getJdbcUrl().replace("mysql", "fairlink:mysql")); 259 | foundData = readTwice(underTest); 260 | 261 | Assert.assertEquals(2, i[0].size()); 262 | Assert.assertEquals(4, foundData.size()); 263 | Assert.assertTrue( 264 | foundData.stream() 265 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE1")) 266 | .findFirst() 267 | .isPresent()); 268 | Assert.assertTrue( 269 | foundData.stream() 270 | .filter(d -> d.getB().equalsIgnoreCase("SLAVE2")) 271 | .findFirst() 272 | .isPresent()); 273 | } 274 | 275 | private List readTwice(AuroraReadReplicasDriver underTest) throws SQLException { 276 | List foundData = new ArrayList(); 277 | Connection connect = 278 | underTest.connect( 279 | master.getJdbcUrl().replace("mysql", "fairlink:mysql"), this.baseTestProperties()); 280 | ResultSet resultSet = connect.createStatement().executeQuery("SELECT * FROM data"); 281 | while (resultSet.next()) { 282 | Data data = new Data(resultSet.getString("a"), resultSet.getString("b")); 283 | foundData.add(data); 284 | } 285 | connect.close(); 286 | resultSet.close(); 287 | connect = 288 | underTest.connect( 289 | master.getJdbcUrl().replace("mysql", "fairlink:mysql"), this.baseTestProperties()); 290 | resultSet = connect.createStatement().executeQuery("SELECT * FROM data"); 291 | while (resultSet.next()) { 292 | Data data = new Data(resultSet.getString("a"), resultSet.getString("b")); 293 | foundData.add(data); 294 | } 295 | 296 | connect.close(); 297 | resultSet.close(); 298 | 299 | return foundData; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/driver/AuroraReadReplicasDriverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.driver; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | import java.sql.DriverManager; 11 | import java.sql.DriverPropertyInfo; 12 | import java.sql.SQLException; 13 | import java.util.NoSuchElementException; 14 | import java.util.Properties; 15 | import java.util.logging.Logger; 16 | import org.junit.Assert; 17 | import org.junit.BeforeClass; 18 | import org.junit.Test; 19 | import technology.dice.dicefairlink.support.driver.TestDriver; 20 | 21 | public class AuroraReadReplicasDriverTest { 22 | private static final String VALID_JDBC_URL = 23 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc"; 24 | 25 | @BeforeClass 26 | public static void setupClass() throws SQLException { 27 | DriverManager.registerDriver(new TestDriver()); 28 | } 29 | 30 | @Test 31 | public void driverInterfaceLock() { 32 | final AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 33 | Assert.assertFalse(underTest.jdbcCompliant()); 34 | Assert.assertEquals(2, underTest.getMajorVersion()); 35 | Assert.assertEquals(1, underTest.getMinorVersion()); 36 | Assert.assertArrayEquals( 37 | new DriverPropertyInfo[] {}, underTest.getPropertyInfo(VALID_JDBC_URL, new Properties())); 38 | Assert.assertEquals(Logger.getLogger(""), underTest.getParentLogger()); 39 | } 40 | 41 | @Test 42 | public void accepts() throws SQLException { 43 | final AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 44 | Assert.assertTrue(underTest.acceptsURL(VALID_JDBC_URL)); 45 | } 46 | 47 | @Test 48 | public void doesNotAccept() throws SQLException { 49 | final AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 50 | Assert.assertFalse( 51 | underTest.acceptsURL( 52 | "jdbc:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc")); 53 | } 54 | 55 | @Test(expected = SQLException.class) 56 | public void throwsOnNullAccepts() throws SQLException { 57 | final AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 58 | underTest.acceptsURL(null); 59 | } 60 | 61 | @Test(expected = SQLException.class) 62 | public void throwsOnAcceptsURL_nullString() throws Exception { 63 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 64 | underTest.acceptsURL(null); 65 | } 66 | 67 | @Test 68 | public void canAcceptsURL_emptyString() throws Exception { 69 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 70 | boolean retunedValue = underTest.acceptsURL(""); 71 | assertThat(retunedValue).isEqualTo(false); 72 | } 73 | 74 | @Test 75 | public void refuses_vanillaJdbc() throws Exception { 76 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 77 | boolean retunedValue = 78 | underTest.acceptsURL("jdbc:fairlinktestdriver://host:3306/id?useSSL=false"); 79 | assertThat(retunedValue).isEqualTo(false); 80 | } 81 | 82 | @Test 83 | public void canAcceptsURL_validString() throws Exception { 84 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 85 | boolean retunedValue = underTest.acceptsURL(VALID_JDBC_URL); 86 | assertThat(retunedValue).isEqualTo(true); 87 | } 88 | 89 | @Test(expected = NullPointerException.class) 90 | public void failToConnectToValidUrl_nullProperties() throws Exception { 91 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 92 | underTest.connect(VALID_JDBC_URL, null); // last call must throw 93 | } 94 | 95 | @Test(expected = IllegalStateException.class) 96 | public void failToConnectToValidUrl_emptyProperties_andNoRegionAvailable() throws Exception { 97 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 98 | underTest.connect(VALID_JDBC_URL, new Properties()); // last call must throw 99 | } 100 | 101 | @Test 102 | public void failedToConnectBadUrl() throws Exception { 103 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 104 | Assert.assertNull( 105 | underTest.connect("jdbc:fairlinktestdriver://host:3306/id?useSSL=false", new Properties())); 106 | } 107 | 108 | @Test 109 | public void failedToConnectIncompatibleDiscoveryMode() throws Exception { 110 | final Properties properties = new Properties(); 111 | properties.setProperty("discoveryMode", "SQL_MYSQL"); 112 | properties.setProperty("auroraClusterRegion", "eu-west-1"); 113 | properties.setProperty("replicaEndpointTemplate", "%s"); 114 | 115 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 116 | Assert.assertNull( 117 | underTest.connect( 118 | "jdbc:fairlink:fairlinktestdriver://host:3306/id?useSSL=false", properties)); 119 | } 120 | 121 | @Test 122 | public void testWellformedConnectionstringWithMalformedHost() throws Exception { 123 | AuroraReadReplicasDriver underTest = new AuroraReadReplicasDriver(); 124 | Assert.assertNull(underTest.connect("jdbc:fairlink:<>|/://host:3306/id?useSSL=false", new Properties())); 125 | } 126 | 127 | @Test 128 | public void staticLoad() throws ClassNotFoundException { 129 | Class.forName(AuroraReadReplicasDriver.class.getCanonicalName()); 130 | } 131 | 132 | @Test 133 | public void throwsNoSuchElementException() throws Exception { 134 | 135 | AuroraReadReplicasDriver underTest = 136 | new AuroraReadReplicasDriver( 137 | () -> {throw new NoSuchElementException();}, 138 | () -> {throw new NoSuchElementException();}, 139 | null, 140 | null, 141 | null, 142 | null 143 | ); 144 | Properties properties = new Properties(); 145 | properties.setProperty("discoveryMode", "AWS_API"); 146 | properties.setProperty("auroraClusterRegion", "eu-west-1"); 147 | properties.setProperty("replicaEndpointTemplate", "%s"); 148 | 149 | Assert.assertNull(underTest.connect("jdbc:fairlink:fairlinktestdriver://host:3306/id?useSSL=false", properties)); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/driver/Data.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.driver; 2 | 3 | public class Data { 4 | private final String a; 5 | private final String b; 6 | 7 | public Data(String a, String b) { 8 | this.a = a; 9 | this.b = b; 10 | } 11 | 12 | public String getA() { 13 | return a; 14 | } 15 | 16 | public String getB() { 17 | return b; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/driver/FairlinkConnectionStringTest.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.driver; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.net.URISyntaxException; 7 | import java.util.Properties; 8 | 9 | public class FairlinkConnectionStringTest { 10 | @Test 11 | public void valid() throws URISyntaxException { 12 | String connString = 13 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc"; 14 | final Properties properties = new Properties(); 15 | properties.setProperty("a", "b"); 16 | final FairlinkConnectionString underTest = new FairlinkConnectionString(connString, properties); 17 | Assert.assertEquals(connString, underTest.getFairlinkUri()); 18 | Assert.assertEquals("fairlinktestdriver", underTest.getDelegateProtocol()); 19 | Assert.assertEquals("aa", underTest.getHost()); 20 | Assert.assertEquals( 21 | "jdbc:fairlinktestdriver://anotherHost:123/db?param1=123¶m2=true¶m3=abc", 22 | underTest.delegateConnectionString("anotherHost")); 23 | Assert.assertEquals( 24 | "jdbc:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 25 | underTest.delegateConnectionString()); 26 | Assert.assertEquals(1, underTest.getProperties().size()); 27 | Assert.assertEquals("b", underTest.getProperties().getProperty("a")); 28 | } 29 | 30 | @Test 31 | public void validBackwardCompatibility() throws URISyntaxException { 32 | String connString = 33 | "jdbc:auroraro:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc"; 34 | final Properties properties = new Properties(); 35 | properties.setProperty("a", "b"); 36 | final FairlinkConnectionString underTest = new FairlinkConnectionString(connString, properties); 37 | Assert.assertEquals(connString, underTest.getFairlinkUri()); 38 | Assert.assertEquals("fairlinktestdriver", underTest.getDelegateProtocol()); 39 | Assert.assertEquals("aa", underTest.getHost()); 40 | Assert.assertEquals( 41 | "jdbc:fairlinktestdriver://anotherHost:123/db?param1=123¶m2=true¶m3=abc", 42 | underTest.delegateConnectionString("anotherHost")); 43 | Assert.assertEquals( 44 | "jdbc:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 45 | underTest.delegateConnectionString()); 46 | Assert.assertEquals(1, underTest.getProperties().size()); 47 | Assert.assertEquals("b", underTest.getProperties().getProperty("a")); 48 | } 49 | 50 | @Test 51 | public void validHostInDifferentPort() throws URISyntaxException { 52 | String connString = 53 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc"; 54 | final Properties properties = new Properties(); 55 | properties.setProperty("a", "b"); 56 | final FairlinkConnectionString underTest = new FairlinkConnectionString(connString, properties); 57 | Assert.assertEquals(connString, underTest.getFairlinkUri()); 58 | Assert.assertEquals("fairlinktestdriver", underTest.getDelegateProtocol()); 59 | Assert.assertEquals("aa", underTest.getHost()); 60 | Assert.assertEquals( 61 | "jdbc:fairlinktestdriver://anotherHost:999/db?param1=123¶m2=true¶m3=abc", 62 | underTest.delegateConnectionString("anotherHost:999")); 63 | Assert.assertEquals( 64 | "jdbc:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", 65 | underTest.delegateConnectionString()); 66 | Assert.assertEquals(1, underTest.getProperties().size()); 67 | Assert.assertEquals("b", underTest.getProperties().getProperty("a")); 68 | } 69 | 70 | @Test 71 | public void accepts() { 72 | Assert.assertTrue( 73 | FairlinkConnectionString.accepts( 74 | "jdbc:fairlink:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc")); 75 | } 76 | 77 | @Test 78 | public void acceptsBackwardsCompatibility() { 79 | Assert.assertTrue( 80 | FairlinkConnectionString.accepts( 81 | "jdbc:auroraro:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc")); 82 | } 83 | 84 | @Test 85 | public void doesNotAccept() { 86 | Assert.assertFalse( 87 | FairlinkConnectionString.accepts( 88 | "jdbc:something:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc")); 89 | Assert.assertFalse( 90 | FairlinkConnectionString.accepts( 91 | "jdbc:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc")); 92 | } 93 | 94 | @Test(expected = IllegalArgumentException.class) 95 | public void ctorBadConnString() throws URISyntaxException { 96 | new FairlinkConnectionString( 97 | "jdbc:fairlinktestdriver://aa:123/db?param1=123¶m2=true¶m3=abc", new Properties()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/iterators/RandomisedCyclicIteratorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 - present by Dice Technology Ltd. 3 | * 4 | * Please see distribution for license. 5 | */ 6 | package technology.dice.dicefairlink.iterators; 7 | 8 | import org.assertj.core.api.Assertions; 9 | import org.junit.Before; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.rules.TestName; 13 | 14 | import java.util.Collections; 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | import java.util.NoSuchElementException; 18 | import java.util.concurrent.ThreadLocalRandom; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | 22 | public class RandomisedCyclicIteratorTest { 23 | 24 | private static final int MIN_SIZE = 10; 25 | private static final int MAX_SIZE = 1_000; 26 | private static final String ELEMENT_PREFIX = "TEST_"; 27 | 28 | private int numberOfElementsToTest = -1; 29 | 30 | @Rule public TestName testName = new TestName(); 31 | 32 | @Before 33 | public void setUp() { 34 | numberOfElementsToTest = ThreadLocalRandom.current().nextInt(MIN_SIZE, MAX_SIZE); 35 | } 36 | 37 | @Test(expected = NoSuchElementException.class) 38 | public void canCallOfFromEmptyList() { 39 | RandomisedCyclicIterator cyclicIterator = RandomisedCyclicIterator.of(Collections.EMPTY_LIST); 40 | 41 | Assertions.assertThat(cyclicIterator).isNotNull(); 42 | 43 | assertThat(cyclicIterator.hasNext()).isFalse(); 44 | cyclicIterator.next(); // final step throws 45 | } 46 | 47 | @Test(expected = NoSuchElementException.class) 48 | public void canCallOfFromEmptySet() { 49 | RandomisedCyclicIterator cyclicIterator = RandomisedCyclicIterator.of(Collections.EMPTY_SET); 50 | 51 | Assertions.assertThat(cyclicIterator).isNotNull(); 52 | 53 | assertThat(cyclicIterator.hasNext()).isFalse(); 54 | cyclicIterator.next(); // final step throws 55 | } 56 | 57 | @Test 58 | public void canOperateWithListOfSingleElement() { 59 | final String singleElement = ELEMENT_PREFIX + "_1"; 60 | 61 | final List listOfStrings = new LinkedList<>(); 62 | listOfStrings.add(singleElement); 63 | 64 | RandomisedCyclicIterator cyclicIterator = RandomisedCyclicIterator.of(listOfStrings); 65 | 66 | assertThat(cyclicIterator).isNotNull(); 67 | for (int cycle = 0; cycle < numberOfElementsToTest; cycle++) { 68 | assertThat(cyclicIterator.hasNext()).isTrue(); 69 | assertThat(cyclicIterator.next()).isEqualTo(singleElement); 70 | } 71 | } 72 | 73 | @Test 74 | public void canOperateWithListOfMultipleIdenticalElements() { 75 | final String singleElement = ELEMENT_PREFIX + "_1"; 76 | final List listOfStrings = new LinkedList<>(); 77 | 78 | for (int i = 0; i < numberOfElementsToTest; i++) { 79 | listOfStrings.add(singleElement); 80 | } 81 | 82 | RandomisedCyclicIterator cyclicIterator = RandomisedCyclicIterator.of(listOfStrings); 83 | 84 | assertThat(cyclicIterator).isNotNull(); 85 | for (int cycle = 0; cycle < numberOfElementsToTest; cycle++) { 86 | for (int i = 0; i < numberOfElementsToTest; i++) { 87 | assertThat(cyclicIterator.hasNext()).isTrue(); 88 | assertThat(cyclicIterator.next()).isEqualTo(singleElement); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/support/discovery/members/FailingReplicasFinder.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.support.discovery.members; 2 | 3 | import technology.dice.dicefairlink.discovery.members.ClusterInfo; 4 | 5 | import java.util.Set; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | public class FailingReplicasFinder extends FixedSetReplicasFinder { 9 | private final AtomicInteger discoveriesUntilFailure; 10 | 11 | public FailingReplicasFinder(String fallbackEndpoint, Set replicas, int failAfter) { 12 | super(fallbackEndpoint, replicas); 13 | this.discoveriesUntilFailure = new AtomicInteger(failAfter); 14 | } 15 | 16 | @Override 17 | public ClusterInfo discoverCluster() { 18 | final int left = this.discoveriesUntilFailure.decrementAndGet(); 19 | if (left < 0) { 20 | throw new RuntimeException("Programmed exception set number of executions"); 21 | } 22 | return super.discoverCluster(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/support/discovery/members/FixedMemberFinder.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.support.discovery.members; 2 | 3 | import technology.dice.dicefairlink.discovery.members.MemberFinder; 4 | import technology.dice.dicefairlink.iterators.SizedIterator; 5 | import technology.dice.dicefairlink.support.iterators.TestCyclicIterator; 6 | 7 | import java.util.Collection; 8 | 9 | public class FixedMemberFinder implements MemberFinder { 10 | private Collection members; 11 | 12 | public FixedMemberFinder(Collection members) { 13 | this.members = members; 14 | } 15 | 16 | public void updateMembers(Collection replicas) { 17 | this.members = replicas; 18 | } 19 | 20 | @Override 21 | public SizedIterator discoverReplicas() { 22 | return TestCyclicIterator.of(this.members); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/support/discovery/members/FixedSetReplicasFinder.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.support.discovery.members; 2 | 3 | import technology.dice.dicefairlink.discovery.members.ClusterInfo; 4 | import technology.dice.dicefairlink.discovery.members.MemberFinderMethod; 5 | 6 | import java.util.Set; 7 | 8 | public class FixedSetReplicasFinder implements MemberFinderMethod { 9 | private Set replicas; 10 | private final String fallbackEndpoint; 11 | 12 | public FixedSetReplicasFinder(String fallbackEndpoint, Set replicas) { 13 | this.fallbackEndpoint = fallbackEndpoint; 14 | this.replicas = replicas; 15 | } 16 | 17 | public void updateReplicas(Set replicas) { 18 | this.replicas = replicas; 19 | } 20 | 21 | @Override 22 | public ClusterInfo discoverCluster() { 23 | return new ClusterInfo(this.fallbackEndpoint, this.replicas); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/support/discovery/tags/FailingExcludedReplicasFinder.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.support.discovery.tags; 2 | 3 | import technology.dice.dicefairlink.discovery.tags.ExclusionTag; 4 | 5 | import java.util.Collection; 6 | import java.util.Set; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | public class FailingExcludedReplicasFinder extends FixedSetExcludedReplicasFinder { 10 | private final AtomicInteger discoveriesUntilFailure; 11 | 12 | public FailingExcludedReplicasFinder(Collection exclusions, int failAfter) { 13 | super(exclusions); 14 | this.discoveriesUntilFailure = new AtomicInteger(failAfter); 15 | } 16 | 17 | @Override 18 | public Set listExcludedInstances(ExclusionTag tag) { 19 | final int left = this.discoveriesUntilFailure.decrementAndGet(); 20 | if (left < 0) { 21 | throw new RuntimeException("Programmed exception set number of executions"); 22 | } 23 | return super.listExcludedInstances(tag); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/support/discovery/tags/FixedSetExcludedReplicasFinder.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.support.discovery.tags; 2 | 3 | import technology.dice.dicefairlink.discovery.tags.ExclusionTag; 4 | import technology.dice.dicefairlink.discovery.tags.TagFilter; 5 | 6 | import java.util.Collection; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | 10 | public class FixedSetExcludedReplicasFinder implements TagFilter { 11 | private Collection exclusions; 12 | 13 | public FixedSetExcludedReplicasFinder(Collection exclusions) { 14 | this.exclusions = exclusions; 15 | } 16 | 17 | public void updateExclusions(Collection replicas) { 18 | this.exclusions = replicas; 19 | } 20 | 21 | @Override 22 | public Set listExcludedInstances(ExclusionTag tag) { 23 | return this.exclusions.stream().collect(Collectors.toSet()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/support/driver/TestDriver.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.support.driver; 2 | 3 | import java.sql.Connection; 4 | import java.sql.Driver; 5 | import java.sql.DriverPropertyInfo; 6 | import java.sql.SQLException; 7 | import java.sql.SQLFeatureNotSupportedException; 8 | import java.util.Properties; 9 | import java.util.logging.Logger; 10 | 11 | public class TestDriver implements Driver { 12 | private static final Logger LOGGER = Logger.getLogger(TestDriver.class.getName()); 13 | 14 | @Override 15 | public Connection connect(String url, Properties info) throws SQLException { 16 | return null; 17 | } 18 | 19 | @Override 20 | public boolean acceptsURL(String url) throws SQLException { 21 | return url.contains("fairlinktestdriver"); 22 | } 23 | 24 | @Override 25 | public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { 26 | return new DriverPropertyInfo[0]; 27 | } 28 | 29 | @Override 30 | public int getMajorVersion() { 31 | return 0; 32 | } 33 | 34 | @Override 35 | public int getMinorVersion() { 36 | return 0; 37 | } 38 | 39 | @Override 40 | public boolean jdbcCompliant() { 41 | return false; 42 | } 43 | 44 | @Override 45 | public Logger getParentLogger() throws SQLFeatureNotSupportedException { 46 | return LOGGER.getParent(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/technology/dice/dicefairlink/support/iterators/TestCyclicIterator.java: -------------------------------------------------------------------------------- 1 | package technology.dice.dicefairlink.support.iterators; 2 | 3 | import technology.dice.dicefairlink.iterators.CyclicIterator; 4 | 5 | import java.util.Collection; 6 | 7 | public class TestCyclicIterator extends CyclicIterator { 8 | private final Collection collection; 9 | 10 | protected TestCyclicIterator(Collection collection) { 11 | super(collection); 12 | this.collection = collection; 13 | } 14 | 15 | public static CyclicIterator of(Collection collection) { 16 | return new TestCyclicIterator<>(collection); 17 | } 18 | 19 | public Collection getElements() { 20 | return this.collection; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/awsapi/allAvailableDbInstances.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1 6 | 7 | error 8 | slowquery 9 | 10 | 11 | 12 | 13 | param-group 14 | in-sync 15 | 16 | 17 | eu-west-1a 18 | 19 | 20 | keyId 21 | 22 | 23 | monitor-id 24 | 25 | 731 26 | 5.6.10a 27 | username 28 | 2019-07-04T13:18:00.906Z 29 | db.r5.large 30 | false 31 | 32 | 5 33 | available 34 | 30 35 | 1 36 | 37 | 38 | default:aurora-5-6 39 | in-sync 40 | 41 | 42 | my.cluster 43 | rds-ca-2015 44 | 0 45 | db-id-2 46 | 22:25-22:55 47 | false 48 | my-db-cluster-agd-1 49 | arn:aws:rds:eu-west-1:123:db:my-db-cluster-agd-1 50 | 51 | 52 | hostedZoneId 53 |
address1
54 | 3306 55 |
56 | aurora 57 | true 58 | false 59 | true 60 | false 61 | arn:aws:iam::123:role/rds-monitoring-role 62 | 63 | false 64 | 65 | vpc-id-2 66 | 67 | 68 | subnet-id 69 | Active 70 | 71 | eu-west-1a 72 | 73 | 74 | 75 | subnet-id-2 76 | Active 77 | 78 | eu-west-1c 79 | 80 | 81 | 82 | subnet-id-3 83 | Active 84 | 85 | eu-west-1b 86 | 87 | 88 | 89 | Complete 90 | Subnets used by Consumer-payment (prod) Aurora cluster 91 | 92 | my.cluster 93 | 94 | 95 | 96 | sg-id 97 | active 98 | 99 | 100 | general-public-license 101 | 102 | tue:13:00-tue:15:00 103 | aurora 104 | false 105 | false 106 |
107 | 108 | 1 109 | 110 | error 111 | slowquery 112 | 113 | 114 | 115 | 116 | param-group 117 | in-sync 118 | 119 | 120 | eu-west-1a 121 | 122 | 123 | keyId 124 | 125 | 126 | monitor-id 127 | 128 | 731 129 | 5.6.10a 130 | username 131 | 2019-07-04T13:18:00.906Z 132 | db.r5.large 133 | false 134 | 135 | 5 136 | available 137 | 30 138 | 1 139 | 140 | 141 | default:aurora-5-6 142 | in-sync 143 | 144 | 145 | my.cluster 146 | rds-ca-2015 147 | 0 148 | db-id-2 149 | 22:25-22:55 150 | false 151 | my-db-cluster-agd-2 152 | arn:aws:rds:eu-west-1:123:db:my-db-cluster-agd-2 153 | 154 | 155 | hostedZoneId 156 |
address2
157 | 3306 158 |
159 | aurora 160 | true 161 | false 162 | true 163 | false 164 | arn:aws:iam::123:role/rds-monitoring-role 165 | 166 | false 167 | 168 | vpc-id-2 169 | 170 | 171 | subnet-id 172 | Active 173 | 174 | eu-west-1a 175 | 176 | 177 | 178 | subnet-id-2 179 | Active 180 | 181 | eu-west-1c 182 | 183 | 184 | 185 | subnet-id-3 186 | Active 187 | 188 | eu-west-1b 189 | 190 | 191 | 192 | Complete 193 | Subnets used by Consumer-payment (prod) Aurora cluster 194 | 195 | my.cluster 196 | 197 | 198 | 199 | sg-id 200 | active 201 | 202 | 203 | general-public-license 204 | 205 | tue:13:00-tue:15:00 206 | aurora 207 | false 208 | false 209 |
210 | 211 | 1 212 | 213 | error 214 | slowquery 215 | 216 | 217 | 218 | 219 | param-group 220 | in-sync 221 | 222 | 223 | eu-west-1b 224 | 225 | 226 | keyId 227 | 228 | 229 | piArn 230 | 231 | 731 232 | 5.6.10a 233 | username 234 | 2019-07-04T13:23:31.598Z 235 | db.r5.large 236 | false 237 | 238 | 5 239 | available 240 | 30 241 | 1 242 | 243 | 244 | default:aurora-5-6 245 | in-sync 246 | 247 | 248 | my.cluster 249 | rds-ca-2015 250 | 0 251 | dbId 252 | 22:25-22:55 253 | false 254 | my-db-cluster-agd-3 255 | arn:aws:rds:eu-west-1:123:db:my-db-cluster-agd-3 256 | 257 | 258 | hostedZoneId 259 |
address3
260 | 3306 261 |
262 | aurora 263 | true 264 | false 265 | true 266 | false 267 | arn:aws:iam::123:role/rds-monitoring-role 268 | 269 | false 270 | 271 | vpc-id-2 272 | 273 | 274 | subnet-id 275 | 276 | Active 277 | 278 | eu-west-1a 279 | 280 | 281 | 282 | subnet-id-2 283 | Active 284 | 285 | eu-west-1c 286 | 287 | 288 | 289 | subnet-id-3 290 | Active 291 | 292 | eu-west-1b 293 | 294 | 295 | 296 | Complete 297 | Subnets used by Consumer-payment (prod) Aurora cluster 298 | 299 | my.cluster 300 | 301 | 302 | 303 | sg-id 304 | active 305 | 306 | 307 | general-public-license 308 | 309 | wed:11:00-wed:13:00 310 | aurora 311 | false 312 | false 313 |
314 |
315 |
316 | 317 | requestId 318 | 319 |
-------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/awsapi/emptyResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | a0ccfa67-61ec-445d-9304-4a17e9787522 7 | 8 | -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/awsapi/oneReplicaDeletingInstances.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1 6 | 7 | error 8 | slowquery 9 | 10 | 11 | 12 | 13 | param-group 14 | in-sync 15 | 16 | 17 | eu-west-1a 18 | 19 | 20 | keyId 21 | 22 | 23 | monitor-id 24 | 25 | 731 26 | 5.6.10a 27 | username 28 | 2019-07-04T13:18:00.906Z 29 | db.r5.large 30 | false 31 | 32 | 5 33 | available 34 | 30 35 | 1 36 | 37 | 38 | default:aurora-5-6 39 | in-sync 40 | 41 | 42 | my.cluster 43 | rds-ca-2015 44 | 0 45 | db-id-2 46 | 22:25-22:55 47 | false 48 | my-db-cluster-agd-1 49 | arn:aws:rds:eu-west-1:123:db:my-db-cluster-agd-1 50 | 51 | 52 | hostedZoneId 53 |
address1
54 | 3306 55 |
56 | aurora 57 | true 58 | false 59 | true 60 | false 61 | arn:aws:iam::123:role/rds-monitoring-role 62 | 63 | false 64 | 65 | vpc-id-2 66 | 67 | 68 | subnet-id 69 | Active 70 | 71 | eu-west-1a 72 | 73 | 74 | 75 | subnet-id-2 76 | Active 77 | 78 | eu-west-1c 79 | 80 | 81 | 82 | subnet-id-3 83 | Active 84 | 85 | eu-west-1b 86 | 87 | 88 | 89 | Complete 90 | Subnets used by Consumer-payment (prod) Aurora cluster 91 | 92 | my.cluster 93 | 94 | 95 | 96 | sg-id 97 | active 98 | 99 | 100 | general-public-license 101 | 102 | tue:13:00-tue:15:00 103 | aurora 104 | false 105 | false 106 |
107 | 108 | 1 109 | 110 | error 111 | slowquery 112 | 113 | 114 | 115 | 116 | param-group 117 | in-sync 118 | 119 | 120 | eu-west-1a 121 | 122 | 123 | keyId 124 | 125 | 126 | monitor-id 127 | 128 | 731 129 | 5.6.10a 130 | username 131 | 2019-07-04T13:18:00.906Z 132 | db.r5.large 133 | false 134 | 135 | 5 136 | available 137 | 30 138 | 1 139 | 140 | 141 | default:aurora-5-6 142 | in-sync 143 | 144 | 145 | my.cluster 146 | rds-ca-2015 147 | 0 148 | db-id-2 149 | 22:25-22:55 150 | false 151 | my-db-cluster-agd-2 152 | arn:aws:rds:eu-west-1:123:db:my-db-cluster-agd-2 153 | 154 | 155 | hostedZoneId 156 |
address2
157 | 3306 158 |
159 | aurora 160 | true 161 | false 162 | true 163 | false 164 | arn:aws:iam::123:role/rds-monitoring-role 165 | 166 | false 167 | 168 | vpc-id-2 169 | 170 | 171 | subnet-id 172 | Active 173 | 174 | eu-west-1a 175 | 176 | 177 | 178 | subnet-id-2 179 | Active 180 | 181 | eu-west-1c 182 | 183 | 184 | 185 | subnet-id-3 186 | Active 187 | 188 | eu-west-1b 189 | 190 | 191 | 192 | Complete 193 | Subnets used by Consumer-payment (prod) Aurora cluster 194 | 195 | my.cluster 196 | 197 | 198 | 199 | sg-id 200 | active 201 | 202 | 203 | general-public-license 204 | 205 | tue:13:00-tue:15:00 206 | aurora 207 | false 208 | false 209 |
210 | 211 | 1 212 | 213 | error 214 | slowquery 215 | 216 | 217 | 218 | 219 | param-group 220 | in-sync 221 | 222 | 223 | eu-west-1b 224 | 225 | 226 | keyId 227 | 228 | 229 | piArn 230 | 231 | 731 232 | 5.6.10a 233 | username 234 | 2019-07-04T13:23:31.598Z 235 | db.r5.large 236 | false 237 | 238 | 5 239 | deleting 240 | 30 241 | 1 242 | 243 | 244 | default:aurora-5-6 245 | in-sync 246 | 247 | 248 | my.cluster 249 | rds-ca-2015 250 | 0 251 | dbId 252 | 22:25-22:55 253 | false 254 | my-db-cluster-agd-3 255 | arn:aws:rds:eu-west-1:123:db:my-db-cluster-agd-3 256 | 257 | 258 | hostedZoneId 259 |
address3
260 | 3306 261 |
262 | aurora 263 | true 264 | false 265 | true 266 | false 267 | arn:aws:iam::123:role/rds-monitoring-role 268 | 269 | false 270 | 271 | vpc-id-2 272 | 273 | 274 | subnet-id 275 | 276 | Active 277 | 278 | eu-west-1a 279 | 280 | 281 | 282 | subnet-id-2 283 | Active 284 | 285 | eu-west-1c 286 | 287 | 288 | 289 | subnet-id-3 290 | Active 291 | 292 | eu-west-1b 293 | 294 | 295 | 296 | Complete 297 | Subnets used by Consumer-payment (prod) Aurora cluster 298 | 299 | my.cluster 300 | 301 | 302 | 303 | sg-id 304 | active 305 | 306 | 307 | general-public-license 308 | 309 | wed:11:00-wed:13:00 310 | aurora 311 | false 312 | false 313 |
314 |
315 |
316 | 317 | requestId 318 | 319 |
-------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/awsapi/withMembers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | 1 8 | 9 | error 10 | slowquery 11 | 12 | 13 | 14 | eu-west-1c 15 | eu-west-1a 16 | eu-west-1b 17 | 18 | 19 | 5.6.10a 20 | masterUsername 21 | 22 | 23 | my-db-cluster-agd-3 24 | in-sync 25 | 1 26 | false 27 | 28 | 29 | my-db-cluster-agd-2 30 | in-sync 31 | 1 32 | false 33 | 34 | 35 | my-db-cluster-agd-1 36 | in-sync 37 | 1 38 | true 39 | 40 | 41 | stopped 42 | false 43 | 3306 44 | 30 45 | my-db-cluster-agd 46 | cluster-resouceId 47 | available 48 | 2019-08-28T17:09:22.630Z 49 | 00:56-01:26 50 | false 51 | cluster-writer-endpoint 52 | global 53 | super-turbo 54 | cluster-reader-endpoint 55 | false 56 | 2019-07-29T01:03:27.962Z 57 | 2019-07-01T18:23:35.376Z 58 | stopped 59 | true 60 | 61 | false 62 | my-db-cluster-agd 63 | 64 | 65 | sg-id 66 | active 67 | 68 | 69 | hostedZoneId 70 | mon:01:57-mon:02:27 71 | paramGroup 72 | false 73 | arn:aws:rds:eu-west-1:accountId:cluster:my-db-cluster-agd 74 | 75 | 76 | 77 | 78 | 748735e7-fe64-48d8-96c6-42fcdf60a421 79 | 80 | -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/awsapi/withoutMembers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 1 7 | 8 | 9 | eu-west-1c 10 | eu-west-1a 11 | eu-west-1b 12 | 13 | 14 | 5.7.12 15 | master 16 | 17 | stopped 18 | false 19 | 3306 20 | 1 21 | sample-cluster 22 | cluster-clusterId 23 | available 24 | 2019-08-28T17:29:21.234Z 25 | 23:19-23:49 26 | false 27 | sample-cluster.cluster-writer-endpoint 28 | provisioned 29 | aurora-mysql 30 | sample-cluster.reader-endpoint 31 | false 32 | 2019-08-28T17:29:21.234Z 33 | 2019-08-28T17:28:46.587Z 34 | stopped 35 | false 36 | 37 | false 38 | dev-consumer 39 | 40 | 41 | securityGroupId 42 | active 43 | 44 | 45 | hostedZoneId 46 | sat:22:13-sat:22:43 47 | default.aurora-mysql5.7 48 | false 49 | arn:aws:rds:eu-west-1:accountId:cluster:sample-cluster 50 | 51 | 52 | 53 | 54 | a0ccfa67-61ec-445d-9304-4a17e9787522 55 | 56 | -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/mysql/2replicas.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO replica_host_status VALUES ('master','MASTER_SESSION_ID'); 2 | INSERT INTO replica_host_status VALUES ('replica','another-sesion'); -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/mysql/3replicasPorts.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO replica_host_status 2 | VALUES ('writer', 'MASTER_SESSION_ID'); 3 | INSERT INTO replica_host_status 4 | VALUES ('localhost:__PORT_SLAVE_1__', 'another-sesion1'); 5 | INSERT INTO replica_host_status 6 | VALUES ('localhost:__PORT_SLAVE_2__', 'another-sesion2'); -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/mysql/masterData.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO data VALUES ('abc','def'); 2 | INSERT INTO data VALUES ('ghi','MASTER'); -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/mysql/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE replica_host_status( 2 | SERVER_ID varchar(100)NOT NULL DEFAULT '', 3 | SESSION_ID varchar(100)NOT NULL DEFAULT '' 4 | ); 5 | 6 | CREATE TABLE data( 7 | a varchar(100)NOT NULL, 8 | b varchar(100)NOT NULL 9 | ); 10 | 11 | -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/mysql/slave1Data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO data VALUES ('abc','def'); 2 | INSERT INTO data VALUES ('ghi','SLAVE1'); -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/mysql/slave2Data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO data VALUES ('abc','def'); 2 | INSERT INTO data VALUES ('ghi','SLAVE2'); -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/postgresql/2replicas.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO aurora_cluster_information VALUES ('master','MASTER_SESSION_ID'); 2 | INSERT INTO aurora_cluster_information VALUES ('replica','another-sesion'); 3 | -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/postgresql/drop_function.sql: -------------------------------------------------------------------------------- 1 | drop function aurora_replica_status(); 2 | -------------------------------------------------------------------------------- /src/test/resources/technology/dice/dicefairlink/discovery/members/sql/postgresql/schema.sql: -------------------------------------------------------------------------------- 1 | 2 | create table aurora_cluster_information (server_id text, session_id text); 3 | 4 | create or replace function aurora_replica_status() returns setof aurora_cluster_information as $$ 5 | select * from aurora_cluster_information; 6 | $$ language SQL; 7 | --------------------------------------------------------------------------------