call, RouteDecision routeDecision)
9 | }
10 |
11 | DualConnection *-- DatabaseCall
12 | DualConnection --> DatabaseCall::call : db operation
13 |
14 |
15 | @enduml
16 |
17 |
18 | @enduml
19 |
--------------------------------------------------------------------------------
/docs/switching-between-main-and-replica.md:
--------------------------------------------------------------------------------
1 | ## How DualConnection chooses between main and replica?
2 |
3 | [DualConnection](../src/main/java/com/atlassian/db/replica/api/DualConnection.java) takes multiple aspects while deciding
4 | which connection to use:
5 |
6 | 1. A [connection's state](dual-connection-states.md).
7 | 2. A replica's [consistency](consistency.md).
8 | 3. Context of `java.sql.Connection`/`java.sql.Statement` API usage.
9 |
10 | Some of the methods are intended to write into the database. For example every call to `java.sq.PreparedStatement#executeUpdate`
11 | will switch the connection's state to the main database.
12 |
13 | 4. Availability of replica
14 | 5. The query will use the main database in case it's:
15 | - `SELECT FOR UPDATE` statement.
16 | - `UPDATE` statement.
17 | - `DELETE` statement.
18 | - All calls with transaction isolation level higher than `TRANSACTION_READ_COMMITTED`.
19 |
20 | 6. The query will use the main database in case it's an unknown function call. Known read-only functions are [standard
21 | SQL functions](https://www.postgresql.org/docs/9.4/functions.html) and user defined functions.
22 |
--------------------------------------------------------------------------------
/docs/uml.md:
--------------------------------------------------------------------------------
1 | ## UML
2 |
3 |
4 | [UML](https://en.wikipedia.org/wiki/Unified_Modeling_Language) is a structured visual way of explaining a system.
5 |
6 | It can be used to:
7 |
8 | - document how db-replica works
9 |
10 | - suggest how db-replica could or will work
11 |
12 | - explain complex scenarios
13 |
14 |
15 | ### Sequence diagram
16 |
17 |
18 | An [UML sequence diagram](https://en.wikipedia.org/wiki/Sequence_diagram) shows time passing top-down.
19 |
20 | Participants are exchanging messages between each other at different points in time.
21 |
22 |
23 | ### PlantUML
24 |
25 |
26 | [PlantUML](https://plantuml.com/sitemap-language-specification) is a library, which turns text syntax into UML images.
27 |
28 |
29 | There's an [IntelliJ plugin](https://plugins.jetbrains.com/plugin/7017-plantuml-integration).
30 |
31 |
32 | Here's a few online editors:
33 |
34 | - [LiveUML](https://liveuml.com/)
35 |
36 | - [PlantUML Server](http://www.plantuml.com/plantuml/)
37 |
38 | - [PlantText](https://www.planttext.com/)
39 |
--------------------------------------------------------------------------------
/gradle/dependency-locks/annotationProcessor.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 |
--------------------------------------------------------------------------------
/gradle/dependency-locks/compileClasspath.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 |
--------------------------------------------------------------------------------
/gradle/dependency-locks/runtimeClasspath.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 |
--------------------------------------------------------------------------------
/gradle/dependency-locks/signatures.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 |
--------------------------------------------------------------------------------
/gradle/dependency-locks/testAnnotationProcessor.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 |
--------------------------------------------------------------------------------
/gradle/dependency-locks/testCompileClasspath.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 | com.fasterxml.jackson.core:jackson-annotations:2.10.3
5 | com.fasterxml.jackson.core:jackson-core:2.10.3
6 | com.fasterxml.jackson.core:jackson-databind:2.10.3
7 | com.github.docker-java:docker-java-api:3.2.6
8 | com.github.docker-java:docker-java-core:3.2.6
9 | com.github.docker-java:docker-java-transport-httpclient5:3.2.6
10 | com.github.docker-java:docker-java-transport:3.2.6
11 | com.google.guava:guava:19.0
12 | com.h2database:h2:1.4.200
13 | commons-codec:commons-codec:1.13
14 | commons-io:commons-io:2.6
15 | commons-lang:commons-lang:2.6
16 | net.bytebuddy:byte-buddy-agent:1.11.1
17 | net.bytebuddy:byte-buddy:1.11.1
18 | net.java.dev.jna:jna:5.5.0
19 | org.apache.commons:commons-compress:1.20
20 | org.apache.httpcomponents.client5:httpclient5:5.0
21 | org.apache.httpcomponents.core5:httpcore5:5.0
22 | org.apiguardian:apiguardian-api:1.1.0
23 | org.assertj:assertj-core:3.19.0
24 | org.bouncycastle:bcpkix-jdk15on:1.64
25 | org.bouncycastle:bcprov-jdk15on:1.64
26 | org.junit.jupiter:junit-jupiter-api:5.7.2
27 | org.junit.jupiter:junit-jupiter-params:5.7.2
28 | org.junit.jupiter:junit-jupiter:5.7.2
29 | org.junit.platform:junit-platform-commons:1.7.2
30 | org.junit:junit-bom:5.7.2
31 | org.mockito:mockito-core:3.11.0
32 | org.mockito:mockito-junit-jupiter:3.11.0
33 | org.objenesis:objenesis:3.2
34 | org.opentest4j:opentest4j:1.2.0
35 | org.postgresql:postgresql:42.2.18
36 | org.slf4j:slf4j-api:1.7.30
37 | org.threeten:threeten-extra:1.5.0
38 |
--------------------------------------------------------------------------------
/gradle/dependency-locks/testRuntimeClasspath.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 | com.fasterxml.jackson.core:jackson-annotations:2.10.3
5 | com.fasterxml.jackson.core:jackson-core:2.10.3
6 | com.fasterxml.jackson.core:jackson-databind:2.10.3
7 | com.github.docker-java:docker-java-api:3.2.6
8 | com.github.docker-java:docker-java-core:3.2.6
9 | com.github.docker-java:docker-java-transport-httpclient5:3.2.6
10 | com.github.docker-java:docker-java-transport:3.2.6
11 | com.google.guava:guava:19.0
12 | com.h2database:h2:1.4.200
13 | commons-codec:commons-codec:1.13
14 | commons-io:commons-io:2.6
15 | commons-lang:commons-lang:2.6
16 | net.bytebuddy:byte-buddy-agent:1.11.1
17 | net.bytebuddy:byte-buddy:1.11.1
18 | net.java.dev.jna:jna:5.5.0
19 | org.apache.commons:commons-compress:1.20
20 | org.apache.httpcomponents.client5:httpclient5:5.0
21 | org.apache.httpcomponents.core5:httpcore5:5.0
22 | org.apiguardian:apiguardian-api:1.1.0
23 | org.assertj:assertj-core:3.19.0
24 | org.bouncycastle:bcpkix-jdk15on:1.64
25 | org.bouncycastle:bcprov-jdk15on:1.64
26 | org.checkerframework:checker-qual:3.5.0
27 | org.junit.jupiter:junit-jupiter-api:5.7.2
28 | org.junit.jupiter:junit-jupiter-engine:5.7.2
29 | org.junit.jupiter:junit-jupiter-params:5.7.2
30 | org.junit.jupiter:junit-jupiter:5.7.2
31 | org.junit.platform:junit-platform-commons:1.7.2
32 | org.junit.platform:junit-platform-engine:1.7.2
33 | org.junit:junit-bom:5.7.2
34 | org.mockito:mockito-core:3.11.0
35 | org.mockito:mockito-junit-jupiter:3.11.0
36 | org.objenesis:objenesis:3.2
37 | org.opentest4j:opentest4j:1.2.0
38 | org.postgresql:postgresql:42.2.18
39 | org.slf4j:slf4j-api:1.7.30
40 | org.threeten:threeten-extra:1.5.0
41 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atlassian-labs/db-replica/096dcc0493ed7c0aafc5de550a9f18a99a11e96e/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "db-replica"
2 |
3 | pluginManagement {
4 | repositories {
5 | gradlePluginPortal() // work around artifactory-sidekick https://jdog.jira-dev.com/browse/JCES-1751
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/AuroraConnectionDetails.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import com.atlassian.db.replica.spi.ReplicaConnectionPerUrlProvider;
4 |
5 | import java.sql.DriverManager;
6 |
7 | /**
8 | * @deprecated use {@link ReplicaConnectionPerUrlProvider} instead. example usage:
9 | *
10 | * {@code
11 | * url -> () -> DriverManager.getConnection(
12 | * url.toString(),
13 | * username,
14 | * password
15 | * )
16 | * }
17 | *
18 | */
19 | @Deprecated
20 | public final class AuroraConnectionDetails {
21 | private final String username;
22 | private final String password;
23 |
24 | private AuroraConnectionDetails(String username, String password) {
25 | this.username = username;
26 | this.password = password;
27 | }
28 |
29 | public static Builder builder() {
30 | return new Builder();
31 | }
32 |
33 | public String getUsername() {
34 | return this.username;
35 | }
36 |
37 | public String getPassword() {
38 | return this.password;
39 | }
40 |
41 | public ReplicaConnectionPerUrlProvider convert() {
42 | return replicaUrl -> () -> DriverManager.getConnection(
43 | replicaUrl.toString(),
44 | username,
45 | password
46 | );
47 | }
48 |
49 | /**
50 | * @deprecated see {@link AuroraConnectionDetails}.
51 | */
52 | @Deprecated
53 | public static final class Builder {
54 | private String username;
55 | private String password;
56 |
57 | public Builder username(String username) {
58 | this.username = username;
59 | return this;
60 | }
61 |
62 | public Builder password(String password) {
63 | this.password = password;
64 | return this;
65 | }
66 |
67 | /**
68 | * @deprecated see {@link AuroraConnectionDetails}.
69 | */
70 | @Deprecated
71 | public static Builder anAuroraConnectionDetailsBuilder() {
72 | return new Builder();
73 | }
74 |
75 | public AuroraConnectionDetails build() {
76 | return new AuroraConnectionDetails(username, password);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/AuroraPostgresLsnReplicaConsistency.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import com.atlassian.db.replica.internal.MonotonicMemoryCache;
4 | import com.atlassian.db.replica.internal.NoCacheSuppliedCache;
5 | import com.atlassian.db.replica.spi.Cache;
6 | import com.atlassian.db.replica.spi.ReplicaConsistency;
7 | import com.atlassian.db.replica.spi.SuppliedCache;
8 |
9 | import java.sql.Connection;
10 | import java.sql.PreparedStatement;
11 | import java.sql.ResultSet;
12 | import java.sql.SQLException;
13 | import java.util.Optional;
14 | import java.util.function.Supplier;
15 |
16 | /**
17 | * It remembers last write LSN (log sequence number) and compares it with LSN for subsequent reads to detect inconsistencies.
18 | * If it cannot remember the LSN of last write, pessimistically assumes it's going to be inconsistent.
19 | *
20 | * @see Monitoring Aurora PostgreSQL-based Aurora global databases
21 | *
22 | * It's an PostgreSQL-based Aurora database engine specific implementation.
23 | */
24 | public final class AuroraPostgresLsnReplicaConsistency implements ReplicaConsistency {
25 |
26 | private final Cache lastWrite;
27 | private final SuppliedCache replicaLsnCache;
28 |
29 | public static final class Builder {
30 | private Cache lastWrite = new MonotonicMemoryCache<>();
31 | private SuppliedCache replicaLsnCache = new NoCacheSuppliedCache<>();
32 |
33 | /**
34 | * @param lastWrite remembers last write
35 | */
36 | public Builder cacheLastWrite(Cache lastWrite) {
37 | this.lastWrite = lastWrite;
38 | return this;
39 | }
40 |
41 | /**
42 | * @param cache remembers replica lsn (potentially stale)
43 | */
44 | public Builder replicaLsnCache(SuppliedCache cache) {
45 | this.replicaLsnCache = cache;
46 | return this;
47 | }
48 |
49 | /**
50 | * @return consistency assuming that LSN (log sequence number) is greater or equal to LSN for last write (if known)
51 | */
52 | public AuroraPostgresLsnReplicaConsistency build() {
53 | return new AuroraPostgresLsnReplicaConsistency(lastWrite, replicaLsnCache);
54 | }
55 | }
56 |
57 | private AuroraPostgresLsnReplicaConsistency(
58 | Cache lastWrite,
59 | SuppliedCache replicaLsnCache
60 | ) {
61 | this.lastWrite = lastWrite;
62 | this.replicaLsnCache = replicaLsnCache;
63 | }
64 |
65 | @Override
66 | public void write(Connection main) {
67 | try {
68 | lastWrite.put(queryMainDbLsn(main));
69 | } catch (Exception e) {
70 | throw new RuntimeException("failure during LSN fetching for main database", e);
71 | }
72 | }
73 |
74 | @Override
75 | public boolean isConsistent(Supplier replica) {
76 | try {
77 | return lastWrite.get()
78 | .flatMap(lastWriteLsn -> isConsistentBasedOnLsn(replica, lastWriteLsn))
79 | .orElse(false);
80 | } catch (Exception e) {
81 | return false;
82 | }
83 | }
84 |
85 | private Optional isConsistentBasedOnLsn(Supplier replica, Long lastWriteLsn) {
86 | return replicaLsnCache
87 | .get(() -> queryReplicaDbLsn(replica.get()))
88 | .map(lsn -> lsn >= lastWriteLsn);
89 | }
90 |
91 | /**
92 | * @return LSN (log sequence number) for the main database (the highest LSN)
93 | */
94 | private long queryMainDbLsn(Connection connection) {
95 | return queryLsn(connection, "SELECT MAX(durable_lsn) AS lsn FROM aurora_global_db_instance_status();");
96 | }
97 |
98 | /**
99 | * @return LSN (log sequence number) for the most outdated replica database (the lowest LSN)
100 | */
101 | private long queryReplicaDbLsn(Connection connection) {
102 | return queryLsn(connection, "SELECT MIN(durable_lsn) AS lsn FROM aurora_global_db_instance_status();");
103 | }
104 |
105 | private long queryLsn(Connection connection, String rawSqlQuery) {
106 | try (
107 | PreparedStatement query = connection.prepareStatement(rawSqlQuery);
108 | ResultSet results = query.executeQuery()
109 | ) {
110 | results.next();
111 | return results.getLong("lsn");
112 | } catch (SQLException e) {
113 | throw new RuntimeException("An SQLException occurred during LSN fetching", e);
114 | }
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/Database.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import java.sql.Connection;
4 | import java.util.function.Supplier;
5 |
6 | public interface Database {
7 | String getId();
8 |
9 | Supplier getConnectionSupplier();
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/PessimisticPropagationConsistency.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import com.atlassian.db.replica.internal.MonotonicMemoryCache;
4 | import com.atlassian.db.replica.spi.Cache;
5 | import com.atlassian.db.replica.spi.ReplicaConsistency;
6 |
7 | import java.sql.Connection;
8 | import java.time.Clock;
9 | import java.time.Duration;
10 | import java.time.Instant;
11 | import java.util.function.Supplier;
12 |
13 | /**
14 | * Assumes that writes propagate from main to replicas in at most a given amount of time.
15 | * If it cannot remember the time of last write, pessimistically assumes it's going to be inconsistent.
16 | */
17 | public final class PessimisticPropagationConsistency implements ReplicaConsistency {
18 |
19 | private final Clock clock;
20 | private final Duration maxPropagation;
21 | private final Cache lastWrite;
22 |
23 | public static class Builder {
24 | private Duration maxPropagation = Duration.ofMillis(100);
25 | private Cache lastWrite = new MonotonicMemoryCache<>();
26 | private Clock clock = Clock.systemUTC();
27 |
28 | /**
29 | * @param maxPropagation how long do writes propagate from main to replica
30 | */
31 | public Builder assumeMaxPropagation(Duration maxPropagation) {
32 | this.maxPropagation = maxPropagation;
33 | return this;
34 | }
35 |
36 | /**
37 | * @param lastWrite remembers last write
38 | */
39 | public Builder cacheLastWrite(Cache lastWrite) {
40 | this.lastWrite = lastWrite;
41 | return this;
42 | }
43 |
44 | /**
45 | * @param clock measures flow of time
46 | */
47 | public Builder measureTime(Clock clock) {
48 | this.clock = clock;
49 | return this;
50 | }
51 |
52 | /**
53 | * @return consistency assuming consistency after max propagation since last write (if known)
54 | */
55 | public ReplicaConsistency build() {
56 | return new PessimisticPropagationConsistency(clock, maxPropagation, lastWrite);
57 | }
58 | }
59 |
60 | private PessimisticPropagationConsistency(Clock clock, Duration maxPropagation, Cache lastWrite) {
61 | this.clock = clock;
62 | this.maxPropagation = maxPropagation;
63 | this.lastWrite = lastWrite;
64 | }
65 |
66 | @Override
67 | public void write(Connection main) {
68 | lastWrite.put(clock.instant());
69 | }
70 |
71 | @Override
72 | public boolean isConsistent(Supplier replica) {
73 | Instant assumedRefresh = assumeLastRefresh();
74 | Instant assumedWrite = assumeLastWrite();
75 | return assumedRefresh.isAfter(assumedWrite);
76 | }
77 |
78 | /**
79 | * @return assumed time of last replica refresh
80 | */
81 | private Instant assumeLastRefresh() {
82 | return clock.instant().minus(maxPropagation);
83 | }
84 |
85 | /**
86 | * If {@code lastWrite} is unknown, assume the write just happened, e.g. it didn't propagate yet.
87 | * This assumption errs on the side of caution: more true inconsistencies at the cost of fewer true consistencies.
88 | *
89 | * Propagates write assumption to the cache. This prevents from assuming write just happened until the next write.
90 | *
91 | * @return known or assumed time of last write
92 | */
93 | private Instant assumeLastWrite() {
94 | return lastWrite
95 | .get()
96 | .orElseGet(this::assumeWriteJustHappened);
97 | }
98 |
99 | private Instant assumeWriteJustHappened() {
100 | final Instant now = clock.instant();
101 | lastWrite.put(now);
102 | return now;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/SqlCall.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import java.sql.*;
4 |
5 | /**
6 | * Like a {@link java.util.concurrent.Callable}, but with a checked exception.
7 | * @param
8 | */
9 | public interface SqlCall {
10 | T call() throws SQLException;
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/exception/ConnectionCouldNotBeClosedException.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.exception;
2 |
3 | public final class ConnectionCouldNotBeClosedException extends RuntimeException {
4 | public ConnectionCouldNotBeClosedException(Throwable cause) {
5 | super(cause);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/exception/ReadReplicaConnectionCreationException.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.exception;
2 |
3 | public class ReadReplicaConnectionCreationException extends RuntimeException {
4 |
5 | public ReadReplicaConnectionCreationException(Throwable cause) {
6 | super("Failure during replica connection creation", cause);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/jdbc/JdbcProtocol.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.jdbc;
2 |
3 | import java.util.Objects;
4 |
5 | public final class JdbcProtocol {
6 | public static final JdbcProtocol POSTGRES = new JdbcProtocol("postgresql");
7 |
8 | private final String protocol;
9 |
10 | private JdbcProtocol(String protocol) {
11 | this.protocol = protocol;
12 | }
13 |
14 | @Override
15 | public boolean equals(Object o) {
16 | if (this == o) return true;
17 | if (o == null || getClass() != o.getClass()) return false;
18 | JdbcProtocol that = (JdbcProtocol) o;
19 | return Objects.equals(protocol, that.protocol);
20 | }
21 |
22 | @Override
23 | public int hashCode() {
24 | return Objects.hash(protocol);
25 | }
26 |
27 | @Override
28 | public String toString() {
29 | return protocol;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/jdbc/JdbcUrl.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.jdbc;
2 |
3 | import java.net.URI;
4 |
5 | public final class JdbcUrl {
6 | private static final String PREFIX = "jdbc";
7 |
8 | private final String internalUrl;
9 |
10 | private JdbcUrl(JdbcProtocol protocol, String endpoint, String database) {
11 | String url = String.format("%s:%s://%s/%s", PREFIX, protocol, endpoint, database);
12 | this.internalUrl = URI.create(url).toString();
13 | }
14 |
15 | public static Builder builder() {
16 | return new Builder();
17 | }
18 |
19 | @Override
20 | public String toString() {
21 | return this.internalUrl;
22 | }
23 |
24 | public static final class Builder {
25 | private JdbcProtocol protocol;
26 | private String endpoint;
27 | private String database;
28 |
29 | private Builder() {
30 | }
31 |
32 | public Builder protocol(JdbcProtocol protocol) {
33 | this.protocol = protocol;
34 | return this;
35 | }
36 |
37 | public Builder endpoint(String endpoint) {
38 | this.endpoint = endpoint;
39 | return this;
40 | }
41 |
42 | public Builder database(String database) {
43 | this.database = database;
44 | return this;
45 | }
46 |
47 | public JdbcUrl build() {
48 | return new JdbcUrl(protocol, endpoint, database);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/reason/Reason.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.reason;
2 |
3 | import java.util.Objects;
4 |
5 | /**
6 | * Describes the reason to choose either replica or main database connection.
7 | */
8 | public final class Reason {
9 | private final String name;
10 | private final boolean isRunOnMain;
11 | private final boolean isWrite;
12 |
13 | private Reason(final String name, boolean isRunOnMain, boolean isWrite) {
14 | this.name = name;
15 | this.isRunOnMain = isRunOnMain;
16 | this.isWrite = isWrite;
17 | }
18 |
19 | public static final Reason RW_API_CALL =
20 | new ReasonBuilder("RW_API_CALL").isRunOnMain(true).isWrite(true).build();
21 | public static final Reason REPLICA_INCONSISTENT =
22 | new ReasonBuilder("REPLICA_INCONSISTENT").isRunOnMain(true).isWrite(false).build();
23 | public static final Reason READ_OPERATION =
24 | new ReasonBuilder("READ_OPERATION").isRunOnMain(false).isWrite(false).build();
25 | public static final Reason WRITE_OPERATION =
26 | new ReasonBuilder("WRITE_OPERATION").isRunOnMain(true).isWrite(true).build();
27 | public static final Reason LOCK =
28 | new ReasonBuilder("LOCK").isRunOnMain(true).isWrite(false).build();
29 | public static final Reason MAIN_CONNECTION_REUSE =
30 | new ReasonBuilder("MAIN_CONNECTION_REUSE").isRunOnMain(true).isWrite(false).build();
31 | public static final Reason HIGH_TRANSACTION_ISOLATION_LEVEL =
32 | new ReasonBuilder("HIGH_TRANSACTION_ISOLATION_LEVEL").isRunOnMain(true).isWrite(false).build();
33 | public static final Reason RO_API_CALL =
34 | new ReasonBuilder("RO_API_CALL").isRunOnMain(false).isWrite(false).build();
35 |
36 | public String getName() {
37 | return name;
38 | }
39 |
40 | boolean isRunOnMain() {
41 | return isRunOnMain;
42 | }
43 |
44 | boolean isWrite() {
45 | return isWrite;
46 | }
47 |
48 | @Override
49 | public boolean equals(Object o) {
50 | if (this == o) return true;
51 | if (o == null || getClass() != o.getClass()) return false;
52 | Reason reason = (Reason) o;
53 | return isRunOnMain == reason.isRunOnMain && isWrite == reason.isWrite && Objects.equals(name, reason.name);
54 | }
55 |
56 | @Override
57 | public int hashCode() {
58 | return Objects.hash(name, isRunOnMain);
59 | }
60 |
61 | @Override
62 | public String toString() {
63 | return "Reason{" +
64 | "name='" + name + '\'' +
65 | ", isRunOnMain=" + isRunOnMain +
66 | ", isWrite=" + isWrite +
67 | '}';
68 | }
69 |
70 | private static class ReasonBuilder {
71 | private final String name;
72 | private boolean isRunOnMain = false;
73 | private boolean isWrite = false;
74 |
75 | ReasonBuilder(final String name) {
76 | this.name = name;
77 | }
78 |
79 | ReasonBuilder isRunOnMain(boolean isRunOnMain) {
80 | this.isRunOnMain = isRunOnMain;
81 | return this;
82 | }
83 |
84 | ReasonBuilder isWrite(boolean isWrite) {
85 | this.isWrite = isWrite;
86 | return this;
87 | }
88 |
89 | Reason build() {
90 | return new Reason(name, isRunOnMain, isWrite);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/api/reason/RouteDecision.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.reason;
2 |
3 | import java.util.Objects;
4 | import java.util.Optional;
5 |
6 | /**
7 | * Reveals details related to why, and which database will be used.
8 | */
9 | public final class RouteDecision {
10 | private final Reason reason;
11 | private final String sql;
12 | private final RouteDecision cause;
13 |
14 | public RouteDecision(String sql, Reason reason, RouteDecision cause) {
15 | this.sql = sql;
16 | this.reason = reason;
17 | this.cause = cause;
18 | }
19 |
20 | /**
21 | * @return Reason for the current route. The state of the connection may enforce it.
22 | */
23 | public Reason getReason() {
24 | return reason;
25 | }
26 |
27 | /**
28 | * @return An SQL corresponding to the current route, if any.
29 | */
30 | public Optional getSql() {
31 | return Optional.ofNullable(sql);
32 | }
33 |
34 | /**
35 | * @return The initial decision to change the state, if any.
36 | */
37 | public Optional getCause() {
38 | return Optional.ofNullable(cause);
39 | }
40 |
41 | /**
42 | * @return true if the accompanying {@link com.atlassian.db.replica.api.SqlCall#call()} would fail when run on replica.
43 | */
44 | public boolean mustRunOnMain() {
45 | return reason.isWrite();
46 | }
47 |
48 | /**
49 | * @return true if the accompanying {@link com.atlassian.db.replica.api.SqlCall#call()} will be run on the main database.
50 | */
51 | public boolean willRunOnMain() {
52 | return reason.isRunOnMain();
53 | }
54 |
55 | @Override
56 | public boolean equals(Object o) {
57 | if (this == o) return true;
58 | if (o == null || getClass() != o.getClass()) return false;
59 | RouteDecision that = (RouteDecision) o;
60 | return Objects.equals(reason, that.reason) && Objects.equals(
61 | sql,
62 | that.sql
63 | ) && Objects.equals(cause, that.cause);
64 | }
65 |
66 | @Override
67 | public int hashCode() {
68 | return Objects.hash(reason, sql, cause);
69 | }
70 |
71 | @Override
72 | public String toString() {
73 | return "RouteDecision{" +
74 | "reason=" + reason +
75 | ", sql='" + sql + '\'' +
76 | ", cause=" + cause +
77 | '}';
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/ClientInfo.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import java.sql.Connection;
4 | import java.sql.SQLClientInfoException;
5 | import java.util.Objects;
6 | import java.util.Properties;
7 |
8 | public final class ClientInfo {
9 | private final String name;
10 | private final String value;
11 | private final Properties properties;
12 |
13 | public ClientInfo(String value, String name) {
14 | this.value = value;
15 | this.name = name;
16 | this.properties = null;
17 | }
18 |
19 | public ClientInfo(Properties properties) {
20 | this.properties = properties;
21 | this.name = null;
22 | this.value = null;
23 | }
24 |
25 | public void configure(Connection connection) throws SQLClientInfoException {
26 | if (properties != null) {
27 | connection.setClientInfo(properties);
28 | } else {
29 | connection.setClientInfo(name, value);
30 | }
31 | }
32 |
33 | @Override
34 | public boolean equals(Object o) {
35 | if (this == o) return true;
36 | if (o == null || getClass() != o.getClass()) return false;
37 | ClientInfo that = (ClientInfo) o;
38 | return Objects.equals(name, that.name) && Objects.equals(value, that.value) && Objects.equals(
39 | properties,
40 | that.properties
41 | );
42 | }
43 |
44 | @Override
45 | public int hashCode() {
46 | return Objects.hash(name, value, properties);
47 | }
48 |
49 | @Override
50 | public String toString() {
51 | return "ClientInfo{" +
52 | "name='" + name + '\'' +
53 | ", value='" + value + '\'' +
54 | ", properties=" + properties +
55 | '}';
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/ConnectionOperation.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 |
4 | import java.sql.Connection;
5 | import java.sql.SQLException;
6 |
7 | public interface ConnectionOperation {
8 | void accept(Connection connection) throws SQLException;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/DecisionAwareReference.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import java.util.concurrent.atomic.AtomicReference;
4 |
5 | public abstract class DecisionAwareReference extends LazyReference {
6 | private final AtomicReference firstCause = new AtomicReference<>();
7 |
8 | protected DecisionAwareReference() {
9 | super();
10 | }
11 |
12 | public T get(RouteDecisionBuilder currentCause) {
13 | firstCause.compareAndSet(null, currentCause);
14 | return super.get();
15 | }
16 |
17 | @Override
18 | public void reset() {
19 | super.reset();
20 | firstCause.set(null);
21 | }
22 |
23 | public RouteDecisionBuilder getFirstCause() {
24 | if (firstCause.get() == null) {
25 | throw new IllegalStateException("The decision builder is not initialized");
26 | }
27 | return firstCause.get();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/ForwardCall.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.api.SqlCall;
4 | import com.atlassian.db.replica.api.reason.RouteDecision;
5 | import com.atlassian.db.replica.spi.DatabaseCall;
6 |
7 | import java.sql.SQLException;
8 |
9 | public class ForwardCall implements DatabaseCall {
10 |
11 | @Override
12 | public T call(final SqlCall call, RouteDecision decision) throws SQLException {
13 | return call.call();
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/LazyReference.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 |
4 | import com.atlassian.db.replica.internal.util.ThreadSafe;
5 |
6 | import java.util.function.Supplier;
7 |
8 | @ThreadSafe
9 | public abstract class LazyReference implements Supplier {
10 | private T reference;
11 | private final Object lock = new Object();
12 |
13 | protected LazyReference() {
14 | }
15 |
16 | protected abstract T create() throws Exception;
17 |
18 | public boolean isInitialized() {
19 | return reference != null;
20 | }
21 |
22 | @Override
23 | public T get() {
24 | synchronized (lock) {
25 | if (!isInitialized()) {
26 | try {
27 | reference = create();
28 | } catch (Exception e) {
29 | throw new RuntimeException(e);
30 | }
31 | }
32 | }
33 | return reference;
34 | }
35 |
36 | public void reset() {
37 | reference = null;
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/LockBasedThrottledCache.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.internal.util.ThreadSafe;
4 | import com.atlassian.db.replica.spi.SuppliedCache;
5 |
6 | import java.util.Optional;
7 | import java.util.concurrent.locks.ReentrantLock;
8 | import java.util.function.Supplier;
9 |
10 | @ThreadSafe
11 | public final class LockBasedThrottledCache implements SuppliedCache {
12 | private final ReentrantLock lock = new ReentrantLock();
13 | private T value = null;
14 |
15 | @Override
16 | public Optional get(Supplier supplier) {
17 | maybeRefresh(supplier);
18 | return Optional.ofNullable(value);
19 | }
20 |
21 | @Override
22 | public Optional get() {
23 | return Optional.ofNullable(value);
24 | }
25 |
26 | private void maybeRefresh(Supplier supplier) {
27 | if(lock.tryLock()) {
28 | try {
29 | value = supplier.get();
30 | } finally {
31 | lock.unlock();
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/MonotonicMemoryCache.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.spi.*;
4 |
5 | import java.util.*;
6 | import java.util.concurrent.atomic.*;
7 |
8 | import static com.atlassian.db.replica.internal.util.Comparables.*;
9 |
10 | /**
11 | * Holds values that grow over time, unless reset. Holds a value in JVM memory.
12 | *
13 | * @param
14 | */
15 | public class MonotonicMemoryCache> implements Cache {
16 |
17 | private final AtomicReference cache = new AtomicReference<>(null);
18 |
19 | @Override
20 | public Optional get() {
21 | return Optional.ofNullable(cache.get());
22 | }
23 |
24 | @Override
25 | public void put(T value) {
26 | cache.updateAndGet(prev -> max(prev, value));
27 | }
28 |
29 | @Override
30 | public void reset() {
31 | cache.set(null);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/NetworkTimeout.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import java.sql.Connection;
4 | import java.sql.SQLException;
5 | import java.util.Objects;
6 | import java.util.concurrent.Executor;
7 |
8 | public final class NetworkTimeout {
9 | private final Executor executor;
10 | private final int milliseconds;
11 |
12 | public NetworkTimeout(Executor executor, int milliseconds) {
13 | this.executor = executor;
14 | this.milliseconds = milliseconds;
15 | }
16 |
17 | public void configure(Connection connection) throws SQLException {
18 | connection.setNetworkTimeout(executor, milliseconds);
19 | }
20 |
21 | @Override
22 | public boolean equals(Object o) {
23 | if (this == o) return true;
24 | if (o == null || getClass() != o.getClass()) return false;
25 | NetworkTimeout that = (NetworkTimeout) o;
26 | return milliseconds == that.milliseconds && Objects.equals(executor, that.executor);
27 | }
28 |
29 | @Override
30 | public int hashCode() {
31 | return Objects.hash(executor, milliseconds);
32 | }
33 |
34 | @Override
35 | public String toString() {
36 | return "NetworkTimeout{" +
37 | "executor=" + executor +
38 | ", milliseconds=" + milliseconds +
39 | '}';
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/NoCacheSuppliedCache.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.spi.SuppliedCache;
4 |
5 | import java.util.Optional;
6 | import java.util.function.Supplier;
7 |
8 | /**
9 | * No cache implementation
10 | */
11 | public final class NoCacheSuppliedCache implements SuppliedCache {
12 | @Override
13 | public Optional get(Supplier supplier) {
14 | return Optional.ofNullable(supplier.get());
15 | }
16 |
17 | @Override
18 | public Optional get() {
19 | return Optional.empty();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/NoOpDirtyConnectionCloseHook.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.spi.DirtyConnectionCloseHook;
4 |
5 | import java.sql.Connection;
6 |
7 | public class NoOpDirtyConnectionCloseHook implements DirtyConnectionCloseHook {
8 | @Override
9 | public void onClose(Connection connection) {
10 | // do nothing
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/NotLoggingLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.spi.Logger;
4 |
5 | public class NotLoggingLogger implements Logger {
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/ReadReplicaUnsupportedOperationException.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | /**
4 | * I just want to have a single place to keep debugger aware of calls to unsupported operations.
5 | * I plan to remove it once all the calls supported.
6 | */
7 | public class ReadReplicaUnsupportedOperationException extends RuntimeException {
8 | public ReadReplicaUnsupportedOperationException() {
9 | super();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/RouteDecisionBuilder.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.api.reason.Reason;
4 | import com.atlassian.db.replica.api.reason.RouteDecision;
5 |
6 | import java.util.Objects;
7 |
8 | public final class RouteDecisionBuilder {
9 | private String sql = null;
10 | private Reason reason;
11 | private RouteDecision cause = null;
12 |
13 | public RouteDecisionBuilder(Reason reason) {
14 | this.reason = reason;
15 | }
16 |
17 | public RouteDecisionBuilder sql(final String sql) {
18 | this.sql = sql;
19 | return this;
20 | }
21 |
22 | public RouteDecisionBuilder reason(final Reason reason) {
23 | this.reason = reason;
24 | return this;
25 | }
26 |
27 | public RouteDecisionBuilder cause(final RouteDecision cause) {
28 | this.cause = cause;
29 | return this;
30 | }
31 |
32 | public String getSql() {
33 | return sql;
34 | }
35 |
36 | public RouteDecision build() {
37 | return new RouteDecision(sql, reason, cause);
38 | }
39 |
40 | @Override
41 | public boolean equals(Object o) {
42 | if (this == o) return true;
43 | if (o == null || getClass() != o.getClass()) return false;
44 | RouteDecisionBuilder that = (RouteDecisionBuilder) o;
45 | return Objects.equals(sql, that.sql)
46 | && Objects.equals(reason, that.reason)
47 | && Objects.equals(cause, that.cause);
48 | }
49 |
50 | @Override
51 | public int hashCode() {
52 | return Objects.hash(sql, reason, cause);
53 | }
54 |
55 | @Override
56 | public String toString() {
57 | return "RouteDecisionBuilder{" +
58 | "sql='" + sql + '\'' +
59 | ", reason=" + reason +
60 | ", cause=" + cause +
61 | '}';
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/SqlQuery.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | public final class SqlQuery {
4 | private static final int SELECT_FOR_UPDATE_SUFFIX_LIMIT = 100;
5 | private final String sql;
6 |
7 | public SqlQuery(String sql, boolean compatibleWithPreviousVersion) {
8 | if (sql == null) {
9 | throw new RuntimeException("An SqlQuery must have an SQL query string");
10 | }
11 | this.sql = sql;
12 | }
13 |
14 | public boolean isWriteOperation(SqlFunction sqlFunction) {
15 | return sqlFunction.isFunctionCall(sql) || isUpdate() || isDelete() || isInsert();
16 | }
17 |
18 | public boolean isSelectForUpdate() {
19 | final String trimmedQuery = trimForSelectForUpdateCheck();
20 | return containsFor(trimmedQuery) && (containsUpdate(trimmedQuery) || containsShare(trimmedQuery));
21 | }
22 |
23 | private boolean containsUpdate(String trimmedQuery) {
24 | return trimmedQuery.contains("update") || trimmedQuery.contains("UPDATE");
25 | }
26 |
27 | private boolean containsShare(String trimmedQuery) {
28 | return trimmedQuery.contains("share") || trimmedQuery.contains("SHARE");
29 | }
30 |
31 | private boolean containsFor(String trimmedQuery) {
32 | return trimmedQuery.contains("for") || trimmedQuery.contains("FOR");
33 | }
34 |
35 | public boolean isInsert() {
36 | return sql.startsWith("insert") || sql.startsWith("INSERT");
37 | }
38 |
39 | public boolean isSqlSet() {
40 | return sql.startsWith("set") || sql.startsWith("SET");
41 | }
42 |
43 | private boolean isUpdate() {
44 | return sql.startsWith("update") || sql.startsWith("UPDATE");
45 | }
46 |
47 | private boolean isDelete() {
48 | return sql.startsWith("delete") || sql.startsWith("DELETE");
49 | }
50 |
51 | private String trimForSelectForUpdateCheck() {
52 | if (sql.length() < SELECT_FOR_UPDATE_SUFFIX_LIMIT) {
53 | return sql;
54 | } else {
55 | return sql.substring(sql.length() - SELECT_FOR_UPDATE_SUFFIX_LIMIT);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/SqlRunnable.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import java.sql.SQLException;
4 |
5 | public interface SqlRunnable {
6 | void run() throws SQLException;
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/StatementOperation.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 |
4 | import java.sql.SQLException;
5 | import java.sql.Statement;
6 |
7 | public interface StatementOperation {
8 | void accept(T t) throws SQLException;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/Warnings.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import java.sql.SQLWarning;
4 |
5 | public final class Warnings {
6 | private SQLWarning warning;
7 |
8 | public void saveWarning(SQLWarning warning) {
9 | if (warning == null || isLastWarning(warning)) {
10 | return;
11 | }
12 | if (this.warning == null) {
13 | this.warning = warning;
14 | } else {
15 | this.warning.setNextWarning(warning);
16 | }
17 | }
18 |
19 | public SQLWarning getWarning() {
20 | return warning;
21 | }
22 |
23 | public void clear() {
24 | this.warning = null;
25 | }
26 |
27 | private boolean isLastWarning(SQLWarning warning) {
28 | if (this.warning == null) {
29 | return false;
30 | }
31 | SQLWarning lastWarning = this.warning;
32 | for (int i = 0; i < 100; i++) {
33 | if (lastWarning.getNextWarning() == null) {
34 | return lastWarning.equals(warning);
35 | } else
36 | lastWarning = lastWarning.getNextWarning();
37 | }
38 | return true;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/AuroraCluster.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import java.util.Objects;
4 |
5 | import static com.atlassian.db.replica.internal.aurora.AuroraCluster.AuroraClusterBuilder.anAuroraCluster;
6 |
7 | public final class AuroraCluster {
8 | private final String clusterName;
9 | private final String clusterPrefix;
10 |
11 | private AuroraCluster(String clusterName, String clusterPrefix) {
12 | this.clusterName = clusterName;
13 | this.clusterPrefix = clusterPrefix;
14 | }
15 |
16 | public static AuroraCluster parse(String cluster) {
17 | Objects.requireNonNull(cluster);
18 |
19 | String[] chunks = splitByLastOccurence("-", cluster);
20 | if (chunks.length == 1) {
21 | String clusterName = chunks[0];
22 | return anAuroraCluster(clusterName).build();
23 | } else {
24 | String clusterName = chunks[0];
25 | String clusterPrefix = chunks[1];
26 | return anAuroraCluster(clusterName).clusterPrefix(clusterPrefix).build();
27 | }
28 | }
29 |
30 | private static String[] splitByLastOccurence(String delimiter, String str) {
31 | int i = str.lastIndexOf(delimiter);
32 | if (i == -1) {
33 | return new String[]{str};
34 | } else {
35 | return new String[]{str.substring(i + 1), str.substring(0, i)};
36 | }
37 | }
38 |
39 | @Override
40 | public String toString() {
41 | if (clusterPrefix != null && !clusterPrefix.isEmpty()) {
42 | return String.format("%s-%s", clusterPrefix, clusterName);
43 | } else {
44 | return clusterName;
45 | }
46 | }
47 |
48 | @Override
49 | public boolean equals(Object o) {
50 | if (this == o) return true;
51 | if (o == null || getClass() != o.getClass()) return false;
52 |
53 | AuroraCluster that = (AuroraCluster) o;
54 |
55 | if (!Objects.equals(clusterName, that.clusterName)) return false;
56 | return Objects.equals(clusterPrefix, that.clusterPrefix);
57 | }
58 |
59 | @Override
60 | public int hashCode() {
61 | int result = clusterName != null ? clusterName.hashCode() : 0;
62 | result = 31 * result + (clusterPrefix != null ? clusterPrefix.hashCode() : 0);
63 | return result;
64 | }
65 |
66 | public String getClusterName() {
67 | return clusterName;
68 | }
69 |
70 | public String getClusterPrefix() {
71 | return clusterPrefix;
72 | }
73 |
74 |
75 | public static final class AuroraClusterBuilder {
76 | private String clusterName;
77 | private String clusterPrefix;
78 |
79 | private AuroraClusterBuilder() {
80 | }
81 |
82 | public static AuroraClusterBuilder anAuroraCluster(String clusterName) {
83 | return new AuroraClusterBuilder().clusterName(clusterName);
84 | }
85 |
86 | public AuroraClusterBuilder clusterName(String clusterName) {
87 | this.clusterName = clusterName;
88 | return this;
89 | }
90 |
91 | public AuroraClusterBuilder clusterPrefix(String clusterPrefix) {
92 | this.clusterPrefix = clusterPrefix;
93 | return this;
94 | }
95 |
96 | public AuroraCluster build() {
97 | return new AuroraCluster(clusterName, clusterPrefix);
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/AuroraEndpoint.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import java.util.Objects;
4 | import java.util.regex.Matcher;
5 | import java.util.regex.Pattern;
6 |
7 | public final class AuroraEndpoint {
8 | private static final String SERVER_ID_PATTERN = "([^.]+)";
9 | private static final String CLUSTER_PATTERN = "([^.]+)";
10 | private static final String DNS_PATTERN = "(.*)";
11 | private static final Pattern ENDPOINT_PATTERN = Pattern.compile(SERVER_ID_PATTERN + "." + CLUSTER_PATTERN + "." + DNS_PATTERN);
12 |
13 | private final String serverId;
14 | private final AuroraCluster cluster;
15 | private final RdsDns dns;
16 |
17 | public AuroraEndpoint(String serverId, AuroraCluster cluster, RdsDns dns) {
18 | this.serverId = serverId;
19 | this.cluster = cluster;
20 | this.dns = dns;
21 | }
22 |
23 | public static AuroraEndpoint parse(String readerEndpoint) {
24 | Objects.requireNonNull(readerEndpoint);
25 |
26 | Matcher matcher = ENDPOINT_PATTERN.matcher(readerEndpoint);
27 | if (!matcher.matches()) {
28 | throw new IllegalArgumentException(String.format("Can't parse %s.", readerEndpoint));
29 | }
30 | return new AuroraEndpoint(
31 | matcher.group(1),
32 | AuroraCluster.parse(matcher.group(2)),
33 | RdsDns.parse(matcher.group(3))
34 | );
35 | }
36 |
37 | public String getServerId() {
38 | return serverId;
39 | }
40 |
41 | public AuroraCluster getCluster() {
42 | return cluster;
43 | }
44 |
45 | public RdsDns getDns() {
46 | return dns;
47 | }
48 |
49 | @Override
50 | public String toString() {
51 | return String.format("%s.%s.%s", serverId, cluster.toString(), dns.toString());
52 | }
53 |
54 |
55 | public static final class AuroraEndpointBuilder {
56 | private String serverId;
57 | private AuroraCluster cluster;
58 | private RdsDns dns;
59 |
60 | private AuroraEndpointBuilder() {
61 | }
62 |
63 | public static AuroraEndpointBuilder anAuroraEndpoint(AuroraEndpoint endpoint) {
64 | return new AuroraEndpointBuilder()
65 | .serverId(endpoint.serverId)
66 | .cluster(endpoint.cluster)
67 | .dns(endpoint.dns);
68 | }
69 |
70 | public static AuroraEndpointBuilder anAuroraEndpoint() {
71 | return new AuroraEndpointBuilder();
72 | }
73 |
74 | public AuroraEndpointBuilder serverId(String serverId) {
75 | this.serverId = serverId;
76 | return this;
77 | }
78 |
79 | public AuroraEndpointBuilder cluster(AuroraCluster cluster) {
80 | this.cluster = cluster;
81 | return this;
82 | }
83 |
84 | public AuroraEndpointBuilder dns(RdsDns dns) {
85 | this.dns = dns;
86 | return this;
87 | }
88 |
89 | public AuroraEndpoint build() {
90 | return new AuroraEndpoint(serverId, cluster, dns);
91 | }
92 | }
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/AuroraEndpoints.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import static com.atlassian.db.replica.internal.aurora.AuroraCluster.AuroraClusterBuilder.anAuroraCluster;
4 | import static com.atlassian.db.replica.internal.aurora.AuroraEndpoint.AuroraEndpointBuilder.anAuroraEndpoint;
5 |
6 | public class AuroraEndpoints {
7 | private AuroraEndpoints() {
8 | }
9 |
10 | /**
11 | * Transforms reader endpoint to instance endpoint
12 | */
13 | public static AuroraEndpoint instanceEndpoint(AuroraEndpoint readerEndpoint, String serverId) {
14 | return anAuroraEndpoint(readerEndpoint)
15 | .serverId(serverId)
16 | .cluster(anAuroraCluster(readerEndpoint.getCluster().getClusterName()).clusterPrefix(null).build())
17 | .build();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/AuroraJdbcUrl.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import com.atlassian.db.replica.api.jdbc.JdbcProtocol;
4 | import com.atlassian.db.replica.api.jdbc.JdbcUrl;
5 |
6 | public class AuroraJdbcUrl {
7 | private static final String PREFIX = "jdbc:postgresql://";
8 |
9 | private final AuroraEndpoint endpoint;
10 | private final String databaseName;
11 |
12 | public AuroraJdbcUrl(AuroraEndpoint endpoint, String databaseName) {
13 | this.endpoint = endpoint;
14 | this.databaseName = databaseName;
15 | }
16 |
17 | public AuroraEndpoint getEndpoint() {
18 | return endpoint;
19 | }
20 |
21 | public String getDatabaseName() {
22 | return databaseName;
23 | }
24 |
25 | public JdbcUrl toJdbcUrl() {
26 | return JdbcUrl.builder()
27 | .protocol(JdbcProtocol.POSTGRES)
28 | .endpoint(endpoint.toString())
29 | .database(databaseName)
30 | .build();
31 | }
32 |
33 | @Override
34 | public String toString() {
35 | return String.format("%s%s/%s", PREFIX, endpoint, databaseName);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/AuroraReplicaNode.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import com.atlassian.db.replica.api.exception.ReadReplicaConnectionCreationException;
4 | import com.atlassian.db.replica.spi.ReplicaConnectionProvider;
5 | import com.atlassian.db.replica.api.Database;
6 |
7 | import java.sql.Connection;
8 | import java.sql.SQLException;
9 | import java.util.function.Supplier;
10 |
11 | public class AuroraReplicaNode implements Database {
12 | private final String id;
13 | private final ReplicaConnectionProvider replicaConnectionProvider;
14 |
15 | public AuroraReplicaNode(
16 | String id,
17 | ReplicaConnectionProvider replicaConnectionProvider
18 | ) {
19 | this.id = id;
20 | this.replicaConnectionProvider = replicaConnectionProvider;
21 | }
22 |
23 | @Override
24 | public String getId() {
25 | return id;
26 | }
27 |
28 | @Override
29 | public Supplier getConnectionSupplier() {
30 | return () -> {
31 | try {
32 | return replicaConnectionProvider.getReplicaConnection();
33 | } catch (SQLException exception) {
34 | throw new ReadReplicaConnectionCreationException(exception);
35 | }
36 | };
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/AuroraReplicasDiscoverer.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import com.atlassian.db.replica.internal.logs.LazyLogger;
4 | import com.atlassian.db.replica.spi.Logger;
5 |
6 | import java.sql.Connection;
7 | import java.sql.ResultSet;
8 | import java.sql.SQLException;
9 | import java.util.LinkedList;
10 | import java.util.List;
11 |
12 | import static com.atlassian.db.replica.internal.aurora.AuroraEndpoints.instanceEndpoint;
13 | import static java.util.stream.Collectors.toList;
14 |
15 | /**
16 | * Allows discovery of Aurora Replicas cluster information
17 | */
18 | public final class AuroraReplicasDiscoverer {
19 | private final AuroraJdbcUrl readerUrl;
20 |
21 | private final Logger logger;
22 | private final LazyLogger lazyLogger;
23 |
24 | public AuroraReplicasDiscoverer(AuroraJdbcUrl readerUrl, Logger logger, LazyLogger lazyLogger) {
25 | this.readerUrl = readerUrl;
26 | this.logger = logger;
27 | this.lazyLogger = lazyLogger;
28 | }
29 |
30 | /**
31 | * Provides jdbc urls for discovered replicas
32 | *
33 | * @return list of jdbc urls
34 | */
35 | public List fetchReplicasUrls(Connection connection) throws SQLException {
36 | return fetchReplicasServerIds(connection)
37 | .stream()
38 | .map(serverId ->
39 | new AuroraJdbcUrl(
40 | instanceEndpoint(readerUrl.getEndpoint(), serverId),
41 | readerUrl.getDatabaseName()
42 | )
43 | )
44 | .collect(toList());
45 | }
46 |
47 | private List fetchReplicasServerIds(Connection connection) throws SQLException {
48 | List ids = new LinkedList<>();
49 | final String sql = "SELECT server_id, durable_lsn, current_read_lsn, feedback_xmin, " +
50 | "round(extract(milliseconds from (now()-last_update_timestamp))) as state_lag_in_msec, replica_lag_in_msec " +
51 | "FROM aurora_replica_status() " +
52 | "WHERE session_id != 'MASTER_SESSION_ID' and last_update_timestamp > NOW() - INTERVAL '5 minutes';";
53 | try (ResultSet rs =
54 | connection.prepareStatement(sql).executeQuery()) {
55 | while (rs.next()) {
56 | String serverId = rs.getString("server_id");
57 | long replicaLagInMs = rs.getLong("replica_lag_in_msec");
58 | long durableLsn = rs.getLong("durable_lsn");
59 | long currentReadLsn = rs.getLong("current_read_lsn");
60 | long feedbackXmin = rs.getLong("feedback_xmin");
61 | long stateLag = rs.getLong("state_lag_in_msec");
62 | /* Idea for future logging
63 | logger.debug(String.format(
64 | "server_id=%s, replica_lag_in_ms=%d, durable_lsn=%d, current_read_lsn=%d, feedback_xmin=%d, state_lag=%d",
65 | serverId,
66 | replicaLagInMs,
67 | durableLsn,
68 | currentReadLsn,
69 | feedbackXmin,
70 | stateLag
71 | ));
72 | */
73 | ids.add(serverId);
74 | }
75 | }
76 | return ids;
77 | }
78 | }
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/RdsDns.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import java.util.Objects;
4 | import java.util.regex.Matcher;
5 | import java.util.regex.Pattern;
6 |
7 | public final class RdsDns {
8 | private static final Pattern DNS_PATTERN = Pattern.compile("([^.]+).([^:]*):?([0-9]+)?");
9 | private final String region;
10 | private final String domain;
11 | private final Integer port;
12 |
13 | public RdsDns(String region, String domain, Integer port) {
14 | this.region = region;
15 | this.domain = domain;
16 | this.port = port;
17 | }
18 |
19 | public static RdsDns parse(String dns) {
20 | Objects.requireNonNull(dns);
21 |
22 | Matcher matcher = DNS_PATTERN.matcher(dns);
23 | matcher.matches();
24 | return new RdsDns(matcher.group(1), matcher.group(2), parseNullableInteger(groupOrNull(matcher, 3)));
25 | }
26 |
27 | private static String groupOrNull(Matcher matcher, int n) {
28 | try {
29 | return matcher.group(n);
30 | } catch (IndexOutOfBoundsException e) {
31 | return null;
32 | }
33 | }
34 |
35 | private static Integer parseNullableInteger(String str) {
36 | if (str == null) {
37 | return null;
38 | } else {
39 | return Integer.valueOf(str);
40 | }
41 | }
42 |
43 | public String getRegion() {
44 | return region;
45 | }
46 |
47 | public String getDomain() {
48 | return domain;
49 | }
50 |
51 | public Integer getPort() {
52 | return port;
53 | }
54 |
55 | @Override
56 | public String toString() {
57 | if (port != null) {
58 | return String.format("%s.%s:%s", region, domain, port);
59 | } else {
60 | return String.format("%s.%s", region, domain);
61 | }
62 | }
63 |
64 | @Override
65 | public boolean equals(Object o) {
66 | if (this == o) return true;
67 | if (o == null || getClass() != o.getClass()) return false;
68 |
69 | RdsDns rdsDns = (RdsDns) o;
70 |
71 | if (!Objects.equals(region, rdsDns.region)) return false;
72 | if (!Objects.equals(domain, rdsDns.domain)) return false;
73 | return Objects.equals(port, rdsDns.port);
74 | }
75 |
76 | @Override
77 | public int hashCode() {
78 | int result = region != null ? region.hashCode() : 0;
79 | result = 31 * result + (domain != null ? domain.hashCode() : 0);
80 | result = 31 * result + (port != null ? port.hashCode() : 0);
81 | return result;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/ReadReplicaDiscovererCreationException.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | public class ReadReplicaDiscovererCreationException extends RuntimeException {
4 |
5 | public ReadReplicaDiscovererCreationException(Throwable cause) {
6 | super("Failed to create AuroraReplicasDiscoverer", cause);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/ReadReplicaDiscoveryOperationException.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | public class ReadReplicaDiscoveryOperationException extends RuntimeException {
4 |
5 | public ReadReplicaDiscoveryOperationException(Throwable cause) {
6 | super("Failure during read replicas discovery operation", cause);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/ReadReplicaNodeLabelingOperationException.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | public class ReadReplicaNodeLabelingOperationException extends RuntimeException {
4 |
5 | public ReadReplicaNodeLabelingOperationException(String replicaId, Throwable cause) {
6 | super("Failed to label a replica connection with replica id: " + replicaId, cause);
7 | }
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/aurora/ReplicaNode.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import java.sql.Connection;
4 | import java.sql.SQLException;
5 |
6 |
7 | public class ReplicaNode {
8 | private static final String AURORA_REPLICA_ID = "replicaId";
9 |
10 | public Connection mark(final Connection connection, final String replicaId) {
11 | try {
12 | connection.getClientInfo().setProperty(AURORA_REPLICA_ID, replicaId);
13 | } catch (SQLException exception) {
14 | throw new ReadReplicaNodeLabelingOperationException(replicaId, exception);
15 | }
16 | return connection;
17 | }
18 |
19 | public String get(Connection replica) {
20 | try {
21 | return replica.getClientInfo(AURORA_REPLICA_ID);
22 | } catch (SQLException e) {
23 | // LOG.withCustomerData().error("Failed to fetch aurora server id", e);
24 | return null;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/circuitbreaker/BreakOnNotSupportedOperations.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.circuitbreaker;
2 |
3 | import com.atlassian.db.replica.internal.ReadReplicaUnsupportedOperationException;
4 |
5 | import java.sql.SQLFeatureNotSupportedException;
6 |
7 | import static com.atlassian.db.replica.internal.circuitbreaker.BreakerState.CLOSED;
8 | import static com.atlassian.db.replica.internal.circuitbreaker.BreakerState.OPEN;
9 |
10 | public class BreakOnNotSupportedOperations implements CircuitBreaker {
11 | private static volatile BreakerState state = CLOSED;
12 |
13 | @Override
14 | public BreakerState getState() {
15 | return state;
16 | }
17 |
18 | @Override
19 | public void handle(Throwable throwable) {
20 | if (throwable instanceof ReadReplicaUnsupportedOperationException || throwable instanceof SQLFeatureNotSupportedException) {
21 | state = OPEN;
22 | }
23 | }
24 |
25 | public static void reset() {
26 | state = CLOSED;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/circuitbreaker/BreakerHandler.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.circuitbreaker;
2 |
3 | import com.atlassian.db.replica.api.SqlCall;
4 | import com.atlassian.db.replica.internal.SqlRunnable;
5 |
6 | import java.sql.SQLException;
7 |
8 | public class BreakerHandler {
9 | private final CircuitBreaker breaker;
10 |
11 | public BreakerHandler(CircuitBreaker breaker) {
12 | this.breaker = breaker;
13 | }
14 |
15 | public T handle(SqlCall call) throws SQLException {
16 | try {
17 | return call.call();
18 | } catch (Throwable throwable) {
19 | breaker.handle(throwable);
20 | throw throwable;
21 | }
22 | }
23 |
24 | public void handle(SqlRunnable call) throws SQLException {
25 | try {
26 | call.run();
27 | } catch (Throwable throwable) {
28 | breaker.handle(throwable);
29 | throw throwable;
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/circuitbreaker/BreakerState.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.circuitbreaker;
2 |
3 | /**
4 | * States of circuit breakers.
5 | */
6 | public enum BreakerState {
7 | OPEN,
8 | HALF_CLOSED,
9 | CLOSED
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/circuitbreaker/CircuitBreaker.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.circuitbreaker;
2 |
3 |
4 | public interface CircuitBreaker {
5 | BreakerState getState();
6 |
7 | void handle(Throwable throwable);
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/circuitbreaker/ClosedBreaker.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.circuitbreaker;
2 |
3 | public class ClosedBreaker implements CircuitBreaker {
4 | @Override
5 | public BreakerState getState() {
6 | return BreakerState.CLOSED;
7 | }
8 |
9 | @Override
10 | public void handle(Throwable throwable) {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/logs/ConnectionProviderLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.logs;
2 |
3 | import com.atlassian.db.replica.spi.ConnectionProvider;
4 |
5 | import java.sql.Connection;
6 | import java.sql.SQLException;
7 |
8 | import static java.lang.String.format;
9 |
10 | public final class ConnectionProviderLogger implements ConnectionProvider {
11 | private final ConnectionProvider delegate;
12 | private final LazyLogger logger;
13 |
14 | public ConnectionProviderLogger(ConnectionProvider delegate, LazyLogger logger) {
15 | this.delegate = delegate;
16 | this.logger = logger;
17 | }
18 |
19 | @Override
20 | public boolean isReplicaAvailable() {
21 | return delegate.isReplicaAvailable();
22 | }
23 |
24 | @Override
25 | public Connection getMainConnection() throws SQLException {
26 | try {
27 | final Connection mainConnection = delegate.getMainConnection();
28 | logger.debug(() -> format("ConnectionProvider#getMainConnection(connection=%s)", mainConnection));
29 | return mainConnection;
30 | } catch (Exception e) {
31 | logger.debug(() -> "Failed ConnectionProvider#getMainConnection", e);
32 | throw e;
33 | }
34 | }
35 |
36 | @Override
37 | public Connection getReplicaConnection() throws SQLException {
38 | try {
39 | final Connection replicaConnection = delegate.getReplicaConnection();
40 | logger.debug(() -> format("ConnectionProvider#getReplicaConnection(connection=%s)", replicaConnection));
41 | return replicaConnection;
42 | } catch (Exception e) {
43 | logger.debug(() -> "Failed ConnectionProvider#getReplicaConnection", e);
44 | throw e;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/logs/DelegatingLazyLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.logs;
2 |
3 | import com.atlassian.db.replica.spi.Logger;
4 |
5 | import java.util.function.Supplier;
6 |
7 | public class DelegatingLazyLogger implements LazyLogger {
8 | private final Logger log;
9 |
10 | public DelegatingLazyLogger(Logger log) {
11 | this.log = log;
12 | }
13 |
14 | public void debug(Supplier message) {
15 | log.debug(message.get());
16 | }
17 |
18 | public void debug(Supplier message, Throwable t) {
19 | log.debug(message.get(), t);
20 | }
21 |
22 | public void info(Supplier message) {
23 | log.info(message.get());
24 | }
25 |
26 | public void info(Supplier message, Throwable t) {
27 | log.info(message.get(), t);
28 | }
29 |
30 | public void warn(Supplier message) {
31 | log.warn(message.get());
32 | }
33 |
34 | public void warn(Supplier message, Throwable t) {
35 | log.warn(message.get(), t);
36 | }
37 |
38 | public void error(Supplier message) {
39 | log.error(message.get());
40 | }
41 |
42 | public void error(Supplier message, Throwable t) {
43 | log.error(message.get(), t);
44 | }
45 |
46 | @Override
47 | public boolean isEnabled() {
48 | return true;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/logs/LazyLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.logs;
2 |
3 | import java.util.function.Supplier;
4 |
5 | public interface LazyLogger {
6 | void debug(Supplier message);
7 |
8 | void debug(Supplier message, Throwable t);
9 |
10 | void info(Supplier message);
11 |
12 | void info(Supplier message, Throwable t);
13 |
14 | void warn(Supplier message);
15 |
16 | void warn(Supplier message, Throwable t);
17 |
18 | void error(Supplier message);
19 |
20 | void error(Supplier message, Throwable t);
21 |
22 | boolean isEnabled();
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/logs/NoopLazyLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.logs;
2 |
3 | import java.util.function.Supplier;
4 |
5 | public class NoopLazyLogger implements LazyLogger{
6 | @Override
7 | public void debug(Supplier message) {
8 | //noop
9 | }
10 |
11 | @Override
12 | public void debug(Supplier message, Throwable t) {
13 | //noop
14 | }
15 |
16 | @Override
17 | public void info(Supplier message) {
18 | //noop
19 | }
20 |
21 | @Override
22 | public void info(Supplier message, Throwable t) {
23 | //noop
24 | }
25 |
26 | @Override
27 | public void warn(Supplier message) {
28 | //noop
29 | }
30 |
31 | @Override
32 | public void warn(Supplier message, Throwable t) {
33 | //noop
34 | }
35 |
36 | @Override
37 | public void error(Supplier message) {
38 | //noop
39 | }
40 |
41 | @Override
42 | public void error(Supplier message, Throwable t) {
43 | //noop
44 | }
45 |
46 | @Override
47 | public boolean isEnabled() {
48 | return false;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/logs/ReplicaConsistencyLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.logs;
2 |
3 | import com.atlassian.db.replica.spi.ReplicaConsistency;
4 |
5 | import java.sql.Connection;
6 | import java.util.function.Supplier;
7 |
8 | import static java.lang.String.format;
9 |
10 | public final class ReplicaConsistencyLogger implements ReplicaConsistency {
11 | private final ReplicaConsistency delegate;
12 | private final LazyLogger logger;
13 |
14 | public ReplicaConsistencyLogger(ReplicaConsistency delegate, LazyLogger logger) {
15 | this.delegate = delegate;
16 | this.logger = logger;
17 | }
18 |
19 | @Override
20 | public void write(Connection main) {
21 | try {
22 | delegate.write(main);
23 | logger.debug(() -> format("ReplicaConsistency#write(connection=%s)", main));
24 | } catch (Exception e) {
25 | logger.debug(() -> format("Failed ReplicaConsistency#write(connection=%s)", main), e);
26 | throw e;
27 | }
28 | }
29 |
30 | @Override
31 | public void preCommit(Connection main) {
32 | try {
33 | delegate.preCommit(main);
34 | logger.debug(() -> format("ReplicaConsistency#preCommit(connection=%s)", main));
35 | } catch (Exception e) {
36 | logger.debug(() -> format("Failed ReplicaConsistency#preCommit(connection=%s)", main), e);
37 | throw e;
38 | }
39 | }
40 |
41 | @Override
42 | public boolean isConsistent(Supplier replica) {
43 | try {
44 | final boolean consistent = delegate.isConsistent(replica);
45 | logger.debug(() -> format("ReplicaConsistency#isConsistent = %b", consistent));
46 | return consistent;
47 | } catch (Exception e) {
48 | logger.debug(() -> "Failed ReplicaConsistency#isConsistent", e);
49 | throw e;
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/logs/StateAwareLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.logs;
2 |
3 | import com.atlassian.db.replica.internal.state.State;
4 |
5 | import java.util.function.Supplier;
6 |
7 | public class StateAwareLogger implements LazyLogger {
8 | private final Supplier stateSupplier;
9 | private final LazyLogger logger;
10 |
11 | public StateAwareLogger(Supplier stateSupplier, LazyLogger logger) {
12 | this.stateSupplier = stateSupplier;
13 | this.logger = logger;
14 | }
15 |
16 | @Override
17 | public void debug(Supplier message) {
18 | if (isEnabled()) {
19 | logger.debug(messageWithStatus(message));
20 | }
21 | }
22 |
23 | @Override
24 | public void debug(Supplier message, Throwable t) {
25 | if (isEnabled()) {
26 | logger.debug(messageWithStatus(message), t);
27 | }
28 | }
29 |
30 | @Override
31 | public void info(Supplier message) {
32 | if (isEnabled()) {
33 | logger.info(messageWithStatus(message));
34 | }
35 | }
36 |
37 | @Override
38 | public void info(Supplier message, Throwable t) {
39 | if (isEnabled()) {
40 | logger.info(messageWithStatus(message), t);
41 | }
42 | }
43 |
44 | @Override
45 | public void warn(Supplier message) {
46 | if (isEnabled()) {
47 | logger.warn(messageWithStatus(message));
48 | }
49 | }
50 |
51 | @Override
52 | public void warn(Supplier message, Throwable t) {
53 | if (isEnabled()) {
54 | logger.warn(messageWithStatus(message), t);
55 | }
56 | }
57 |
58 | @Override
59 | public void error(Supplier message) {
60 | if (isEnabled()) {
61 | logger.error(messageWithStatus(message));
62 | }
63 | }
64 |
65 | @Override
66 | public void error(Supplier message, Throwable t) {
67 | if (isEnabled()) {
68 | logger.error(messageWithStatus(message), t);
69 | }
70 | }
71 |
72 | @Override
73 | public boolean isEnabled() {
74 | return logger.isEnabled();
75 | }
76 |
77 | private Supplier messageWithStatus(Supplier message) {
78 | return () -> "[state=" + stateSupplier.get().getName() + "] " + message.get();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/logs/TaggedLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.logs;
2 |
3 | import java.util.function.Supplier;
4 |
5 | public class TaggedLogger implements LazyLogger {
6 | private final String key;
7 | private final String value;
8 | private final LazyLogger logger;
9 |
10 | public TaggedLogger(String key, String value, LazyLogger logger) {
11 | this.key = key;
12 | this.value = value;
13 | this.logger = logger;
14 | }
15 |
16 | @Override
17 | public void debug(Supplier message) {
18 | if (isEnabled()) {
19 | logger.debug(messageWithDualConnectionUuid(message));
20 | }
21 | }
22 |
23 | @Override
24 | public void debug(Supplier message, Throwable t) {
25 | if (isEnabled()) {
26 | logger.debug(messageWithDualConnectionUuid(message), t);
27 | }
28 | }
29 |
30 | @Override
31 | public void info(Supplier message) {
32 | if (isEnabled()) {
33 | logger.info(messageWithDualConnectionUuid(message));
34 | }
35 | }
36 |
37 | @Override
38 | public void info(Supplier message, Throwable t) {
39 | if (isEnabled()) {
40 | logger.info(messageWithDualConnectionUuid(message), t);
41 | }
42 | }
43 |
44 | @Override
45 | public void warn(Supplier message) {
46 | if (isEnabled()) {
47 | logger.warn(messageWithDualConnectionUuid(message));
48 | }
49 | }
50 |
51 | @Override
52 | public void warn(Supplier message, Throwable t) {
53 | if (isEnabled()) {
54 | logger.warn(messageWithDualConnectionUuid(message), t);
55 | }
56 | }
57 |
58 | @Override
59 | public void error(Supplier message) {
60 | if (isEnabled()) {
61 | logger.error(messageWithDualConnectionUuid(message));
62 | }
63 | }
64 |
65 | @Override
66 | public void error(Supplier message, Throwable t) {
67 | if (isEnabled()) {
68 | logger.error(messageWithDualConnectionUuid(message), t);
69 | }
70 | }
71 |
72 | @Override
73 | public boolean isEnabled() {
74 | return logger.isEnabled();
75 | }
76 |
77 | private Supplier messageWithDualConnectionUuid(Supplier message) {
78 | return () -> "[" + key + "=" + value + "] " + message.get();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/state/NoOpStateListener.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.state;
2 |
3 | public final class NoOpStateListener implements StateListener {
4 | @Override
5 | public void transition(State from, State to) {
6 | // NoOp implementation.
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/state/State.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.state;
2 |
3 |
4 | import java.util.Objects;
5 |
6 | public final class State {
7 | private final String name;
8 |
9 | private State(String name) {
10 | this.name = name;
11 | }
12 |
13 | public static final State NOT_INITIALISED = new State("NOT_INITIALISED");
14 | public static final State MAIN = new State("MAIN");
15 | public static final State REPLICA = new State("REPLICA");
16 | public static final State CLOSED = new State("CLOSED");
17 | public static final State COMMITED_MAIN = new State("COMMITED_MAIN");
18 |
19 | public String getName() {
20 | return name;
21 | }
22 |
23 | @Override
24 | public String toString() {
25 | return "State{" +
26 | "name='" + name + '\'' +
27 | '}';
28 | }
29 |
30 | @Override
31 | public boolean equals(Object o) {
32 | if (this == o) return true;
33 | if (o == null || getClass() != o.getClass()) return false;
34 | State states = (State) o;
35 | return Objects.equals(name, states.name);
36 | }
37 |
38 | @Override
39 | public int hashCode() {
40 | return Objects.hash(name);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/state/StateListener.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.state;
2 |
3 |
4 | public interface StateListener {
5 |
6 | /**
7 | * Informs that {@link com.atlassian.db.replica.api.DualConnection } changed {@link State}.
8 | *
9 | * @param from {@link State} before the transition.
10 | * @param to {@link State} after the transition.
11 | */
12 | void transition(State from, State to);
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/util/Comparables.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.util;
2 |
3 | public class Comparables {
4 | public static > T max(T c1, T c2) {
5 | if (c1 == c2) {
6 | return c1;
7 | } else if (c1 == null) {
8 | return c2;
9 | } else if (c2 == null) {
10 | return c1;
11 | } else {
12 | return c1.compareTo(c2) > 0 ? c1 : c2;
13 | }
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/internal/util/ThreadSafe.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.util;
2 |
3 |
4 | import java.lang.annotation.Documented;
5 | import java.lang.annotation.ElementType;
6 | import java.lang.annotation.Retention;
7 | import java.lang.annotation.RetentionPolicy;
8 | import java.lang.annotation.Target;
9 |
10 | /*
11 | * Copyright (c) 2005 Brian Goetz and Tim Peierls
12 | * Released under the Creative Commons Attribution License
13 | * (http://creativecommons.org/licenses/by/2.5)
14 | * Official home: http://www.jcip.net
15 | *
16 | * Any republication or derived work distributed in source code form
17 | * must include this copyright and license notice.
18 | */
19 |
20 | /**
21 | * The class to which this annotation is applied is thread-safe. This means that
22 | * no sequences of accesses (reads and writes to public fields, calls to public methods)
23 | * may put the object into an invalid state, regardless of the interleaving of those actions
24 | * by the runtime, and without requiring any additional synchronization or coordination on the
25 | * part of the caller.
26 | */
27 | @Documented
28 | @Target(ElementType.TYPE)
29 | @Retention(RetentionPolicy.RUNTIME)
30 | public @interface ThreadSafe {
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/Cache.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | import com.atlassian.db.replica.internal.*;
4 |
5 | import java.util.*;
6 |
7 | /**
8 | * Holds a single value. Might be empty.
9 | *
10 | * @param cached value
11 | */
12 | public interface Cache {
13 |
14 | static > Cache cacheMonotonicValuesInMemory() {
15 | return new MonotonicMemoryCache<>();
16 | }
17 |
18 | /**
19 | * @return last known value or empty if it's unknown, not null
20 | */
21 | Optional get();
22 |
23 | /**
24 | * @param value last known value
25 | */
26 | void put(T value);
27 |
28 | /**
29 | * Forgets the last known value.
30 | */
31 | void reset();
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/ConnectionProvider.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | import java.sql.Connection;
4 | import java.sql.SQLException;
5 |
6 | public interface ConnectionProvider extends ReplicaConnectionProvider {
7 |
8 | boolean isReplicaAvailable();
9 |
10 | /**
11 | * @return a connection to the main database
12 | */
13 | Connection getMainConnection() throws SQLException;
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/DatabaseCall.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | import com.atlassian.db.replica.api.SqlCall;
4 | import com.atlassian.db.replica.api.reason.RouteDecision;
5 |
6 | import java.sql.SQLException;
7 |
8 | /**
9 | * Intercepts call to a database.
10 | */
11 | public interface DatabaseCall {
12 | T call(final SqlCall call, RouteDecision decision) throws SQLException;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/DirtyConnectionCloseHook.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | import java.sql.Connection;
4 | import java.sql.SQLException;
5 |
6 | /**
7 | * The hook will be invoked before closing a connection with uncommitted data.
8 | */
9 | public interface DirtyConnectionCloseHook {
10 | void onClose(Connection connection) throws SQLException;
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/Logger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | public interface Logger {
4 |
5 | default void debug(String message) {}
6 |
7 | default void debug(String message, Throwable t) {}
8 |
9 | default void info(String message) {}
10 |
11 | default void info(String message, Throwable t) {}
12 |
13 | default void warn(String message) {}
14 |
15 | default void warn(String message, Throwable t) {}
16 |
17 | default void error(String message) {}
18 |
19 | default void error(String message, Throwable t) {}
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/ReplicaConnectionPerUrlProvider.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | import com.atlassian.db.replica.api.jdbc.JdbcUrl;
4 |
5 | @FunctionalInterface
6 | public interface ReplicaConnectionPerUrlProvider {
7 | ReplicaConnectionProvider getReplicaConnectionProvider(JdbcUrl replicaUrl);
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/ReplicaConnectionProvider.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | import java.sql.Connection;
4 | import java.sql.SQLException;
5 |
6 | @FunctionalInterface
7 | public interface ReplicaConnectionProvider {
8 | /**
9 | * @return a connection to a replica database
10 | */
11 | Connection getReplicaConnection() throws SQLException;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/ReplicaConsistency.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | import com.atlassian.db.replica.internal.util.ThreadSafe;
4 |
5 | import java.sql.Connection;
6 | import java.util.function.Supplier;
7 |
8 | /**
9 | * Tracks data consistency between replica and main databases.
10 | */
11 | @ThreadSafe
12 | public interface ReplicaConsistency {
13 |
14 | /**
15 | * Informs that {@code main} received an UPDATE, INSERT or DELETE or transaction commit
16 | * when in a transaction.
17 | *
18 | * @param main connects to the main database
19 | */
20 | void write(Connection main);
21 |
22 | /**
23 | * Invoked just before transaction commit.
24 | *
25 | * Notice: The method will not handle all writes. Writes done outside of a transaction
26 | * needs to be handled in `ReplicaConnection#write`.
27 | *
28 | * @param main connects to the main database
29 | */
30 | default void preCommit(Connection main) {
31 | }
32 |
33 | /**
34 | * Judges if {@code replica} is ready to be queried.
35 | *
36 | * @param replica connects to the replica database
37 | * @return true if {@code replica} is consistent with main
38 | */
39 | boolean isConsistent(Supplier replica);
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/atlassian/db/replica/spi/SuppliedCache.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.spi;
2 |
3 | import com.atlassian.db.replica.internal.util.ThreadSafe;
4 |
5 | import java.util.Optional;
6 | import java.util.function.Supplier;
7 |
8 | @ThreadSafe
9 | public interface SuppliedCache {
10 | /**
11 | * @param supplier used to populate the value. It must always return a value, never null.
12 | * @return T last remembered value or empty
13 | */
14 | Optional get(Supplier supplier);
15 |
16 | /**
17 | * @return the cached value
18 | */
19 | Optional get();
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/AuroraClusterMock.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import com.google.common.collect.Sets;
4 | import org.h2.tools.SimpleResultSet;
5 | import org.mockito.Mockito;
6 | import org.mockito.invocation.InvocationOnMock;
7 | import org.mockito.stubbing.Answer;
8 |
9 | import java.sql.Connection;
10 | import java.sql.DriverManager;
11 | import java.sql.PreparedStatement;
12 | import java.sql.ResultSet;
13 | import java.sql.SQLException;
14 | import java.sql.Statement;
15 | import java.sql.Types;
16 | import java.util.Objects;
17 | import java.util.Set;
18 | import java.util.UUID;
19 | import java.util.concurrent.atomic.AtomicInteger;
20 |
21 | import static org.mockito.ArgumentMatchers.eq;
22 | import static org.mockito.Mockito.mock;
23 | import static org.mockito.Mockito.when;
24 |
25 | public class AuroraClusterMock {
26 | private final Connection connection;
27 | private static final Set replicas = Sets.newConcurrentHashSet();
28 | private static final AtomicInteger counter = new AtomicInteger();
29 |
30 | public AuroraClusterMock() throws SQLException {
31 | counter.set(0);
32 | replicas.clear();
33 | this.connection = mock(Connection.class);
34 | final PreparedStatement preparedStatement = mock(PreparedStatement.class);
35 | when(this.connection.prepareStatement(eq("SELECT server_id, durable_lsn, current_read_lsn, feedback_xmin, " +
36 | "round(extract(milliseconds from (now()-last_update_timestamp))) as state_lag_in_msec, replica_lag_in_msec " +
37 | "FROM aurora_replica_status() " +
38 | "WHERE session_id != 'MASTER_SESSION_ID' and last_update_timestamp > NOW() - INTERVAL '5 minutes';")))
39 | .thenReturn(preparedStatement);
40 | when(preparedStatement.executeQuery()).thenAnswer((Answer) invocation -> {
41 | return auroraGlobalDbInstanceStatus();
42 | });
43 | }
44 |
45 | public Connection getMainConnection() {
46 | return connection;
47 | }
48 |
49 | public AuroraClusterMock scaleUp() {
50 | replicas.add(
51 | new Node(
52 | "apg-global-db-rpo-mammothrw-elephantro-n" + counter.incrementAndGet(),
53 | UUID.randomUUID().toString()
54 | )
55 | );
56 | return this;
57 | }
58 |
59 | public AuroraClusterMock scaleDown() {
60 | replicas.remove(replicas.stream().findAny().orElse(null));
61 | return this;
62 | }
63 |
64 | public static ResultSet auroraGlobalDbInstanceStatus() {
65 | SimpleResultSet rs = new SimpleResultSet();
66 | rs.addColumn("SERVER_ID", Types.VARCHAR, 255, 0);
67 | rs.addColumn("SESSION_ID", Types.VARCHAR, 255, 0);
68 | rs.addColumn("REPLICA_LAG_IN_MSEC", Types.INTEGER,0,0);
69 | rs.addColumn("DURABLE_LSN", Types.INTEGER,0,0);
70 | rs.addColumn("CURRENT_READ_LSN", Types.INTEGER,0,0);
71 | rs.addColumn("FEEDBACK_XMIN", Types.INTEGER,0,0);
72 | rs.addColumn("STATE_LAG_IN_MSEC", Types.INTEGER,0,0);
73 | replicas.forEach(replica -> rs.addRow(replica.getServerId(), replica.getSessionId()));
74 | return rs;
75 | }
76 |
77 | private static class Node {
78 | private final String serverId;
79 | private final String sessionId;
80 |
81 | public Node(String serverId, String sessionId) {
82 | this.serverId = serverId;
83 | this.sessionId = sessionId;
84 | }
85 |
86 | public String getServerId() {
87 | return serverId;
88 | }
89 |
90 | public String getSessionId() {
91 | return sessionId;
92 | }
93 |
94 | @Override
95 | public boolean equals(Object o) {
96 | if (this == o) return true;
97 | if (o == null || getClass() != o.getClass()) return false;
98 | Node node = (Node) o;
99 | return Objects.equals(serverId, node.serverId) && Objects.equals(sessionId, node.sessionId);
100 | }
101 |
102 | @Override
103 | public int hashCode() {
104 | return Objects.hash(serverId, sessionId);
105 | }
106 |
107 | @Override
108 | public String toString() {
109 | return "Node{" +
110 | "serverId='" + serverId + '\'' +
111 | ", sessionId='" + sessionId + '\'' +
112 | '}';
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/CacheLoader.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import java.util.concurrent.CountDownLatch;
4 | import java.util.concurrent.ExecutorService;
5 | import java.util.concurrent.Executors;
6 |
7 | public class CacheLoader {
8 | private final ExecutorService executor = Executors.newCachedThreadPool();
9 |
10 | public WaitingWork asyncPutWithSlowSupplier(
11 | ThrottledCache cache,
12 | long value
13 | ) throws InterruptedException {
14 | final CountDownLatch asyncThreadStarted = new CountDownLatch(1);
15 | final CountDownLatch asyncThreadFinished = new CountDownLatch(1);
16 | final CountDownLatch threadWaiting = new CountDownLatch(1);
17 | executor.submit(() -> {
18 | cache.get(() -> {
19 | asyncThreadStarted.countDown();
20 | try {
21 | threadWaiting.await();
22 | } catch (InterruptedException e) {
23 | throw new RuntimeException(e);
24 | }
25 | return value;
26 | });
27 | asyncThreadFinished.countDown();
28 | });
29 | asyncThreadStarted.await();
30 | return new WaitingWork(threadWaiting, asyncThreadFinished);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/ReplicaConsistencyMock.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import com.atlassian.db.replica.spi.ReplicaConsistency;
4 |
5 | import java.sql.Connection;
6 | import java.util.function.Supplier;
7 |
8 | public class ReplicaConsistencyMock implements ReplicaConsistency {
9 | private final boolean consistent;
10 |
11 | public ReplicaConsistencyMock(boolean consistent) {
12 | this.consistent = consistent;
13 | }
14 |
15 | @Override
16 | public void write(Connection main) {
17 |
18 | }
19 |
20 | @Override
21 | public boolean isConsistent(Supplier replica) {
22 | Connection connection = replica.get();
23 | return consistent;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/ThrottledCacheTest.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.ArrayList;
6 | import java.util.ConcurrentModificationException;
7 | import java.util.Optional;
8 | import java.util.Random;
9 |
10 |
11 | import static java.time.Duration.ofMillis;
12 | import static java.time.Duration.ofSeconds;
13 | import static org.assertj.core.api.Assertions.assertThat;
14 | import static org.assertj.core.api.Assertions.assertThatNoException;
15 | import static org.assertj.core.api.Assertions.catchThrowable;
16 |
17 | public class ThrottledCacheTest {
18 | private final CacheLoader cacheLoader = new CacheLoader();
19 | private final TickingClock clock = new TickingClock();
20 |
21 | @Test
22 | public void staleWhileInvalidating() throws InterruptedException {
23 | ThrottledCache cache = ThrottledCache.builder(clock, ofSeconds(60)).build();
24 |
25 | cache.get(() -> 1L);
26 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
27 |
28 | assertThat(cache.get(this::anyValue)).hasValue(1L);
29 | }
30 |
31 | @Test
32 | public void readsCachedValue() {
33 | ThrottledCache cache = ThrottledCache.builder(clock, ofSeconds(60)).build();
34 |
35 | cache.get(() -> 1L);
36 |
37 | assertThat(cache.get()).hasValue(1L);
38 | }
39 |
40 | @Test
41 | public void serveLatestValue() {
42 | ThrottledCache cache = ThrottledCache.builder(clock, ofSeconds(60)).build();
43 | cache.get(this::anyValue);
44 | cache.get(this::anyValue);
45 |
46 | assertThat(cache.get(() -> 4L)).hasValue(4L);
47 | }
48 |
49 | @Test
50 | public void noConcurrentInvalidation() throws InterruptedException {
51 | ThrottledCache cache = ThrottledCache.builder(clock, ofSeconds(60)).build();
52 |
53 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
54 | cache.get(() -> {
55 | throw new ConcurrentModificationException("Concurrent invalidation is not allowed");
56 | });
57 |
58 | assertThatNoException();
59 | }
60 |
61 | @Test
62 | public void emptyWhenLoadingFirstTime() throws InterruptedException {
63 | ThrottledCache cache = ThrottledCache.builder(clock, ofSeconds(60)).build();
64 |
65 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
66 |
67 | assertThat(cache.get(this::anyValue)).isEmpty();
68 | }
69 |
70 | @Test
71 | public void shouldSupplierBlockTheCacheUntilTimeout() throws InterruptedException {
72 | ThrottledCache cache = ThrottledCache.builder(clock, ofMillis(1500)).build();
73 |
74 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
75 |
76 | clock.tick();
77 |
78 | assertThat(cache.get(this::anyValue)).isEmpty();
79 | }
80 |
81 | @Test
82 | public void shouldntSupplierBlockTheCacheForever() throws InterruptedException {
83 | ThrottledCache cache = ThrottledCache.builder(clock, ofMillis(1500)).build();
84 |
85 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
86 |
87 | clock.tick();
88 | clock.tick();
89 |
90 | assertThat(cache.get(this::anyValue)).isNotEmpty();
91 |
92 | }
93 |
94 | @Test
95 | public void shouldNotRobbedThreadReleaseLock() throws InterruptedException {
96 | ThrottledCache cache = ThrottledCache.builder(clock, ofMillis(1500)).build();
97 |
98 | final WaitingWork firstThreadWork = cacheLoader.asyncPutWithSlowSupplier(cache, 1);
99 | clock.tick();
100 | clock.tick();
101 | cacheLoader.asyncPutWithSlowSupplier(cache, 2);
102 | firstThreadWork.finish();
103 |
104 | final Throwable throwable = catchThrowable(() -> cache.get(() -> {
105 | throw new ConcurrentModificationException("Concurrent invalidation is not allowed");
106 | }));
107 |
108 | assertThat(throwable).doesNotThrowAnyException();
109 | }
110 |
111 | @Test
112 | public void shouldntRobbedThreadUpdateValue() throws InterruptedException {
113 | ThrottledCache cache = ThrottledCache.builder(clock, ofMillis(1500)).build();
114 |
115 | final WaitingWork firstThreadWork = cacheLoader.asyncPutWithSlowSupplier(cache, 1);
116 | clock.tick();
117 | clock.tick();
118 | cache.get(() -> 2L);
119 | firstThreadWork.finish();
120 | assertThat(cache.get()).isEqualTo(Optional.of(2L));
121 | }
122 |
123 | @Test
124 | public void shouldFailingSupplierReleaseTheLock() {
125 | ThrottledCache cache = ThrottledCache.builder(clock, ofMillis(1500)).build();
126 |
127 | cache.get(() -> 1L);
128 | final Throwable throwable = catchThrowable(() -> {
129 | cache.get(() -> {
130 | throw new RuntimeException();
131 | });
132 | });
133 |
134 | assertThat(throwable).isNotNull();
135 | assertThat(cache.get()).isEqualTo(Optional.of(1L));
136 | assertThat(cache.get(() -> 2L)).isEqualTo(Optional.of(2L));
137 | }
138 |
139 | @Test
140 | public void shouldTimeoutLockMultipleTimes() throws InterruptedException {
141 | ThrottledCache cache = ThrottledCache.builder(clock, ofMillis(500)).build();
142 |
143 | final ArrayList waitingWorks = new ArrayList<>();
144 | for (int i = 0; i < 32; i++) {
145 | final WaitingWork waitingWork = cacheLoader.asyncPutWithSlowSupplier(cache, i);
146 | waitingWorks.add(waitingWork);
147 | clock.tick();
148 | }
149 | waitingWorks.forEach(WaitingWork::finish);
150 |
151 | assertThat(cache.get()).isEqualTo(Optional.of(31L));
152 | }
153 |
154 | private Long anyValue() {
155 | return new Random().nextLong();
156 | }
157 |
158 | }
159 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/ThrottledSequenceCacheTest.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.ArrayList;
6 | import java.util.ConcurrentModificationException;
7 | import java.util.List;
8 | import java.util.Optional;
9 | import java.util.Random;
10 |
11 | import static java.time.Duration.ofMillis;
12 | import static java.time.Duration.ofSeconds;
13 | import static org.assertj.core.api.Assertions.assertThat;
14 | import static org.assertj.core.api.Assertions.assertThatNoException;
15 | import static org.assertj.core.api.Assertions.catchThrowable;
16 |
17 | public class ThrottledSequenceCacheTest {
18 | private final CacheLoader cacheLoader = new CacheLoader();
19 |
20 | private final TickingClock clock = new TickingClock();
21 |
22 | @Test
23 | public void staleWhileInvalidating() throws InterruptedException {
24 | ThrottledCache cache = ThrottledCache.builder(
25 | clock,
26 | ofSeconds(60)
27 | ).sequenceCache(Long::compare).build();
28 |
29 | cache.get(() -> 1L);
30 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
31 |
32 | assertThat(cache.get(this::anyValue)).hasValue(1L);
33 | }
34 |
35 | @Test
36 | public void shouldAllowOnlyToIncreaseTheCachedValue() throws InterruptedException {
37 | ThrottledCache cache = ThrottledCache.builder(
38 | clock,
39 | ofSeconds(60)
40 | ).sequenceCache(Long::compare).build();
41 |
42 | cache.get(() -> 1L);
43 | cache.get(() -> 2L);
44 | cache.get(() -> 3L);
45 | cache.get(() -> 2L);
46 |
47 | assertThat(cache.get(() -> 1L)).hasValue(3L);
48 | }
49 |
50 | @Test
51 | public void readsCachedValue() {
52 | ThrottledCache cache = ThrottledCache.builder(
53 | clock,
54 | ofSeconds(60)
55 | ).sequenceCache(Long::compare).build();
56 |
57 | cache.get(() -> 1L);
58 |
59 | assertThat(cache.get()).hasValue(1L);
60 | }
61 |
62 | @Test
63 | public void serveLatestValue() {
64 | ThrottledCache cache = ThrottledCache.builder(
65 | clock,
66 | ofSeconds(60)
67 | ).sequenceCache(Long::compare).build();
68 | cache.get(() -> 1L);
69 | cache.get(() -> 2L);
70 |
71 | assertThat(cache.get(() -> 4L)).hasValue(4L);
72 | }
73 |
74 | @Test
75 | public void noConcurrentInvalidation() throws InterruptedException {
76 | ThrottledCache cache = ThrottledCache.builder(
77 | clock,
78 | ofSeconds(60)
79 | ).sequenceCache(Long::compare).build();
80 |
81 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
82 | cache.get(() -> {
83 | throw new ConcurrentModificationException("Concurrent invalidation is not allowed");
84 | });
85 |
86 | assertThatNoException();
87 | }
88 |
89 | @Test
90 | public void emptyWhenLoadingFirstTime() throws InterruptedException {
91 | ThrottledCache cache = ThrottledCache.builder(
92 | clock,
93 | ofSeconds(60)
94 | ).sequenceCache(Long::compare).build();
95 |
96 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
97 |
98 | assertThat(cache.get(this::anyValue)).isEmpty();
99 | }
100 |
101 | @Test
102 | public void shouldSupplierBlockTheCacheUntilTimeout() throws InterruptedException {
103 | ThrottledCache cache = ThrottledCache.builder(
104 | clock,
105 | ofMillis(1500)
106 | ).sequenceCache(Long::compare).build();
107 |
108 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
109 |
110 | clock.tick();
111 |
112 | assertThat(cache.get(this::anyValue)).isEmpty();
113 | }
114 |
115 | @Test
116 | public void shouldntSupplierBlockTheCacheForever() throws InterruptedException {
117 | ThrottledCache cache = ThrottledCache.builder(
118 | clock,
119 | ofMillis(1500)
120 | ).sequenceCache(Long::compare).build();
121 |
122 | cacheLoader.asyncPutWithSlowSupplier(cache, anyValue());
123 |
124 | clock.tick();
125 | clock.tick();
126 |
127 | assertThat(cache.get(this::anyValue)).isNotEmpty();
128 |
129 | }
130 |
131 | @Test
132 | public void shouldNotRobbedThreadReleaseLock() throws InterruptedException {
133 | ThrottledCache cache = ThrottledCache.builder(
134 | clock,
135 | ofMillis(1500)
136 | ).sequenceCache(Long::compare).build();
137 |
138 | final WaitingWork firstThreadWork = cacheLoader.asyncPutWithSlowSupplier(cache, 1);
139 | clock.tick();
140 | clock.tick();
141 | cacheLoader.asyncPutWithSlowSupplier(cache, 2);
142 | firstThreadWork.finish();
143 |
144 | final Throwable throwable = catchThrowable(() -> cache.get(() -> {
145 | throw new ConcurrentModificationException("Concurrent invalidation is not allowed");
146 | }));
147 |
148 | assertThat(throwable).doesNotThrowAnyException();
149 | }
150 |
151 | @Test
152 | public void shouldntRobbedThreadUpdateValue() throws InterruptedException {
153 | ThrottledCache cache = ThrottledCache.builder(
154 | clock,
155 | ofMillis(1500)
156 | ).sequenceCache(Long::compare).build();
157 |
158 | final WaitingWork firstThreadWork = cacheLoader.asyncPutWithSlowSupplier(cache, 1);
159 | clock.tick();
160 | clock.tick();
161 | cache.get(() -> 2L);
162 | firstThreadWork.finish();
163 | assertThat(cache.get()).isEqualTo(Optional.of(2L));
164 | }
165 |
166 | @Test
167 | public void shouldFailingSupplierReleaseTheLock() {
168 | ThrottledCache cache = ThrottledCache.builder(
169 | clock,
170 | ofMillis(1500)
171 | ).sequenceCache(Long::compare).build();
172 |
173 | cache.get(() -> 1L);
174 | final Throwable throwable = catchThrowable(() -> {
175 | cache.get(() -> {
176 | throw new RuntimeException();
177 | });
178 | });
179 |
180 | assertThat(throwable).isNotNull();
181 | assertThat(cache.get()).isEqualTo(Optional.of(1L));
182 | assertThat(cache.get(() -> 2L)).isEqualTo(Optional.of(2L));
183 | }
184 |
185 | @Test
186 | public void shouldUpdateValuesWhenCacheLoadSlowerThanTimeout() throws InterruptedException {
187 | ThrottledCache cache = ThrottledCache.builder(
188 | clock,
189 | ofMillis(500)
190 | ).sequenceCache(Long::compare).build();
191 |
192 | final List waitingWorks = new ArrayList<>();
193 | for (int i = 0; i < 32; i++) {
194 | final WaitingWork waitingWork = cacheLoader.asyncPutWithSlowSupplier(cache, i);
195 | waitingWorks.add(waitingWork);
196 | clock.tick();
197 | }
198 | for (int i = 0; i < 32; i++) {
199 | waitingWorks.get(i).finish();
200 | assertThat(cache.get()).isEqualTo(Optional.of((long) i));
201 | }
202 | }
203 |
204 | private Long anyValue() {
205 | return new Random().nextLong();
206 | }
207 |
208 | }
209 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/TickingClock.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import org.apache.commons.lang.NotImplementedException;
4 |
5 | import java.time.Clock;
6 | import java.time.Instant;
7 | import java.time.ZoneId;
8 |
9 | import static java.time.Duration.ofSeconds;
10 |
11 | class TickingClock extends Clock {
12 | private Instant instant = Instant.now();
13 |
14 | public void tick() {
15 | instant = instant.plus(ofSeconds(1));
16 | }
17 |
18 | @Override
19 | public ZoneId getZone() {
20 | throw new NotImplementedException();
21 | }
22 |
23 | @Override
24 | public Clock withZone(ZoneId zone) {
25 | throw new NotImplementedException();
26 | }
27 |
28 | @Override
29 | public Instant instant() {
30 | return instant;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/WaitingWork.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api;
2 |
3 | import java.util.concurrent.CountDownLatch;
4 |
5 | final class WaitingWork {
6 | private final CountDownLatch threadWaiting;
7 | private final CountDownLatch asyncThreadFinished;
8 |
9 | WaitingWork(CountDownLatch threadWaiting, CountDownLatch asyncThreadFinished) {
10 | this.threadWaiting = threadWaiting;
11 | this.asyncThreadFinished = asyncThreadFinished;
12 | }
13 |
14 | public void finish() {
15 | threadWaiting.countDown();
16 | try {
17 | asyncThreadFinished.await();
18 | } catch (InterruptedException e) {
19 | throw new RuntimeException(e);
20 | }
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/circuitbreaker/TestCircuitBreaker.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.circuitbreaker;
2 |
3 | import com.atlassian.db.replica.api.DualConnection;
4 | import com.atlassian.db.replica.api.mocks.ConnectionProviderMock;
5 | import com.atlassian.db.replica.internal.ReadReplicaUnsupportedOperationException;
6 | import com.atlassian.db.replica.internal.circuitbreaker.BreakOnNotSupportedOperations;
7 | import org.junit.jupiter.api.AfterEach;
8 | import org.junit.jupiter.api.Test;
9 |
10 | import java.sql.Connection;
11 | import java.sql.SQLException;
12 |
13 | import static com.atlassian.db.replica.api.Queries.SIMPLE_QUERY;
14 | import static com.atlassian.db.replica.api.mocks.CircularConsistency.permanentConsistency;
15 | import static org.assertj.core.api.Assertions.assertThat;
16 | import static org.assertj.core.api.Assertions.catchThrowable;
17 |
18 | public class TestCircuitBreaker {
19 |
20 | @AfterEach
21 | public void after() {
22 | BreakOnNotSupportedOperations.reset();
23 | }
24 |
25 | @Test
26 | public void shouldPropagateUnimplementedMethodCall() throws SQLException {
27 | final Connection connection = DualConnection.builder(
28 | new ConnectionProviderMock(),
29 | permanentConsistency().build()
30 | ).build();
31 | Throwable firstCall = catchThrowable(() -> connection.prepareStatement(SIMPLE_QUERY).getMetaData());
32 | final ConnectionProviderMock connectionProvider = new ConnectionProviderMock();
33 | final Connection newConnection = DualConnection.builder(connectionProvider, permanentConsistency().build()).build();
34 | Throwable secondCall = catchThrowable(() -> newConnection.prepareStatement(SIMPLE_QUERY).getMetaData());
35 |
36 | assertThat(connectionProvider.getPreparedStatements()).isEmpty();
37 | assertThat(firstCall).isInstanceOf(ReadReplicaUnsupportedOperationException.class);
38 | assertThat(secondCall).isInstanceOf(ReadReplicaUnsupportedOperationException.class);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/mocks/CircularConsistency.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.mocks;
2 |
3 | import com.atlassian.db.replica.spi.ReplicaConsistency;
4 | import com.google.common.collect.ImmutableList;
5 |
6 | import java.sql.Connection;
7 | import java.util.List;
8 | import java.util.concurrent.atomic.AtomicInteger;
9 | import java.util.function.Supplier;
10 |
11 | public class CircularConsistency implements ReplicaConsistency {
12 | private final List consistency;
13 | private final boolean ignoreSupplier;
14 | private final AtomicInteger counter = new AtomicInteger();
15 |
16 | private CircularConsistency(List consistency, boolean ignoreSupplier) {
17 | this.consistency = consistency;
18 | this.ignoreSupplier = ignoreSupplier;
19 | }
20 |
21 | @Override
22 | public void write(Connection main) {
23 |
24 | }
25 |
26 | @Override
27 | public boolean isConsistent(Supplier replica) {
28 | if (!this.ignoreSupplier) {
29 | replica.get();
30 | }
31 | return consistency.get(counter.getAndIncrement() % consistency.size());
32 | }
33 |
34 | public static Builder permanentConsistency() {
35 | return new Builder(ImmutableList.of(true));
36 | }
37 |
38 | public static Builder permanentInconsistency() {
39 | return new Builder(ImmutableList.of(false));
40 | }
41 |
42 | public static class Builder {
43 | private final List consistency;
44 | private boolean ignoreSupplier = false;
45 |
46 | public Builder(List consistency) {
47 | this.consistency = consistency;
48 | }
49 |
50 | public Builder ignoreSupplier(boolean ignoreSupplier) {
51 | this.ignoreSupplier = ignoreSupplier;
52 | return this;
53 | }
54 |
55 | public ReplicaConsistency build() {
56 | return new CircularConsistency(consistency, ignoreSupplier);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/mocks/MockLogger.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.mocks;
2 |
3 | import com.atlassian.db.replica.spi.Logger;
4 |
5 | import java.util.LinkedList;
6 | import java.util.List;
7 | import java.util.stream.Collectors;
8 |
9 | import static java.util.stream.Collectors.joining;
10 |
11 | public class MockLogger implements Logger {
12 | private final List messages = new LinkedList<>();
13 |
14 | public void debug(String message) {
15 | messages.add(message);
16 | }
17 |
18 | public void debug(String message, Throwable t) {
19 | messages.add(message);
20 | }
21 |
22 | public void info(String message) {
23 | messages.add(message);
24 | }
25 |
26 | public void info(String message, Throwable t) {
27 | messages.add(message);
28 | }
29 |
30 | public void warn(String message) {
31 | messages.add(message);
32 | }
33 |
34 | public void warn(String message, Throwable t) {
35 | messages.add(message);
36 | }
37 |
38 | public void error(String message) {
39 | messages.add(message);
40 | }
41 |
42 | public void error(String message, Throwable t) {
43 | messages.add(message);
44 | }
45 |
46 | public List getMessages() {
47 | return messages;
48 | }
49 |
50 | @Override
51 | public String toString() {
52 | return messages.stream().map(message -> message + "\n").collect(joining());
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/mocks/NoOpConnectionProvider.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.mocks;
2 |
3 | import com.atlassian.db.replica.spi.ConnectionProvider;
4 |
5 | import java.sql.Connection;
6 |
7 | public class NoOpConnectionProvider implements ConnectionProvider {
8 |
9 | @Override
10 | public boolean isReplicaAvailable() {
11 | return true;
12 | }
13 |
14 | @Override
15 | public Connection getMainConnection() {
16 | return new NoOpConnection();
17 | }
18 |
19 | @Override
20 | public Connection getReplicaConnection() {
21 | return new NoOpConnection();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/mocks/ReadOnlyAwareConnection.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.mocks;
2 |
3 | import java.sql.Connection;
4 |
5 | public abstract class ReadOnlyAwareConnection implements Connection {
6 | private boolean isReadOnly;
7 |
8 | @Override
9 | public void setReadOnly(boolean readOnly) {
10 | this.isReadOnly = readOnly;
11 | }
12 |
13 | @Override
14 | public boolean isReadOnly() {
15 | return isReadOnly;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/api/mocks/SingleConnectionProvider.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.api.mocks;
2 |
3 | import com.atlassian.db.replica.spi.ConnectionProvider;
4 |
5 | import java.sql.Connection;
6 |
7 | public class SingleConnectionProvider implements ConnectionProvider {
8 | private Connection connection;
9 |
10 | public SingleConnectionProvider(Connection connection) {
11 | this.connection = connection;
12 | }
13 |
14 | @Override
15 | public boolean isReplicaAvailable() {
16 | return true;
17 | }
18 |
19 | @Override
20 | public Connection getMainConnection() {
21 | return connection;
22 | }
23 |
24 | @Override
25 | public Connection getReplicaConnection() {
26 | return connection;
27 | }
28 |
29 | public void setConnection(Connection connection) {
30 | this.connection = connection;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/AuroraPostgresLsnReplicaConsistencyTest.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.api.AuroraPostgresLsnReplicaConsistency;
4 | import com.atlassian.db.replica.internal.util.ConnectionSupplier;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.sql.Connection;
9 | import java.sql.PreparedStatement;
10 | import java.sql.ResultSet;
11 | import java.sql.SQLException;
12 |
13 | import static org.assertj.core.api.Assertions.*;
14 | import static org.mockito.ArgumentMatchers.anyString;
15 | import static org.mockito.Mockito.mock;
16 | import static org.mockito.Mockito.when;
17 |
18 | public class AuroraPostgresLsnReplicaConsistencyTest {
19 |
20 | private static final long LAST_WRITE_LSN = 8679792506L;
21 |
22 | private VolatileCache lastWriteCache;
23 | private AuroraPostgresLsnReplicaConsistency consistency;
24 | private Connection main;
25 | private Connection replica;
26 |
27 | @BeforeEach
28 | public void setUp() {
29 | lastWriteCache = new VolatileCache<>();
30 | consistency = new AuroraPostgresLsnReplicaConsistency.Builder()
31 | .cacheLastWrite(lastWriteCache)
32 | .build();
33 |
34 | main = mock(Connection.class);
35 | replica = mock(Connection.class);
36 | }
37 |
38 | @Test
39 | public void shouldThrowRuntimeExceptionWhenLastWriteLsnFails() throws Exception {
40 | mockLsnFetchingFailure(main);
41 |
42 | assertThatThrownBy(() -> consistency.write(main)).isInstanceOf(RuntimeException.class);
43 | }
44 |
45 | @Test
46 | public void shouldBeInconsistentWhenLastWriteLsnUnknown() {
47 | lastWriteCache.reset();
48 |
49 | boolean isConsistent = consistency.isConsistent(new ConnectionSupplier(replica));
50 |
51 | assertThat(isConsistent).isFalse();
52 | }
53 |
54 | @Test
55 | public void shouldBeInconsistentWhenReplicaLsnFails() throws Exception {
56 | mockLsnFetching(main, LAST_WRITE_LSN);
57 | mockLsnFetchingFailure(replica);
58 | consistency.write(main);
59 |
60 | boolean isConsistent = consistency.isConsistent(new ConnectionSupplier(replica));
61 |
62 | assertThat(isConsistent).isFalse();
63 | }
64 |
65 | @Test
66 | public void shouldBeInconsistentWhenReplicaIsBehind() throws Exception {
67 | mockLsnFetching(main, LAST_WRITE_LSN);
68 | mockLsnFetching(replica, LAST_WRITE_LSN - 1);
69 | consistency.write(main);
70 |
71 | boolean isConsistent = consistency.isConsistent(new ConnectionSupplier(replica));
72 |
73 | assertThat(isConsistent).isFalse();
74 | }
75 |
76 | @Test
77 | public void shouldBeConsistentWhenReplicaIsAhead() throws Exception {
78 | mockLsnFetching(main, LAST_WRITE_LSN);
79 | mockLsnFetching(replica, LAST_WRITE_LSN + 1);
80 | consistency.write(main);
81 |
82 | boolean isConsistent = consistency.isConsistent(new ConnectionSupplier(replica));
83 |
84 | assertThat(isConsistent).isTrue();
85 | }
86 |
87 | @Test
88 | public void shouldBeConsistentWhenReplicaCaughtUp() throws Exception {
89 | mockLsnFetching(main, LAST_WRITE_LSN);
90 | mockLsnFetching(replica, LAST_WRITE_LSN);
91 | consistency.write(main);
92 |
93 | boolean isConsistent = consistency.isConsistent(new ConnectionSupplier(replica));
94 |
95 | assertThat(isConsistent).isTrue();
96 | }
97 |
98 | private void mockLsnFetching(Connection connection, long lsn) throws SQLException {
99 | PreparedStatement preparedStatement = mock(PreparedStatement.class);
100 | when(connection.prepareStatement(anyString())).thenReturn(preparedStatement);
101 | ResultSet resultSet = mock(ResultSet.class);
102 | when(preparedStatement.executeQuery()).thenReturn(resultSet);
103 | when(resultSet.getLong("lsn")).thenReturn(lsn);
104 | }
105 |
106 | private void mockLsnFetchingFailure(Connection connection) throws SQLException {
107 | PreparedStatement preparedStatement = mock(PreparedStatement.class);
108 | when(connection.prepareStatement(anyString())).thenReturn(preparedStatement);
109 | when(preparedStatement.executeQuery()).thenThrow(new SQLException());
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/DefaultReplicaConnectionPerUrlProvider.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.api.jdbc.JdbcUrl;
4 | import com.atlassian.db.replica.spi.ReplicaConnectionPerUrlProvider;
5 | import com.atlassian.db.replica.spi.ReplicaConnectionProvider;
6 |
7 | import java.sql.DriverManager;
8 |
9 | public class DefaultReplicaConnectionPerUrlProvider implements ReplicaConnectionPerUrlProvider {
10 | private final String username;
11 | private final String password;
12 |
13 | public DefaultReplicaConnectionPerUrlProvider(String username, String password) {
14 | this.username = username;
15 | this.password = password;
16 | }
17 |
18 | @Override
19 | public ReplicaConnectionProvider getReplicaConnectionProvider(JdbcUrl replicaUrl) {
20 | return () -> DriverManager.getConnection(replicaUrl.toString(), username, password);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/LazyReferenceTest.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.concurrent.CountDownLatch;
6 | import java.util.concurrent.ExecutorService;
7 | import java.util.concurrent.Executors;
8 | import java.util.concurrent.atomic.AtomicInteger;
9 |
10 | import static org.assertj.core.api.Assertions.assertThat;
11 |
12 |
13 | public class LazyReferenceTest {
14 |
15 | @Test
16 | public void shouldCreateValueOnce() {
17 | final CountingReference countingReference = new CountingReference();
18 |
19 | countingReference.get();
20 | countingReference.get();
21 | countingReference.get();
22 |
23 | assertThat(countingReference.getCounter()).isEqualTo(1);
24 | }
25 |
26 | @Test
27 | public void shouldCreateValueOnceWhileAccessedConcurrently() throws InterruptedException {
28 | final CountingReference countingReference = new CountingReference();
29 | final int threads = 128;
30 | final ExecutorService executor = Executors.newFixedThreadPool(threads);
31 | final CountDownLatch start = new CountDownLatch(threads);
32 | final CountDownLatch end = new CountDownLatch(threads);
33 | for (int i = 0; i < threads; i++) {
34 | executor.submit(() -> fetchReference(countingReference, start, end));
35 | }
36 | end.await();
37 |
38 | assertThat(countingReference.getCounter()).isEqualTo(1);
39 | executor.shutdown();
40 | }
41 |
42 | private void fetchReference(CountingReference countingReference, CountDownLatch start, CountDownLatch end) {
43 | start.countDown();
44 | try {
45 | start.await();
46 | } catch (InterruptedException e) {
47 | throw new RuntimeException(e);
48 | }
49 | countingReference.get();
50 | end.countDown();
51 | }
52 |
53 |
54 | private static class CountingReference extends LazyReference {
55 | final AtomicInteger counter = new AtomicInteger();
56 |
57 | private CountingReference() {
58 | super();
59 | }
60 |
61 | @Override
62 | protected Integer create() {
63 | return counter.incrementAndGet();
64 | }
65 |
66 | public int getCounter() {
67 | return counter.get();
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/LsnReplicaConsistency.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.internal.util.ThreadSafe;
4 | import com.atlassian.db.replica.spi.*;
5 | import org.postgresql.replication.LogSequenceNumber;
6 |
7 | import java.sql.Connection;
8 | import java.sql.PreparedStatement;
9 | import java.sql.ResultSet;
10 | import java.util.*;
11 | import java.util.function.Supplier;
12 |
13 | /**
14 | * [LSN] means "log sequence number". It points to a place in the PostgreSQL write-ahead log.
15 | * Each replica updates its WAL via recovery. Recovery-time LSN can be queried by recovery info.
16 | * When a replica is caught up, it's no longer in recovery. Non-recovery LSN can be queried by backup control.
17 | *
18 | * @see LSN
19 | * @see recovery info
20 | * @see backup control
21 | *
22 | * It's a DB specific implementation used in integration tests.
23 | */
24 | @ThreadSafe
25 | public class LsnReplicaConsistency implements ReplicaConsistency {
26 |
27 | private final Cache lastWrite = Cache.cacheMonotonicValuesInMemory();
28 |
29 | @Override
30 | public void write(Connection main) {
31 | try {
32 | lastWrite.put(queryLsn(main));
33 | } catch (Exception e) {
34 | //TODO: log warning
35 | lastWrite.reset();
36 | }
37 | }
38 |
39 | @Override
40 | public boolean isConsistent(Supplier replica) {
41 | Optional maybeLastWrite = lastWrite.get();
42 | if (!maybeLastWrite.isPresent()) {
43 | return false;
44 | }
45 | LogSequenceNumber lastRefresh;
46 | try {
47 | lastRefresh = queryLsn(replica.get());
48 | } catch (Exception e) {
49 | //TODO: log warning
50 | return false;
51 | }
52 | return lastRefresh.asLong() >= maybeLastWrite.get().asLong();
53 | }
54 |
55 | private LogSequenceNumber queryLsn(Connection connection) throws Exception {
56 | try (
57 | PreparedStatement query = prepareQuery(connection);
58 | ResultSet results = query.executeQuery()
59 | ) {
60 | results.next();
61 | String lsn = results.getString("lsn");
62 | return LogSequenceNumber.valueOf(lsn);
63 | }
64 | }
65 |
66 | private PreparedStatement prepareQuery(Connection connection) throws Exception {
67 | return connection.prepareStatement(
68 | "SELECT\n" +
69 | "CASE WHEN pg_is_in_recovery()\n" +
70 | " THEN pg_last_xlog_replay_location()\n" +
71 | " ELSE pg_current_xlog_location()\n" +
72 | "END AS lsn;"
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/LsnReplicaConsistencyTest.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.internal.util.ConnectionSupplier;
4 | import com.atlassian.db.replica.spi.ReplicaConsistency;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.sql.Connection;
8 | import java.sql.PreparedStatement;
9 | import java.sql.ResultSet;
10 | import java.sql.SQLException;
11 | import java.util.function.Supplier;
12 |
13 | import static org.assertj.core.api.Assertions.assertThat;
14 | import static org.mockito.ArgumentMatchers.anyString;
15 | import static org.mockito.Mockito.mock;
16 | import static org.mockito.Mockito.when;
17 |
18 | public class LsnReplicaConsistencyTest {
19 |
20 | @Test
21 | public void shouldBeConsistentIfReplicaGivesTheSameLsnAsMain() throws SQLException {
22 | final ReplicaConsistency consistency = new LsnReplicaConsistency();
23 | consistency.write(getConnection("16/3002D50"));
24 |
25 | final boolean isConsistent = consistency.isConsistent(getConnectionSupplier("16/3002D50"));
26 |
27 | assertThat(isConsistent).isTrue();
28 | }
29 |
30 | @Test
31 | public void shouldNotBeConsistentAfterFailedWrite() throws SQLException {
32 | final ReplicaConsistency consistency = new LsnReplicaConsistency();
33 | final Connection main = mock(Connection.class);
34 | when(main.prepareStatement(anyString())).thenThrow(new SQLException("Main connection fails"));
35 |
36 | consistency.write(main);
37 |
38 | final boolean isConsistent = consistency.isConsistent(getConnectionSupplier("16/3002D50"));
39 |
40 | assertThat(isConsistent).isFalse();
41 | }
42 |
43 | @Test
44 | public void shouldRecoverAfterFailedWrite() throws SQLException {
45 | final ReplicaConsistency consistency = new LsnReplicaConsistency();
46 | final Connection main = mock(Connection.class);
47 | when(main.prepareStatement(anyString())).thenThrow(new SQLException("Main connection fails"));
48 |
49 | consistency.write(main);
50 | consistency.write(getConnection("16/3002D50"));
51 |
52 | final boolean isConsistent = consistency.isConsistent(getConnectionSupplier("16/3002D50"));
53 |
54 | assertThat(isConsistent).isTrue();
55 | }
56 |
57 | @Test
58 | public void shouldNotBeConsistentBeforeTheFirstWrite() {
59 | final ReplicaConsistency consistency = new LsnReplicaConsistency();
60 | final Supplier replica = mock(ConnectionSupplier.class);
61 |
62 | final boolean isConsistent = consistency.isConsistent(replica);
63 |
64 | assertThat(isConsistent).isFalse();
65 | }
66 |
67 | @Test
68 | public void shouldNotBeConsistentIfReplicaConnectionFails() throws SQLException {
69 | final ReplicaConsistency consistency = new LsnReplicaConsistency();
70 | final Connection mainConnection = getConnection("16/3002D50");
71 | consistency.write(mainConnection);
72 | final Connection replica = mock(Connection.class);
73 | when(replica.prepareStatement(anyString())).thenThrow(new SQLException("Replica connection fails"));
74 |
75 | final boolean isConsistent = consistency.isConsistent(new ConnectionSupplier(replica));
76 |
77 | assertThat(isConsistent).isFalse();
78 | }
79 |
80 | private Supplier getConnectionSupplier(String lsn) throws SQLException {
81 | return new ConnectionSupplier(getConnection(lsn));
82 | }
83 |
84 | private Connection getConnection(String lsn) throws SQLException {
85 | final Connection main = mock(Connection.class);
86 | final PreparedStatement preparedStatement = mock(PreparedStatement.class);
87 | final ResultSet resultSet = mock(ResultSet.class);
88 | when(main.prepareStatement(anyString())).thenReturn(preparedStatement);
89 | when(preparedStatement.executeQuery()).thenReturn(resultSet);
90 | when(resultSet.getString(anyString())).thenReturn(lsn);
91 | return main;
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/PessimisticPropagationConsistencyTest.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.api.PessimisticPropagationConsistency;
4 | import com.atlassian.db.replica.internal.util.ConnectionSupplier;
5 | import com.atlassian.db.replica.spi.ReplicaConsistency;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.api.Test;
8 | import org.threeten.extra.MutableClock;
9 |
10 | import java.sql.Connection;
11 | import java.time.Duration;
12 |
13 | import static org.assertj.core.api.Assertions.assertThat;
14 |
15 | public class PessimisticPropagationConsistencyTest {
16 |
17 | private PessimisticPropagationConsistency.Builder consistencyBuilder;
18 | private MutableClock clock;
19 | private Connection main;
20 | private Connection replica;
21 |
22 | @BeforeEach
23 | public void resetState() {
24 | clock = MutableClock.epochUTC();
25 | consistencyBuilder = new PessimisticPropagationConsistency.Builder()
26 | .measureTime(clock)
27 | .cacheLastWrite(new VolatileCache<>());
28 | main = null;
29 | replica = null;
30 | }
31 |
32 | @Test
33 | public void shouldBeConsistentAfterPropagation() {
34 | ReplicaConsistency consistency = consistencyBuilder
35 | .assumeMaxPropagation(Duration.ofMillis(200))
36 | .build();
37 |
38 | consistency.write(main);
39 | clock.add(Duration.ofMillis(300));
40 | boolean consistent = consistency.isConsistent(new ConnectionSupplier(replica));
41 |
42 | assertThat(consistent).isTrue();
43 | }
44 |
45 | @Test
46 | public void shouldBeInconsistentBeforePropagation() {
47 | ReplicaConsistency consistency = consistencyBuilder
48 | .assumeMaxPropagation(Duration.ofMillis(200))
49 | .build();
50 |
51 | consistency.write(main);
52 | clock.add(Duration.ofMillis(50));
53 | boolean consistent = consistency.isConsistent(new ConnectionSupplier(replica));
54 |
55 | assertThat(consistent).isFalse();
56 | }
57 |
58 | @Test
59 | public void shouldAssumeInconsistencyWhenUnknown() {
60 | ReplicaConsistency consistency = consistencyBuilder
61 | .assumeMaxPropagation(Duration.ofMillis(200))
62 | .build();
63 |
64 | clock.add(Duration.ofMillis(700));
65 | boolean consistent = consistency.isConsistent(new ConnectionSupplier(replica));
66 |
67 | assertThat(consistent).isFalse();
68 | }
69 |
70 | @Test
71 | public void shouldNotAssumeInconsistentForeverWhenUnknown() {
72 | ReplicaConsistency consistency = consistencyBuilder
73 | .assumeMaxPropagation(Duration.ofMillis(200))
74 | .build();
75 |
76 | clock.add(Duration.ofMillis(700));
77 | boolean isConsistent = consistency.isConsistent(new ConnectionSupplier(replica));
78 | clock.add(Duration.ofMillis(700));
79 | boolean isConsistentLater = consistency.isConsistent(new ConnectionSupplier(replica));
80 |
81 | assertThat(isConsistent).isFalse();
82 | assertThat(isConsistentLater).isTrue();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/VolatileCache.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal;
2 |
3 | import com.atlassian.db.replica.spi.*;
4 |
5 | import java.util.*;
6 |
7 | public class VolatileCache implements Cache {
8 |
9 | private volatile T value;
10 |
11 | @Override
12 | public Optional get() {
13 | return Optional.ofNullable(value);
14 | }
15 |
16 | @Override
17 | public void put(T value) {
18 | this.value = value;
19 | }
20 |
21 | @Override
22 | public void reset() {
23 | value = null;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/aurora/AuroraEndpointTest.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.aurora;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | class AuroraEndpointTest {
8 |
9 | @Test
10 | void shouldCreateAuroraEndpointFromReaderEndpoint() {
11 | // when
12 | AuroraEndpoint endpoint = AuroraEndpoint.parse("arch-app-staging-1-001-lr.cm9o6ayveq1a.us-east-9.rds.amazonaws.com");
13 |
14 | // then
15 | assertThat(endpoint.getServerId()).isEqualTo("arch-app-staging-1-001-lr");
16 | assertThat(endpoint.getCluster()).isEqualTo(AuroraCluster.AuroraClusterBuilder.anAuroraCluster("cm9o6ayveq1a").build());
17 | assertThat(endpoint.getDns()).isEqualTo(new RdsDns("us-east-9", "rds.amazonaws.com", null));
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/util/ConnectionSupplier.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.util;
2 |
3 | import java.sql.Connection;
4 | import java.util.function.Supplier;
5 |
6 | public class ConnectionSupplier implements Supplier {
7 | private final Connection connection;
8 |
9 | public ConnectionSupplier(Connection connection) {
10 | this.connection = connection;
11 | }
12 |
13 | @Override
14 | public Connection get() {
15 | return connection;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/internal/util/TestComparables.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.internal.util;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | public class TestComparables {
8 |
9 | @Test
10 | public void shouldReturnMaxValue() {
11 | final Integer max = Comparables.max(1, 2);
12 |
13 | assertThat(max).isEqualTo(2);
14 | }
15 |
16 | @Test
17 | public void shouldReturnEqualValue() {
18 | final Integer max = Comparables.max(5, 5);
19 |
20 | assertThat(max).isEqualTo(5);
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/DualConnectionPerfIT.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it;
2 |
3 | import com.atlassian.db.replica.api.DualConnection;
4 | import com.atlassian.db.replica.api.mocks.NoOpConnectionProvider;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.sql.Connection;
8 | import java.sql.SQLException;
9 | import java.time.Duration;
10 | import java.time.Instant;
11 |
12 | import static com.atlassian.db.replica.api.Queries.LARGE_SQL_QUERY;
13 | import static com.atlassian.db.replica.api.mocks.CircularConsistency.permanentConsistency;
14 | import static org.assertj.core.api.Assertions.assertThat;
15 |
16 | public class DualConnectionPerfIT {
17 |
18 | @Test
19 | public void shouldHaveAcceptableThruput() throws SQLException {
20 | final Connection connection = DualConnection
21 | .builder(
22 | new NoOpConnectionProvider(),
23 | permanentConsistency().build()
24 | ).build();
25 | final int times = 100000000;
26 |
27 | final Duration duration = runBenchmark(connection, times);
28 |
29 | float thruputPerMillis = (float) times / duration.toMillis();
30 | assertThat(thruputPerMillis)
31 | .as("thruput per ms")
32 | .isGreaterThan(2_200);
33 | }
34 |
35 | private Duration runBenchmark(Connection connection, int times) throws SQLException {
36 | final Instant start = Instant.now();
37 | int hashCode = 0;
38 | for (int i = 0; i < times; i++) {
39 | hashCode += connection.prepareStatement(LARGE_SQL_QUERY).executeQuery().hashCode();
40 | }
41 | System.out.println("I really need that number. JIT gods don't kill my code paths. " + hashCode);
42 | return Duration.between(start, Instant.now());
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/PostgresConnectionProvider.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it;
2 |
3 | import com.atlassian.db.replica.spi.ConnectionProvider;
4 | import com.github.dockerjava.api.DockerClient;
5 | import com.github.dockerjava.api.command.CreateContainerResponse;
6 | import com.github.dockerjava.api.model.ExposedPort;
7 | import com.github.dockerjava.api.model.Link;
8 | import com.github.dockerjava.api.model.PortBinding;
9 | import com.github.dockerjava.core.DefaultDockerClientConfig;
10 | import com.github.dockerjava.core.DockerClientImpl;
11 | import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
12 | import com.github.dockerjava.transport.DockerHttpClient;
13 | import com.google.common.collect.ImmutableList;
14 |
15 | import java.sql.Connection;
16 | import java.sql.DriverManager;
17 | import java.time.Duration;
18 | import java.util.Properties;
19 |
20 | public class PostgresConnectionProvider implements ConnectionProvider, AutoCloseable {
21 | final DefaultDockerClientConfig config = DefaultDockerClientConfig
22 | .createDefaultConfigBuilder()
23 | .build();
24 | final DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
25 | .dockerHost(config.getDockerHost())
26 | .sslConfig(config.getSSLConfig())
27 | .build();
28 | final DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient);
29 | boolean isInitialized = false;
30 |
31 | @Override
32 | public boolean isReplicaAvailable() {
33 | return true;
34 | }
35 |
36 | @Override
37 | public Connection getMainConnection() {
38 | initialize();
39 | final String url = "jdbc:postgresql://localhost:5440/jira";
40 | Properties props = new Properties();
41 | props.setProperty("user", "postgres");
42 | props.setProperty("password", "jira");
43 | try {
44 | return DriverManager.getConnection(url, props);
45 | } catch (Exception e) {
46 | throw new RuntimeException(e);
47 | }
48 | }
49 |
50 | @Override
51 | public Connection getReplicaConnection() {
52 | initialize();
53 | final String url = "jdbc:postgresql://localhost:5450/jira";
54 | Properties props = new Properties();
55 | props.setProperty("user", "postgres");
56 | props.setProperty("password", "jira");
57 | try {
58 | return DriverManager.getConnection(url, props);
59 | } catch (Exception e) {
60 | throw new RuntimeException(e);
61 | }
62 | }
63 |
64 | private synchronized void initialize() {
65 | if (isInitialized) {
66 | return;
67 | }
68 | isInitialized = true;
69 | cleanUp();
70 | pullImage();
71 | startMaster();
72 | startReplica();
73 | }
74 |
75 | private void pullImage() {
76 | try {
77 | dockerClient.pullImageCmd("bitnami/postgresql:13").start().awaitCompletion();
78 | } catch (InterruptedException e) {
79 | throw new RuntimeException(e);
80 | }
81 | }
82 |
83 | @Override
84 | public void close() {
85 | cleanUp();
86 | }
87 |
88 | private void startReplica() {
89 | final CreateContainerResponse replicaCreate = dockerClient.createContainerCmd("bitnami/postgresql:13")
90 | .withEnv(
91 | "POSTGRESQL_REPLICATION_MODE=slave",
92 | "POSTGRESQL_MASTER_HOST=master",
93 | "POSTGRESQL_MASTER_PORT_NUMBER=5432",
94 | "POSTGRESQL_REPLICATION_USER=postgres",
95 | "POSTGRESQL_REPLICATION_PASSWORD=jira",
96 | "POSTGRESQL_PASSWORD=jira"
97 | )
98 | .withExposedPorts(ExposedPort.tcp(5432))
99 | .withPortBindings(PortBinding.parse("5450:5432"))
100 | .withName("db-replica-postgresql-replica")
101 | .withLinks(Link.parse("db-replica-postgresql-main:master"))
102 | .exec();
103 | dockerClient
104 | .startContainerCmd(replicaCreate.getId())
105 | .exec();
106 | for (int i = 0; i < 10; i++) {
107 | try {
108 | getReplicaConnection().close();
109 | } catch (Exception e) {
110 | try {
111 | Thread.sleep(Duration.ofSeconds(1).toMillis());
112 | } catch (Exception ex) {
113 | throw new RuntimeException(e);
114 | }
115 | }
116 | }
117 | }
118 |
119 | private void startMaster() {
120 | final CreateContainerResponse masterCreate = dockerClient.createContainerCmd("bitnami/postgresql:13")
121 | .withEnv(
122 | "POSTGRESQL_REPLICATION_MODE=master",
123 | "POSTGRESQL_USERNAME=postgres",
124 | "POSTGRESQL_PASSWORD=jira",
125 | "POSTGRESQL_DATABASE=jira",
126 | "POSTGRESQL_REPLICATION_USER=postgres",
127 | "POSTGRESQL_REPLICATION_PASSWORD=jira"
128 | ).withExposedPorts(ExposedPort.tcp(5432))
129 | .withPortBindings(PortBinding.parse("5440:5432"))
130 | .withName("db-replica-postgresql-main")
131 | .exec();
132 | dockerClient
133 | .startContainerCmd(masterCreate.getId())
134 | .exec();
135 | for (int i = 0; i < 10; i++) {
136 | try {
137 | getMainConnection().close();
138 | } catch (Exception e) {
139 | try {
140 | Thread.sleep(Duration.ofSeconds(1).toMillis());
141 | } catch (Exception ex) {
142 | throw new RuntimeException(e);
143 | }
144 | }
145 | }
146 | }
147 |
148 | private void cleanUp() {
149 | dockerClient
150 | .listContainersCmd()
151 | .withShowAll(true)
152 | .withNameFilter(
153 | ImmutableList.of(
154 | "db-replica-postgresql-main",
155 | "db-replica-postgresql-replica"
156 | )
157 | )
158 | .exec()
159 | .forEach(container -> {
160 | try {
161 | dockerClient.stopContainerCmd(container.getId()).exec();
162 | } catch (Exception e) {
163 | // it's probably already stopped
164 | }
165 | dockerClient.removeContainerCmd(container.getId()).exec();
166 | });
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/ReplicaStatementIT.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it;
2 |
3 | import com.atlassian.db.replica.api.DualConnection;
4 | import com.atlassian.db.replica.internal.LsnReplicaConsistency;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.sql.Connection;
8 | import java.sql.SQLException;
9 | import java.sql.SQLFeatureNotSupportedException;
10 | import java.sql.Statement;
11 |
12 | import static java.sql.ResultSet.CONCUR_UPDATABLE;
13 | import static java.sql.ResultSet.FETCH_REVERSE;
14 | import static java.sql.ResultSet.TYPE_SCROLL_SENSITIVE;
15 | import static java.sql.Statement.RETURN_GENERATED_KEYS;
16 | import static org.assertj.core.api.Assertions.assertThat;
17 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
18 |
19 | public class ReplicaStatementIT {
20 |
21 | @Test
22 | public void shouldImplementAllStatementMethods() throws SQLException {
23 | try (PostgresConnectionProvider connectionProvider = new PostgresConnectionProvider()) {
24 | final Connection connection = DualConnection.builder(
25 | connectionProvider,
26 | new LsnReplicaConsistency()
27 | ).build();
28 | final Statement statement = connection.createStatement();
29 | statement.executeQuery("SELECT 1;");
30 | statement.executeUpdate("CREATE SEQUENCE mysequence START 101;");
31 | statement.setMaxFieldSize(300);
32 | assertThat(statement.getMaxFieldSize()).isEqualTo(300);
33 | statement.setMaxRows(301);
34 | assertThat(statement.getMaxRows()).isEqualTo(301);
35 | statement.setEscapeProcessing(false);
36 | statement.setEscapeProcessing(true);
37 | statement.setQueryTimeout(33);
38 | assertThat(statement.getQueryTimeout()).isEqualTo(33);
39 | statement.getWarnings();
40 | statement.clearWarnings();
41 | statement.setCursorName("alosaf");
42 | statement.execute("SELECT 1;");
43 | statement.getResultSet();
44 | statement.getUpdateCount();
45 | statement.getMoreResults();
46 | statement.setFetchDirection(FETCH_REVERSE);
47 | assertThat(statement.getFetchDirection()).isEqualTo(FETCH_REVERSE);
48 | statement.setFetchSize(1234);
49 | assertThat(statement.getFetchSize()).isEqualTo(1234);
50 | statement.getResultSetConcurrency();
51 | statement.getResultSetType();
52 | statement.addBatch("SELECT 1;");
53 | statement.executeBatch();
54 | statement.clearBatch();
55 | statement.getConnection();
56 | statement.getMoreResults(Statement.KEEP_CURRENT_RESULT);
57 | connection.createStatement(
58 | TYPE_SCROLL_SENSITIVE,
59 | CONCUR_UPDATABLE
60 | ).getGeneratedKeys();
61 | connection.createStatement(
62 | TYPE_SCROLL_SENSITIVE,
63 | CONCUR_UPDATABLE
64 | ).executeUpdate(
65 | "CREATE SEQUENCE mysequence2;",
66 | Statement.NO_GENERATED_KEYS
67 | );
68 | connection.createStatement(
69 | TYPE_SCROLL_SENSITIVE,
70 | CONCUR_UPDATABLE
71 | ).executeUpdate(
72 | "CREATE SEQUENCE mysequence3;",
73 | new int[]{}
74 | );
75 | connection.createStatement(
76 | TYPE_SCROLL_SENSITIVE,
77 | CONCUR_UPDATABLE
78 | ).executeUpdate(
79 | "CREATE SEQUENCE mysequence4;",
80 | new String[]{}
81 | );
82 | connection.createStatement(
83 | TYPE_SCROLL_SENSITIVE,
84 | CONCUR_UPDATABLE
85 | ).execute(
86 | "CREATE SEQUENCE mysequence5;",
87 | RETURN_GENERATED_KEYS
88 | );
89 | connection.createStatement(
90 | TYPE_SCROLL_SENSITIVE,
91 | CONCUR_UPDATABLE
92 | ).execute(
93 | "CREATE SEQUENCE mysequence6;",
94 | new int[]{}
95 | );
96 | connection.createStatement(
97 | TYPE_SCROLL_SENSITIVE,
98 | CONCUR_UPDATABLE
99 | ).execute(
100 | "CREATE SEQUENCE mysequence7;",
101 | new String[]{}
102 | );
103 | statement.getResultSetHoldability();
104 | statement.isClosed();
105 | statement.closeOnCompletion();
106 | statement.isCloseOnCompletion();
107 | statement.setLargeMaxRows(12345);
108 | assertThatThrownBy(statement::getLargeMaxRows).isInstanceOf(SQLFeatureNotSupportedException.class);
109 | assertThatThrownBy(statement::executeLargeBatch).isInstanceOf(SQLFeatureNotSupportedException.class);
110 | assertThatThrownBy(() -> statement.executeLargeUpdate("CREATE SEQUENCE mysequence2;"))
111 | .isInstanceOf(SQLFeatureNotSupportedException.class);
112 | assertThatThrownBy(() -> statement.executeLargeUpdate("CREATE SEQUENCE mysequence2;", new int[]{}))
113 | .isInstanceOf(SQLFeatureNotSupportedException.class);
114 | assertThatThrownBy(() -> statement.executeLargeUpdate("CREATE SEQUENCE mysequence2;", new String[]{}))
115 | .isInstanceOf(SQLFeatureNotSupportedException.class);
116 | assertThatThrownBy(statement::cancel).isInstanceOf(SQLFeatureNotSupportedException.class);
117 | statement.close();
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/consistency/WaitingReplicaConsistency.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.consistency;
2 |
3 | import com.atlassian.db.replica.spi.ReplicaConsistency;
4 |
5 | import java.sql.Connection;
6 | import java.util.function.Supplier;
7 |
8 | public class WaitingReplicaConsistency implements ReplicaConsistency {
9 | private final ReplicaConsistency consistency;
10 |
11 | public WaitingReplicaConsistency(ReplicaConsistency consistency) {
12 | this.consistency = consistency;
13 | }
14 |
15 | @Override
16 | public void write(Connection main) {
17 | consistency.write(main);
18 | }
19 |
20 | @Override
21 | public boolean isConsistent(Supplier replica) {
22 | for (int i = 0; i < 30; i++) {
23 | if (consistency.isConsistent(replica)) {
24 | return true;
25 | }
26 | try {
27 | Thread.sleep(1000);
28 | } catch (InterruptedException e) {
29 | throw new RuntimeException(e);
30 | }
31 | }
32 | throw new RuntimeException("Replica is still inconsistent after 30s.");
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/AuroraClusterTest.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora;
2 |
3 | import com.atlassian.db.replica.api.DualConnection;
4 | import com.atlassian.db.replica.api.SqlCall;
5 | import com.atlassian.db.replica.api.reason.Reason;
6 | import com.atlassian.db.replica.api.reason.RouteDecision;
7 | import com.atlassian.db.replica.it.example.aurora.app.User;
8 | import com.atlassian.db.replica.it.example.aurora.app.Users;
9 | import com.atlassian.db.replica.it.example.aurora.replica.AuroraConnectionProvider;
10 | import com.atlassian.db.replica.it.example.aurora.replica.ConsistencyFactory;
11 | import com.atlassian.db.replica.it.example.aurora.utils.DecisionLog;
12 | import com.atlassian.db.replica.it.example.aurora.utils.ReplicationLag;
13 | import com.atlassian.db.replica.spi.ReplicaConnectionPerUrlProvider;
14 | import com.atlassian.db.replica.spi.ConnectionProvider;
15 | import com.atlassian.db.replica.spi.DatabaseCall;
16 | import com.atlassian.db.replica.internal.DefaultReplicaConnectionPerUrlProvider;
17 | import com.atlassian.db.replica.spi.ReplicaConsistency;
18 | import org.junit.jupiter.api.Disabled;
19 | import org.junit.jupiter.api.Test;
20 |
21 | import java.sql.Connection;
22 | import java.sql.SQLException;
23 | import java.util.Collection;
24 | import java.util.List;
25 | import java.util.stream.Collectors;
26 |
27 | import static com.atlassian.db.replica.api.reason.Reason.READ_OPERATION;
28 | import static com.atlassian.db.replica.api.reason.Reason.REPLICA_INCONSISTENT;
29 | import static org.assertj.core.api.Assertions.assertThat;
30 |
31 | class AuroraClusterTest {
32 | final String databaseName = "newdb";
33 | final String readerEndpoint = "database-1.cluster-ro-crmnlihjxqlm.eu-central-1.rds.amazonaws.com:5432";
34 | final String readerJdbcUrl = "jdbc:postgresql://" + readerEndpoint + "/" + databaseName;
35 | final String writerJdbcUrl = "jdbc:postgresql://database-1.cluster-crmnlihjxqlm.eu-central-1.rds.amazonaws.com:5432" + "/" + databaseName;
36 | final String jdbcUsername = "postgres";
37 | final String jdbcPassword = System.getenv("password");
38 |
39 | @Test
40 | @Disabled
41 | void shouldUtilizeReplicaForReadQueriesForSynchronisedWrites() throws SQLException {
42 | final DecisionLog decisionLog = new DecisionLog();
43 | final SqlCall connectionPool = initializeConnectionPool(decisionLog);
44 | new ReplicationLag(connectionPool).set(10);
45 | final Users users = new Users(connectionPool);
46 | final User newUser = new User();
47 |
48 | users.add(newUser);
49 | final Collection allUsers = users.fetch();
50 |
51 | final List reasons = decisionLog.getDecisions().stream().map(RouteDecision::getReason).collect(
52 | Collectors.toList());
53 | assertThat(allUsers).contains(newUser);
54 | assertThat(decisionLog.getDecisions()).contains(new RouteDecision(
55 | "SELECT username FROM users",
56 | READ_OPERATION,
57 | null
58 | ));
59 |
60 | assertThat(reasons).isNotEmpty().doesNotContain(REPLICA_INCONSISTENT);
61 | }
62 |
63 | private SqlCall initializeConnectionPool(final DatabaseCall decisionLog) throws SQLException {
64 | final ConnectionProvider connectionProvider = new AuroraConnectionProvider(
65 | readerJdbcUrl,
66 | writerJdbcUrl
67 | );
68 |
69 | ReplicaConnectionPerUrlProvider replicaConnectionPerUrlProvider = new DefaultReplicaConnectionPerUrlProvider(
70 | jdbcUsername,
71 | jdbcPassword
72 | );
73 |
74 | final ReplicaConsistency replicaConsistency = new ConsistencyFactory(
75 | connectionProvider,
76 | replicaConnectionPerUrlProvider
77 | ).create();
78 |
79 | return () -> DualConnection.builder(connectionProvider, replicaConsistency)
80 | .databaseCall(decisionLog)
81 | .build();
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/app/User.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.app;
2 |
3 | import java.util.Objects;
4 | import java.util.UUID;
5 |
6 | public class User {
7 | private final String name;
8 |
9 | public User() {
10 | this.name = UUID.randomUUID().toString();
11 | }
12 |
13 | public User(String name) {
14 | this.name = name;
15 | }
16 |
17 | public String getName() {
18 | return name;
19 | }
20 |
21 | @Override
22 | public boolean equals(Object o) {
23 | if (this == o) return true;
24 | if (o == null || getClass() != o.getClass()) return false;
25 | User user = (User) o;
26 | return Objects.equals(name, user.name);
27 | }
28 |
29 | @Override
30 | public int hashCode() {
31 | return Objects.hash(name);
32 | }
33 |
34 | @Override
35 | public String toString() {
36 | return "User{" +
37 | "name='" + name + '\'' +
38 | '}';
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/app/Users.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.app;
2 |
3 | import com.atlassian.db.replica.api.SqlCall;
4 | import com.google.common.collect.ImmutableList;
5 |
6 | import java.sql.Connection;
7 | import java.sql.PreparedStatement;
8 | import java.sql.ResultSet;
9 | import java.sql.SQLException;
10 | import java.util.Collection;
11 |
12 | public class Users {
13 | private final SqlCall connectionSupplier;
14 |
15 | public Users(SqlCall connectionSupplier) throws SQLException {
16 | this.connectionSupplier = connectionSupplier;
17 | initialize();
18 | }
19 |
20 | public void add(User user) throws SQLException {
21 | try (final Connection dualConnection = connectionSupplier.call()) {
22 | insertNewUser(dualConnection, user.getName());
23 | }
24 | }
25 |
26 | public Collection fetch() throws SQLException {
27 | try (final Connection connection = connectionSupplier.call()) {
28 | final ImmutableList.Builder usersBuilder = ImmutableList.builder();
29 | final PreparedStatement preparedStatement = connection.prepareStatement(
30 | "SELECT username FROM users");
31 | final ResultSet resultSet = preparedStatement.executeQuery();
32 | while (resultSet.next()) {
33 | final String username = resultSet.getString(1);
34 | usersBuilder.add(new User(username));
35 | }
36 | return usersBuilder.build();
37 | }
38 | }
39 |
40 | private void initialize() throws SQLException {
41 | try (final Connection connection = connectionSupplier.call()) {
42 | try (final PreparedStatement preparedStatement = connection.prepareStatement(
43 | "CREATE TABLE IF NOT EXISTS users (username VARCHAR ( 50 ));")) {
44 | preparedStatement.executeUpdate();
45 | }
46 | }
47 | }
48 |
49 | private void insertNewUser(Connection writerConnection, String newUesrName) throws SQLException {
50 | try (final PreparedStatement preparedStatement = writerConnection.prepareStatement(
51 | "INSERT INTO users VALUES(?)")) {
52 | preparedStatement.setString(1, newUesrName);
53 | preparedStatement.executeUpdate();
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/replica/AuroraConnectionProvider.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.replica;
2 |
3 | import com.atlassian.db.replica.spi.ConnectionProvider;
4 |
5 | import java.sql.Connection;
6 | import java.sql.DriverManager;
7 | import java.sql.SQLException;
8 |
9 | public final class AuroraConnectionProvider implements ConnectionProvider {
10 | private final String readerUrl;
11 | private final String writerUrl;
12 |
13 | public AuroraConnectionProvider(String readerUrl, String writerUrl) {
14 | this.readerUrl = readerUrl;
15 | this.writerUrl = writerUrl;
16 | }
17 |
18 | @Override
19 | public boolean isReplicaAvailable() {
20 | return true;
21 | }
22 |
23 | @Override
24 | public Connection getMainConnection() throws SQLException {
25 | return getConnection(writerUrl);
26 | }
27 |
28 | @Override
29 | public Connection getReplicaConnection() throws SQLException {
30 | return getConnection(readerUrl);
31 | }
32 |
33 | private Connection getConnection(String url) throws SQLException {
34 | return DriverManager.getConnection(
35 | url,
36 | "postgres",
37 | System.getenv("password")
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/replica/ConsistencyFactory.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.replica;
2 |
3 | import com.atlassian.db.replica.api.AuroraMultiReplicaConsistency;
4 | import com.atlassian.db.replica.it.example.aurora.replica.api.SequenceReplicaConsistency;
5 | import com.atlassian.db.replica.it.example.aurora.replica.api.SynchronousWriteConsistency;
6 | import com.atlassian.db.replica.spi.ConnectionProvider;
7 | import com.atlassian.db.replica.spi.ReplicaConnectionPerUrlProvider;
8 | import com.atlassian.db.replica.spi.ReplicaConsistency;
9 |
10 | import java.sql.Connection;
11 | import java.sql.PreparedStatement;
12 | import java.sql.SQLException;
13 |
14 | public class ConsistencyFactory {
15 | private final ConnectionProvider connectionProvider;
16 | private final ReplicaConnectionPerUrlProvider replicaConnectionPerUrlProvider;
17 |
18 | public ConsistencyFactory(
19 | ConnectionProvider connectionProvider,
20 | ReplicaConnectionPerUrlProvider replicaConnectionPerUrlProvider
21 | ) {
22 | this.connectionProvider = connectionProvider;
23 | this.replicaConnectionPerUrlProvider = replicaConnectionPerUrlProvider;
24 | }
25 |
26 | public ReplicaConsistency create() throws SQLException {
27 | initialize();
28 | final SequenceReplicaConsistency sequenceReplicaConsistency = SequenceReplicaConsistency.builder()
29 | .sequenceName("read_replica_replication")
30 | .build();
31 | final ReplicaConsistency multiReplicaConsistency = AuroraMultiReplicaConsistency.builder()
32 | .replicaConsistency(sequenceReplicaConsistency)
33 | .replicaConnectionPerUrlProvider(replicaConnectionPerUrlProvider)
34 | .build();
35 | return new SynchronousWriteConsistency(multiReplicaConsistency, connectionProvider);
36 | }
37 |
38 | private void initialize() throws SQLException {
39 | try (final Connection connection = connectionProvider.getMainConnection()) {
40 | try (final PreparedStatement preparedStatement = connection.prepareStatement(
41 | "CREATE TABLE IF NOT EXISTS read_replica_replication\n" +
42 | " (\n" +
43 | " id integer PRIMARY KEY,\n" +
44 | " lsn bigint NOT NULL\n" +
45 | " );")) {
46 | preparedStatement.executeUpdate();
47 | }
48 | try (final PreparedStatement preparedStatement = connection.prepareStatement(
49 | " INSERT INTO read_replica_replication (id, lsn)\n" +
50 | " SELECT 1, 1 WHERE 1 NOT IN (SELECT id FROM read_replica_replication);")) {
51 | preparedStatement.executeUpdate();
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/replica/api/AuroraSequence.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.replica.api;
2 |
3 | import java.sql.Connection;
4 | import java.sql.PreparedStatement;
5 | import java.sql.ResultSet;
6 |
7 | import static java.lang.String.format;
8 |
9 | public final class AuroraSequence {
10 | private final String sequenceName;
11 |
12 | public AuroraSequence(String sequenceName) {
13 | this.sequenceName = sequenceName;
14 | }
15 |
16 | public void tryBump(Connection connection) {
17 | try (
18 | PreparedStatement query = prepareBumpSequenceQuery(connection)
19 | ) {
20 | query.executeUpdate();
21 | if (!connection.getAutoCommit()) {
22 | connection.commit();
23 | }
24 | } catch (Exception e) {
25 | throw new RuntimeException(format("Can't bump sequence %s", sequenceName), e);
26 | }
27 | }
28 |
29 | public Long fetch(Connection connection) {
30 | try (PreparedStatement query = prepareFetchSequenceValueQuery(connection)) {
31 | query.setQueryTimeout(1);
32 | try (ResultSet results = query.executeQuery()) {
33 | results.next();
34 | return results.getLong("lsn");
35 | }
36 | } catch (Exception e) {
37 | throw new RuntimeException(format("error occurred during sequence[%s] value fetching", sequenceName), e);
38 | }
39 | }
40 |
41 | private PreparedStatement prepareBumpSequenceQuery(Connection connection) throws Exception {
42 | return connection.prepareStatement("UPDATE " + sequenceName + " SET lsn = lsn + 1 WHERE ID = (SELECT id FROM " + sequenceName + " FOR UPDATE SKIP LOCKED);");
43 | }
44 |
45 | private PreparedStatement prepareFetchSequenceValueQuery(Connection connection) throws Exception {
46 | return connection.prepareStatement("SELECT lsn FROM " + sequenceName + ";");
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/replica/api/SequenceReplicaConsistency.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.replica.api;
2 |
3 | import com.atlassian.db.replica.api.ThrottledCache;
4 | import com.atlassian.db.replica.internal.MonotonicMemoryCache;
5 | import com.atlassian.db.replica.internal.aurora.ReplicaNode;
6 | import com.atlassian.db.replica.spi.Cache;
7 | import com.atlassian.db.replica.spi.ReplicaConsistency;
8 | import com.atlassian.db.replica.spi.SuppliedCache;
9 |
10 | import java.sql.Connection;
11 | import java.time.Clock;
12 | import java.time.Duration;
13 | import java.util.concurrent.ConcurrentHashMap;
14 | import java.util.function.Supplier;
15 |
16 | import static java.time.Duration.ofSeconds;
17 |
18 | @SuppressWarnings({"SqlNoDataSourceInspection", "SqlResolve"})
19 | public class SequenceReplicaConsistency implements ReplicaConsistency {
20 | public static final Duration LSN_CHECK_LOCK_TIMEOUT = ofSeconds(1);
21 |
22 | private final AuroraSequence sequence;
23 | private final Cache lastWrite;
24 | private final boolean unknownWritesFallback;
25 | private final ConcurrentHashMap> multiReplicaLsnCache;
26 | private final ReplicaNode replicaNode;
27 |
28 | SequenceReplicaConsistency(
29 | String sequenceName,
30 | Cache lastWrite,
31 | boolean unknownWritesFallback,
32 | ConcurrentHashMap> multiReplicaLsnCache,
33 | ReplicaNode replicaNode
34 | ) {
35 | this.sequence = new AuroraSequence(sequenceName);
36 | this.lastWrite = lastWrite;
37 | this.unknownWritesFallback = unknownWritesFallback;
38 | this.multiReplicaLsnCache = multiReplicaLsnCache;
39 | this.replicaNode = replicaNode;
40 | }
41 |
42 | public static Builder builder() {
43 | return new Builder();
44 | }
45 |
46 | @Override
47 | public void write(Connection main) {
48 | try {
49 | long sequenceValue = sequence.fetch(main) + 1;
50 | lastWrite.put(sequenceValue);
51 | sequence.tryBump(main);
52 | } catch (Exception e) {
53 | throw new RuntimeException("Can't update consistency state.", e);
54 | }
55 | }
56 |
57 | @Override
58 | public boolean isConsistent(Supplier replica) {
59 | try {
60 | return lastWrite.get()
61 | .map(lastWrite1 -> computeReplicasConsistency(replica.get(), lastWrite1))
62 | .orElse(unknownWritesFallback);
63 | } catch (Exception e) {
64 | throw new RuntimeException("Exception occurred during consistency checking.", e);
65 | }
66 | }
67 |
68 | private boolean computeReplicasConsistency(Connection replica, long lastWrite) {
69 | tryRefreshLsnCacheForCurrentReplica(replica);
70 | return isConsistent(replica, lastWrite);
71 | }
72 |
73 | private void tryRefreshLsnCacheForCurrentReplica(Connection replica) {
74 | String replicaId = replicaNode.get(replica);
75 | if (replicaId != null) {
76 | multiReplicaLsnCache.computeIfAbsent(
77 | replicaId,
78 | x -> new ThrottledCache<>(Clock.systemUTC(), LSN_CHECK_LOCK_TIMEOUT)
79 | )
80 | .get(() -> sequence.fetch(replica));
81 | }
82 | }
83 |
84 | private boolean isConsistent(Connection replica, long lastWrite) {
85 | final Long lsn = multiReplicaLsnCache.get(replicaNode.get(replica)).get().orElse(0L);
86 | return lsn >= lastWrite;
87 | }
88 |
89 | public static class Builder {
90 | private String sequenceName;
91 | private Cache lastWrite = new MonotonicMemoryCache<>();
92 | private boolean unknownWritesFallback = false;
93 | private ConcurrentHashMap> multiReplicaLsnCache = new ConcurrentHashMap<>();
94 | private ReplicaNode replicaNode = new ReplicaNode();
95 |
96 | public Builder sequenceName(String sequenceName) {
97 | this.sequenceName = sequenceName;
98 | return this;
99 | }
100 |
101 | public Builder lastWrite(Cache lastWrite) {
102 | this.lastWrite = lastWrite;
103 | return this;
104 | }
105 |
106 | public Builder unknownWritesFallback(boolean unknownWritesFallback) {
107 | this.unknownWritesFallback = unknownWritesFallback;
108 | return this;
109 | }
110 |
111 | public Builder multiReplicaLsnCache(ConcurrentHashMap> multiReplicaLsnCache) {
112 | this.multiReplicaLsnCache = multiReplicaLsnCache;
113 | return this;
114 | }
115 |
116 | public Builder replicaNode(ReplicaNode replicaNode) {
117 | this.replicaNode = replicaNode;
118 | return this;
119 | }
120 |
121 | /**
122 | * @deprecated use {@link Builder#sequenceName(String)}{@code .}{@link Builder#build()} instead.
123 | */
124 | @Deprecated
125 | public SequenceReplicaConsistency build(String sequenceName) {
126 | return sequenceName(sequenceName).build();
127 | }
128 |
129 | public SequenceReplicaConsistency build() {
130 | return new SequenceReplicaConsistency(
131 | sequenceName,
132 | lastWrite,
133 | unknownWritesFallback,
134 | multiReplicaLsnCache,
135 | replicaNode
136 | );
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/replica/api/SynchronousWriteConsistency.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.replica.api;
2 |
3 | import com.atlassian.db.replica.internal.LazyReference;
4 | import com.atlassian.db.replica.spi.ConnectionProvider;
5 | import com.atlassian.db.replica.spi.ReplicaConsistency;
6 |
7 | import java.sql.Connection;
8 | import java.time.Duration;
9 | import java.time.Instant;
10 | import java.util.function.Supplier;
11 |
12 | import static java.lang.Math.max;
13 | import static java.lang.Math.min;
14 | import static java.lang.String.format;
15 |
16 | /**
17 | * Waits until writes propagate to replica.
18 | * It doesn't depend on a cross-JVM cache.
19 | */
20 |
21 | public class SynchronousWriteConsistency implements ReplicaConsistency {
22 | private final ReplicaConsistency replicaConsistency;
23 | private final ConnectionProvider connections;
24 |
25 | /**
26 | * @param replicaConsistency checks consistency
27 | * @param connections connects to replica during polling
28 | */
29 | public SynchronousWriteConsistency(
30 | ReplicaConsistency replicaConsistency,
31 | ConnectionProvider connections
32 | ) {
33 | this.replicaConsistency = replicaConsistency;
34 | this.connections = connections;
35 | }
36 |
37 | @Override
38 | public void write(Connection connection) {
39 | replicaConsistency.write(connection);
40 | Waiting waiting = new Waiting(replicaConsistency, connections);
41 | waiting.waitUntilConsistent();
42 | }
43 |
44 | @Override
45 | public boolean isConsistent(Supplier supplier) {
46 | return replicaConsistency.isConsistent(supplier);
47 | }
48 |
49 | public static class Waiting {
50 | private final ReplicaConsistency consistency;
51 | private final ConnectionProvider connections;
52 |
53 | public Waiting(
54 | ReplicaConsistency consistency,
55 | ConnectionProvider connections
56 | ) {
57 | this.consistency = consistency;
58 | this.connections = connections;
59 | }
60 |
61 | public void waitUntilConsistent() {
62 | try {
63 | waitUntilConsistent(getTimeout());
64 | } catch (Exception exception) {
65 | throw new RuntimeException("TODO", exception);
66 | }
67 | }
68 |
69 | private void waitUntilConsistent(Duration timeout) throws Exception {
70 | final Instant end = Instant.now().plus(Duration.ofMillis(adjustTimeout(timeout)));
71 | while (Instant.now().isBefore(end)) {
72 | final boolean isConsistent = checkConsistency();
73 | if (isConsistent) {
74 | return;
75 | }
76 | Thread.sleep(10);
77 | }
78 | throw new RuntimeException(format("Waiting for consistency failed: %s", timeout));
79 | }
80 |
81 | private long adjustTimeout(Duration timeout) {
82 | final Duration maxTimeout = getTimeout();
83 | return max(min(timeout.toMillis(), maxTimeout.toMillis()), 10);
84 | }
85 |
86 | private boolean checkConsistency() {
87 | try (ConnectionSupplier replica = new ConnectionSupplier(connections)) {
88 | return consistency.isConsistent(replica);
89 | } catch (Exception e) {
90 | throw new RuntimeException(format("Checking consistency failed: %s", consistency), e);
91 | }
92 | }
93 |
94 | private Duration getTimeout() {
95 | return Duration.ofSeconds(30);
96 | }
97 |
98 | }
99 |
100 | private static class ConnectionSupplier implements Supplier, AutoCloseable {
101 | private final LazyReference connection;
102 |
103 | private ConnectionSupplier(ConnectionProvider connectionProvider) {
104 | connection = new LazyReference() {
105 | @Override
106 | protected Connection create() throws Exception {
107 | return connectionProvider.getReplicaConnection();
108 | }
109 | };
110 | }
111 |
112 | @Override
113 | public void close() throws Exception {
114 | if (connection.isInitialized()) {
115 | final Connection connection = this.connection.get();
116 | if (connection != null) {
117 | connection.close();
118 | }
119 | }
120 | }
121 |
122 | @Override
123 | public Connection get() {
124 | return connection.get();
125 | }
126 | }
127 |
128 | }
129 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/utils/DecisionLog.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.utils;
2 |
3 | import com.atlassian.db.replica.api.SqlCall;
4 | import com.atlassian.db.replica.api.reason.RouteDecision;
5 | import com.atlassian.db.replica.spi.DatabaseCall;
6 | import com.google.common.collect.ImmutableList;
7 |
8 | import java.sql.SQLException;
9 | import java.util.List;
10 |
11 | public final class DecisionLog implements DatabaseCall {
12 | private final ImmutableList.Builder decisions = new ImmutableList.Builder<>();
13 |
14 | @Override
15 | public T call(SqlCall call, RouteDecision decision) throws SQLException {
16 | decisions.add(decision);
17 | return call.call();
18 | }
19 |
20 | public List getDecisions() {
21 | return decisions.build();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/java/com/atlassian/db/replica/it/example/aurora/utils/ReplicationLag.java:
--------------------------------------------------------------------------------
1 | package com.atlassian.db.replica.it.example.aurora.utils;
2 |
3 | import com.atlassian.db.replica.api.SqlCall;
4 |
5 | import java.sql.Connection;
6 | import java.sql.PreparedStatement;
7 | import java.sql.SQLException;
8 |
9 | public final class ReplicationLag {
10 | private final SqlCall connectionSupplier;
11 |
12 | public ReplicationLag(SqlCall connectionSupplier) {
13 | this.connectionSupplier = connectionSupplier;
14 | }
15 |
16 | public void set(int seconds) throws SQLException {
17 | try (final Connection connection = connectionSupplier.call()) {
18 | try (final PreparedStatement preparedStatement = connection.prepareStatement(
19 | "SELECT aurora_inject_replica_failure(100, " + seconds + ", '');")) {
20 | preparedStatement.executeQuery();
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------