Skip to content

Commit 04a28c3

Browse files
fix(Spanner): Added read-only validation for Mysql shards.
This does not validate the user authority.
1 parent d52d425 commit 04a28c3

2 files changed

Lines changed: 115 additions & 1 deletion

File tree

v2/spanner-to-sourcedb/src/main/java/com/google/cloud/teleport/v2/templates/dbutils/connection/JdbcConnectionHelper.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import java.io.IOException;
2424
import java.io.StringReader;
2525
import java.sql.Connection;
26+
import java.sql.ResultSet;
27+
import java.sql.SQLException;
28+
import java.sql.Statement;
2629
import java.util.HashMap;
2730
import java.util.Map;
2831
import java.util.Properties;
@@ -77,10 +80,24 @@ public synchronized void init(ConnectionHelperRequest connectionHelperRequest) {
7780
config.addDataSourceProperty(key, value);
7881
}
7982
HikariDataSource ds = new HikariDataSource(config);
83+
validateMysqlHikariConnection(ds);
8084
connectionPoolMap.put(sourceConnectionUrl + "/" + shard.getUserName(), ds);
8185
}
8286
}
8387

88+
private void validateMysqlHikariConnection(HikariDataSource ds) {
89+
try (Statement stmt = ds.getConnection().createStatement();
90+
ResultSet rs = stmt.executeQuery("SELECT @@global.read_only")) {
91+
92+
if (rs.next() && rs.getBoolean(1)) {
93+
// Node is read-only. Close connection and throw exception.
94+
throw new SQLException("Connection rejected: MySQL Shard is in read-only mode.");
95+
}
96+
} catch (SQLException e) {
97+
throw new RuntimeException(e);
98+
}
99+
}
100+
84101
@Override
85102
public Connection getConnection(String connectionRequestKey) throws ConnectionException {
86103
try {

v2/spanner-to-sourcedb/src/test/java/com/google/cloud/teleport/v2/templates/dbutils/connection/JdbcConnectionHelperTest.java

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.Assert.assertFalse;
2121
import static org.junit.Assert.assertNull;
2222
import static org.junit.Assert.assertTrue;
23+
import static org.junit.Assert.fail;
2324
import static org.mockito.Mockito.mock;
2425
import static org.mockito.Mockito.mockConstruction;
2526
import static org.mockito.Mockito.verify;
@@ -31,6 +32,9 @@
3132
import com.zaxxer.hikari.HikariConfig;
3233
import com.zaxxer.hikari.HikariDataSource;
3334
import java.sql.Connection;
35+
import java.sql.ResultSet;
36+
import java.sql.SQLException;
37+
import java.sql.Statement;
3438
import java.util.Collections;
3539
import java.util.List;
3640
import java.util.Map;
@@ -111,7 +115,17 @@ public void testInitConnectionPool() {
111115
try (MockedConstruction<HikariDataSource> mockedDsConstruction =
112116
mockConstruction(
113117
HikariDataSource.class,
114-
(mock, context) -> when(mock.getConnection()).thenReturn(mock(Connection.class)))) {
118+
(mock, context) -> {
119+
Connection mockConnection = mock(Connection.class);
120+
Statement mockStatement = mock(Statement.class);
121+
ResultSet mockResultSet = mock(ResultSet.class);
122+
when(mockResultSet.next()).thenReturn(true);
123+
when(mockResultSet.getBoolean(1)).thenReturn(false); // Not read-only
124+
when(mockStatement.executeQuery("SELECT @@global.read_only"))
125+
.thenReturn(mockResultSet);
126+
when(mockConnection.createStatement()).thenReturn(mockStatement);
127+
when(mock.getConnection()).thenReturn(mockConnection);
128+
})) {
115129
try (MockedConstruction<HikariConfig> mockedConfigConstruction =
116130
mockConstruction(HikariConfig.class)) {
117131
connectionHelper.init(mockRequest);
@@ -135,4 +149,87 @@ public void testInitConnectionPool() {
135149
}
136150
}
137151
}
152+
153+
@Test
154+
public void testInit_readOnlyShard_throwsException() throws SQLException {
155+
ConnectionHelperRequest mockRequest = mock(ConnectionHelperRequest.class);
156+
Shard mockShard = mock(Shard.class);
157+
when(mockShard.getHost()).thenReturn("localhost");
158+
when(mockShard.getPort()).thenReturn("3306");
159+
when(mockShard.getDbName()).thenReturn("testdb");
160+
when(mockShard.getUserName()).thenReturn("testuser");
161+
when(mockShard.getPassword()).thenReturn("testpassword");
162+
when(mockRequest.getDriver()).thenReturn("com.mysql.cj.jdbc.Driver");
163+
when(mockRequest.getMaxConnections()).thenReturn(1);
164+
165+
List<Shard> mockShards = Collections.singletonList(mockShard);
166+
when(mockRequest.getShards()).thenReturn(mockShards);
167+
168+
// Mock the JDBC objects to simulate a read-only database
169+
ResultSet mockResultSet = mock(ResultSet.class);
170+
when(mockResultSet.next()).thenReturn(true);
171+
when(mockResultSet.getBoolean(1)).thenReturn(true); // Read-only is true
172+
173+
Statement mockStatement = mock(Statement.class);
174+
when(mockStatement.executeQuery("SELECT @@global.read_only")).thenReturn(mockResultSet);
175+
176+
Connection mockConnection = mock(Connection.class);
177+
when(mockConnection.createStatement()).thenReturn(mockStatement);
178+
179+
try (MockedConstruction<HikariDataSource> mockedDsConstruction =
180+
mockConstruction(
181+
HikariDataSource.class,
182+
(mock, context) -> {
183+
when(mock.getConnection()).thenReturn(mockConnection);
184+
})) {
185+
186+
try {
187+
connectionHelper.init(mockRequest);
188+
fail("Expected RuntimeException was not thrown");
189+
} catch (RuntimeException e) {
190+
assertTrue(e.getCause() instanceof SQLException);
191+
assertEquals(
192+
"Connection rejected: MySQL Shard is in read-only mode.", e.getCause().getMessage());
193+
}
194+
}
195+
}
196+
197+
@Test
198+
public void testInit_writableShard_succeeds() throws SQLException {
199+
ConnectionHelperRequest mockRequest = mock(ConnectionHelperRequest.class);
200+
Shard mockShard = mock(Shard.class);
201+
when(mockShard.getHost()).thenReturn("localhost");
202+
when(mockShard.getPort()).thenReturn("3306");
203+
when(mockShard.getDbName()).thenReturn("testdb");
204+
when(mockShard.getUserName()).thenReturn("testuser");
205+
when(mockShard.getPassword()).thenReturn("testpassword");
206+
207+
List<Shard> mockShards = Collections.singletonList(mockShard);
208+
when(mockRequest.getShards()).thenReturn(mockShards);
209+
210+
// Mock the JDBC objects to simulate a writable database
211+
ResultSet mockResultSet = mock(ResultSet.class);
212+
when(mockResultSet.next()).thenReturn(true);
213+
when(mockResultSet.getBoolean(1)).thenReturn(false); // Read-only is false
214+
215+
Statement mockStatement = mock(Statement.class);
216+
when(mockStatement.executeQuery("SELECT @@global.read_only")).thenReturn(mockResultSet);
217+
218+
Connection mockConnection = mock(Connection.class);
219+
when(mockConnection.createStatement()).thenReturn(mockStatement);
220+
221+
try (MockedConstruction<HikariDataSource> mockedDsConstruction =
222+
mockConstruction(
223+
HikariDataSource.class,
224+
(mock, context) -> {
225+
when(mock.getConnection()).thenReturn(mockConnection);
226+
})) {
227+
try (MockedConstruction<HikariConfig> mockedConfigConstruction =
228+
mockConstruction(HikariConfig.class)) {
229+
// No exception should be thrown
230+
connectionHelper.init(mockRequest);
231+
assertTrue(connectionHelper.isConnectionPoolInitialized());
232+
}
233+
}
234+
}
138235
}

0 commit comments

Comments
 (0)