Skip to content

Commit d147c41

Browse files
authored
feat: failover read only connections (#1609)
1 parent 5696a56 commit d147c41

File tree

10 files changed

+170
-4
lines changed

10 files changed

+170
-4
lines changed

wrapper/src/main/java/software/amazon/jdbc/PartialPluginService.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,22 @@ public boolean isLoginException(final String sqlState) {
562562
return this.exceptionManager.isLoginException(this.dbDialect, sqlState);
563563
}
564564

565+
@Override
566+
public boolean isReadOnlyConnectionException(@Nullable String sqlState, @Nullable Integer errorCode) {
567+
if (this.exceptionHandler != null) {
568+
return this.exceptionHandler.isReadOnlyConnectionException(sqlState, errorCode);
569+
}
570+
return this.exceptionManager.isReadOnlyConnectionException(this.dbDialect, sqlState, errorCode);
571+
}
572+
573+
@Override
574+
public boolean isReadOnlyConnectionException(Throwable throwable, @Nullable TargetDriverDialect targetDriverDialect) {
575+
if (this.exceptionHandler != null) {
576+
return this.exceptionHandler.isReadOnlyConnectionException(throwable, targetDriverDialect);
577+
}
578+
return this.exceptionManager.isReadOnlyConnectionException(this.dbDialect, throwable, targetDriverDialect);
579+
}
580+
565581
@Override
566582
public Dialect getDialect() {
567583
return this.dbDialect;

wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,22 @@ public boolean isLoginException(final String sqlState) {
690690
return this.exceptionManager.isLoginException(this.dialect, sqlState);
691691
}
692692

693+
@Override
694+
public boolean isReadOnlyConnectionException(@Nullable String sqlState, @Nullable Integer errorCode) {
695+
if (this.exceptionHandler != null) {
696+
return this.exceptionHandler.isReadOnlyConnectionException(sqlState, errorCode);
697+
}
698+
return this.exceptionManager.isReadOnlyConnectionException(this.dialect, sqlState, errorCode);
699+
}
700+
701+
@Override
702+
public boolean isReadOnlyConnectionException(Throwable throwable, @Nullable TargetDriverDialect targetDriverDialect) {
703+
if (this.exceptionHandler != null) {
704+
return this.exceptionHandler.isReadOnlyConnectionException(throwable, targetDriverDialect);
705+
}
706+
return this.exceptionManager.isReadOnlyConnectionException(this.dialect, throwable, targetDriverDialect);
707+
}
708+
693709
@Override
694710
public Dialect getDialect() {
695711
return this.dialect;

wrapper/src/main/java/software/amazon/jdbc/exceptions/AbstractPgExceptionHandler.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import software.amazon.jdbc.util.StringUtils;
2424

2525
public abstract class AbstractPgExceptionHandler implements ExceptionHandler {
26+
27+
protected static final String READ_ONLY_CONNECTION_SQLSTATE = "25006";
28+
2629
public abstract List<String> getNetworkErrors();
2730

2831
public abstract List<String> getAccessErrors();
@@ -95,4 +98,36 @@ public boolean isLoginException(final String sqlState) {
9598
}
9699
return getAccessErrors().contains(sqlState);
97100
}
101+
102+
@Override
103+
public boolean isReadOnlyConnectionException(
104+
final @Nullable String sqlState, final @Nullable Integer errorCode) {
105+
return READ_ONLY_CONNECTION_SQLSTATE.equals(sqlState);
106+
}
107+
108+
@Override
109+
public boolean isReadOnlyConnectionException(
110+
final Throwable throwable, @Nullable TargetDriverDialect targetDriverDialect) {
111+
112+
Throwable exception = throwable;
113+
114+
while (exception != null) {
115+
String sqlState = null;
116+
Integer errorCode = null;
117+
if (exception instanceof SQLException) {
118+
sqlState = ((SQLException) exception).getSQLState();
119+
errorCode = ((SQLException) exception).getErrorCode();
120+
} else if (targetDriverDialect != null) {
121+
sqlState = targetDriverDialect.getSQLState(exception);
122+
}
123+
124+
if (isReadOnlyConnectionException(sqlState, errorCode)) {
125+
return true;
126+
}
127+
128+
exception = exception.getCause();
129+
}
130+
131+
return false;
132+
}
98133
}

wrapper/src/main/java/software/amazon/jdbc/exceptions/ExceptionHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ public interface ExceptionHandler {
2828
boolean isLoginException(String sqlState);
2929

3030
boolean isLoginException(Throwable throwable, @Nullable TargetDriverDialect targetDriverDialect);
31+
32+
boolean isReadOnlyConnectionException(final @Nullable String sqlState, final @Nullable Integer errorCode);
33+
34+
boolean isReadOnlyConnectionException(Throwable throwable, @Nullable TargetDriverDialect targetDriverDialect);
3135
}

wrapper/src/main/java/software/amazon/jdbc/exceptions/ExceptionManager.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package software.amazon.jdbc.exceptions;
1818

19+
import org.checkerframework.checker.nullness.qual.Nullable;
1920
import software.amazon.jdbc.Driver;
2021
import software.amazon.jdbc.dialect.Dialect;
2122
import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect;
@@ -44,6 +45,18 @@ public boolean isNetworkException(final Dialect dialect, final String sqlState)
4445
return handler.isNetworkException(sqlState);
4546
}
4647

48+
public boolean isReadOnlyConnectionException(
49+
final Dialect dialect, final Throwable throwable, final TargetDriverDialect targetDriverDialect) {
50+
final ExceptionHandler handler = getHandler(dialect);
51+
return handler.isReadOnlyConnectionException(throwable, targetDriverDialect);
52+
}
53+
54+
public boolean isReadOnlyConnectionException(
55+
final Dialect dialect, final @Nullable String sqlState, final @Nullable Integer errorCode) {
56+
final ExceptionHandler handler = getHandler(dialect);
57+
return handler.isReadOnlyConnectionException(sqlState, errorCode);
58+
}
59+
4760
private ExceptionHandler getHandler(final Dialect dialect) {
4861
final ExceptionHandler customHandler = Driver.getCustomExceptionHandler();
4962
return customHandler != null ? customHandler : dialect.getExceptionHandler();

wrapper/src/main/java/software/amazon/jdbc/exceptions/GenericExceptionHandler.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public boolean isLoginException(final Throwable throwable, TargetDriverDialect t
8686
if (exception instanceof SQLException) {
8787
sqlState = ((SQLException) exception).getSQLState();
8888
} else if (targetDriverDialect != null) {
89-
sqlState = targetDriverDialect.getSQLState(throwable);
89+
sqlState = targetDriverDialect.getSQLState(exception);
9090
}
9191

9292
if (isLoginException(sqlState)) {
@@ -103,4 +103,15 @@ public boolean isLoginException(final Throwable throwable, TargetDriverDialect t
103103
public boolean isLoginException(final String sqlState) {
104104
return ACCESS_ERRORS.contains(sqlState);
105105
}
106+
107+
@Override
108+
public boolean isReadOnlyConnectionException(@Nullable String sqlState, @Nullable Integer errorCode) {
109+
return false;
110+
}
111+
112+
@Override
113+
public boolean isReadOnlyConnectionException(
114+
Throwable throwable, @Nullable TargetDriverDialect targetDriverDialect) {
115+
return false;
116+
}
106117
}

wrapper/src/main/java/software/amazon/jdbc/exceptions/MySQLExceptionHandler.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
package software.amazon.jdbc.exceptions;
1818

1919
import java.sql.SQLException;
20+
import java.util.Arrays;
21+
import java.util.HashSet;
22+
import java.util.Set;
2023
import org.checkerframework.checker.nullness.qual.Nullable;
2124
import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect;
2225
import software.amazon.jdbc.util.StringUtils;
@@ -27,6 +30,11 @@ public class MySQLExceptionHandler implements ExceptionHandler {
2730
public static final String SET_NETWORK_TIMEOUT_ON_CLOSED_CONNECTION =
2831
"setNetworkTimeout cannot be called on a closed connection";
2932

33+
private static final Set<Integer> SQLSTATE_READ_ONLY_CONNECTION = new HashSet<>(Arrays.asList(
34+
1290, // The MySQL server is running with the --read-only option, so it cannot execute this statement
35+
1836 // Running in read-only mode
36+
));
37+
3038
@Override
3139
public boolean isNetworkException(final Throwable throwable, @Nullable TargetDriverDialect targetDriverDialect) {
3240
Throwable exception = throwable;
@@ -81,7 +89,7 @@ public boolean isLoginException(final Throwable throwable, @Nullable TargetDrive
8189
if (exception instanceof SQLException) {
8290
sqlState = ((SQLException) exception).getSQLState();
8391
} else if (targetDriverDialect != null) {
84-
sqlState = targetDriverDialect.getSQLState(throwable);
92+
sqlState = targetDriverDialect.getSQLState(exception);
8593
}
8694

8795
if (isLoginException(sqlState)) {
@@ -103,6 +111,39 @@ public boolean isLoginException(final String sqlState) {
103111
return SQLSTATE_ACCESS_ERROR.equals(sqlState);
104112
}
105113

114+
@Override
115+
public boolean isReadOnlyConnectionException(
116+
final @Nullable String sqlState, final @Nullable Integer errorCode) {
117+
// HY000 - generic SQL state; use error code for more specific information
118+
return "HY000".equals(sqlState) && errorCode != null && (SQLSTATE_READ_ONLY_CONNECTION.contains(errorCode));
119+
}
120+
121+
@Override
122+
public boolean isReadOnlyConnectionException(
123+
final Throwable throwable, @Nullable TargetDriverDialect targetDriverDialect) {
124+
125+
Throwable exception = throwable;
126+
127+
while (exception != null) {
128+
String sqlState = null;
129+
Integer errorCode = null;
130+
if (exception instanceof SQLException) {
131+
sqlState = ((SQLException) exception).getSQLState();
132+
errorCode = ((SQLException) exception).getErrorCode();
133+
} else if (targetDriverDialect != null) {
134+
sqlState = targetDriverDialect.getSQLState(exception);
135+
}
136+
137+
if (isReadOnlyConnectionException(sqlState, errorCode)) {
138+
return true;
139+
}
140+
141+
exception = exception.getCause();
142+
}
143+
144+
return false;
145+
}
146+
106147
private boolean isHikariMariaDbNetworkException(final SQLException sqlException) {
107148
return sqlException.getSQLState().equals(SQLSTATE_SYNTAX_ERROR_OR_ACCESS_VIOLATION)
108149
&& sqlException.getMessage().contains(SET_NETWORK_TIMEOUT_ON_CLOSED_CONNECTION);

wrapper/src/main/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPlugin.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,14 @@ protected boolean shouldExceptionTriggerConnectionSwitch(final Throwable t) {
894894
return false;
895895
}
896896

897-
return this.pluginService.isNetworkException(t, this.pluginService.getTargetDriverDialect());
897+
if (this.pluginService.isNetworkException(t, this.pluginService.getTargetDriverDialect())) {
898+
return true;
899+
}
900+
901+
// For STRICT_WRITER failover mode when connection exception indicate that the connection's in read-only mode,
902+
// initiate a failover by returning true.
903+
return this.failoverMode == FailoverMode.STRICT_WRITER
904+
&& this.pluginService.isReadOnlyConnectionException(t, this.pluginService.getTargetDriverDialect());
898905
}
899906

900907
/**

wrapper/src/main/java/software/amazon/jdbc/plugin/failover2/FailoverConnectionPlugin.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,14 @@ protected boolean shouldExceptionTriggerConnectionSwitch(final Throwable t) {
702702
return false;
703703
}
704704

705-
return this.pluginService.isNetworkException(t, this.pluginService.getTargetDriverDialect());
705+
if (this.pluginService.isNetworkException(t, this.pluginService.getTargetDriverDialect())) {
706+
return true;
707+
}
708+
709+
// For STRICT_WRITER failover mode when connection exception indicate that the connection's in read-only mode,
710+
// initiate a failover by returning true.
711+
return this.failoverMode == FailoverMode.STRICT_WRITER
712+
&& this.pluginService.isReadOnlyConnectionException(t, this.pluginService.getTargetDriverDialect());
706713
}
707714

708715
/**

wrapper/src/test/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPluginTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818

1919
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
2020
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertFalse;
2122
import static org.junit.jupiter.api.Assertions.assertThrows;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
2224
import static org.mockito.ArgumentMatchers.any;
2325
import static org.mockito.ArgumentMatchers.anyString;
2426
import static org.mockito.ArgumentMatchers.eq;
2527
import static org.mockito.Mockito.atLeastOnce;
2628
import static org.mockito.Mockito.doNothing;
29+
import static org.mockito.Mockito.doReturn;
2730
import static org.mockito.Mockito.doThrow;
2831
import static org.mockito.Mockito.never;
2932
import static org.mockito.Mockito.spy;
@@ -441,4 +444,17 @@ private void initializePlugin() throws SQLException {
441444
spyPlugin.setReaderFailoverHandler(mockReaderFailoverHandler);
442445
// doReturn(mockConnectionService).when(spyPlugin).getConnectionService();
443446
}
447+
448+
@Test
449+
void test_failover_when_read_only_connection() throws SQLException {
450+
initializePlugin();
451+
spyPlugin.failoverMode = FailoverMode.STRICT_WRITER;
452+
453+
when(mockPluginService.isReadOnlyConnectionException(any(), any(TargetDriverDialect.class))).thenReturn(true);
454+
assertTrue(spyPlugin.shouldExceptionTriggerConnectionSwitch(new SQLException("test", "any")));
455+
456+
when(mockPluginService.isReadOnlyConnectionException(any(), any(TargetDriverDialect.class))).thenReturn(false);
457+
assertFalse(spyPlugin.shouldExceptionTriggerConnectionSwitch(new SQLException("test", "any")));
458+
}
459+
444460
}

0 commit comments

Comments
 (0)