fix(oceanbase): allow queries in read-only transactions
This commit is contained in:
parent
1303466947
commit
caa53596d9
|
|
@ -21,8 +21,11 @@ import java.sql.Connection;
|
|||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
|
@ -77,7 +80,14 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
|||
// Connector/J's Statement timeout does not update OceanBase's stricter
|
||||
// session variable, so synchronize both limits before every execution.
|
||||
try (var stmt = connection.createStatement()) {
|
||||
stmt.execute(queryTimeoutSql(timeoutSecs));
|
||||
try {
|
||||
stmt.execute(queryTimeoutSql(timeoutSecs));
|
||||
} catch (SQLException error) {
|
||||
if (isReadOnlyTransactionError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
queryTimeoutChanged = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +123,41 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
|||
return "ALTER SESSION SET ob_query_timeout = " + timeoutSecs * MICROS_PER_SECOND;
|
||||
}
|
||||
|
||||
private static boolean isReadOnlyTransactionError(SQLException error) {
|
||||
Deque<Throwable> pending = new ArrayDeque<>();
|
||||
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
pending.add(error);
|
||||
while (!pending.isEmpty()) {
|
||||
Throwable current = pending.removeFirst();
|
||||
if (!seen.add(current)) {
|
||||
continue;
|
||||
}
|
||||
if (current instanceof SQLException) {
|
||||
SQLException sqlError = (SQLException) current;
|
||||
String message = sqlError.getMessage();
|
||||
if ("25006".equals(sqlError.getSQLState())
|
||||
|| sqlError.getErrorCode() == 1456
|
||||
|| message != null && containsReadOnlyTransactionCode(message)) {
|
||||
return true;
|
||||
}
|
||||
SQLException next = sqlError.getNextException();
|
||||
if (next != null) {
|
||||
pending.addLast(next);
|
||||
}
|
||||
}
|
||||
Throwable cause = current.getCause();
|
||||
if (cause != null) {
|
||||
pending.addLast(cause);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean containsReadOnlyTransactionCode(String message) {
|
||||
String normalized = message.toUpperCase(Locale.ROOT);
|
||||
return normalized.contains("OBE-01456") || normalized.contains("ORA-01456");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DatabaseInfo> listDatabases() {
|
||||
return unchecked(() -> {
|
||||
|
|
|
|||
|
|
@ -105,12 +105,50 @@ class OceanBaseOracleAgentTest {
|
|||
@Test
|
||||
void synchronizesSessionTimeoutForEveryQueryEntryPoint() {
|
||||
List<String> sql = new ArrayList<>();
|
||||
List<Integer> queryTimeouts = new ArrayList<>();
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
TestSupport.setPrivateConnection(agent, executionConnection(sql));
|
||||
Connection connection = executionConnection(sql, queryTimeouts, List.of());
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
|
||||
agent.executeQuery("SELECT 1 FROM DUAL", null, new ExecuteQueryOptions(10, null, 12));
|
||||
agent.executeQueryPage("SELECT 2 FROM DUAL", null, new QueryPageOptions(10, null, 10, 13));
|
||||
agent.startTableRead("SELECT 3 FROM DUAL", null, new QueryPageOptions(10, null, 10, 14));
|
||||
Assertions.assertDoesNotThrow(() -> agent.beforePooledConnectionReturn(connection));
|
||||
|
||||
Assertions.assertEquals(List.of(
|
||||
"ALTER SESSION SET ob_query_timeout = 12000000",
|
||||
"SELECT 1 FROM DUAL",
|
||||
"ALTER SESSION SET ob_query_timeout = 13000000",
|
||||
"SELECT 2 FROM DUAL",
|
||||
"ALTER SESSION SET ob_query_timeout = 14000000",
|
||||
"SELECT 3 FROM DUAL",
|
||||
"ALTER SESSION SET ob_query_timeout = 0"
|
||||
), sql);
|
||||
Assertions.assertEquals(List.of(12, 13, 14), queryTimeouts);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executesEveryQueryEntryPointWhenSessionTimeoutIsRejectedAsReadOnly() {
|
||||
SQLException sqlStateError = new SQLException("wrapped");
|
||||
sqlStateError.setNextException(new SQLException("read only", "25006"));
|
||||
SQLException vendorError = new SQLException("wrapped", new SQLException("read only", null, 1456));
|
||||
SQLException messageError = new SQLException("wrapped", new SQLException(
|
||||
"(conn=1) OBE-01456: may not perform insert/delete/update operation inside a READ ONLY transaction"
|
||||
));
|
||||
List<String> sql = new ArrayList<>();
|
||||
List<Integer> queryTimeouts = new ArrayList<>();
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
Connection connection = executionConnection(
|
||||
sql,
|
||||
queryTimeouts,
|
||||
List.of(sqlStateError, vendorError, messageError)
|
||||
);
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
|
||||
agent.executeQuery("SELECT 1 FROM DUAL", null, new ExecuteQueryOptions(10, null, 12));
|
||||
agent.executeQueryPage("SELECT 2 FROM DUAL", null, new QueryPageOptions(10, null, 10, 13));
|
||||
agent.startTableRead("SELECT 3 FROM DUAL", null, new QueryPageOptions(10, null, 10, 14));
|
||||
Assertions.assertDoesNotThrow(() -> agent.beforePooledConnectionReturn(connection));
|
||||
|
||||
Assertions.assertEquals(List.of(
|
||||
"ALTER SESSION SET ob_query_timeout = 12000000",
|
||||
|
|
@ -120,6 +158,25 @@ class OceanBaseOracleAgentTest {
|
|||
"ALTER SESSION SET ob_query_timeout = 14000000",
|
||||
"SELECT 3 FROM DUAL"
|
||||
), sql);
|
||||
Assertions.assertEquals(List.of(12, 13, 14), queryTimeouts);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnrelatedSessionTimeoutErrorsBeforeExecutingQuery() {
|
||||
SQLException alterError = new SQLException("insufficient privileges", "42000", 1031);
|
||||
List<String> sql = new ArrayList<>();
|
||||
List<Integer> queryTimeouts = new ArrayList<>();
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
TestSupport.setPrivateConnection(agent, executionConnection(sql, queryTimeouts, List.of(alterError)));
|
||||
|
||||
RuntimeException error = Assertions.assertThrows(
|
||||
RuntimeException.class,
|
||||
() -> agent.executeQuery("SELECT 1 FROM DUAL", null, new ExecuteQueryOptions(10, null, 12))
|
||||
);
|
||||
|
||||
Assertions.assertSame(alterError, error.getCause());
|
||||
Assertions.assertEquals(List.of("ALTER SESSION SET ob_query_timeout = 12000000"), sql);
|
||||
Assertions.assertTrue(queryTimeouts.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -418,16 +475,33 @@ class OceanBaseOracleAgentTest {
|
|||
}
|
||||
|
||||
private static Connection executionConnection(List<String> sql) {
|
||||
return executionConnection(sql, new ArrayList<>(), List.of());
|
||||
}
|
||||
|
||||
private static Connection executionConnection(
|
||||
List<String> sql,
|
||||
List<Integer> queryTimeouts,
|
||||
List<SQLException> alterFailures
|
||||
) {
|
||||
int[] alterFailureIndex = {0};
|
||||
Statement statement = proxy(Statement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
sql.add(String.valueOf(args[0]));
|
||||
String statementSql = String.valueOf(args[0]);
|
||||
sql.add(statementSql);
|
||||
if (statementSql.startsWith("ALTER SESSION") && alterFailureIndex[0] < alterFailures.size()) {
|
||||
throw alterFailures.get(alterFailureIndex[0]++);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if ("getUpdateCount".equals(method.getName())) {
|
||||
return 0;
|
||||
}
|
||||
if ("setQueryTimeout".equals(method.getName())) {
|
||||
queryTimeouts.add(((Number) args[0]).intValue());
|
||||
return null;
|
||||
}
|
||||
if ("close".equals(method.getName()) || "setMaxRows".equals(method.getName())
|
||||
|| "setFetchSize".equals(method.getName()) || "setQueryTimeout".equals(method.getName())) {
|
||||
|| "setFetchSize".equals(method.getName())) {
|
||||
return null;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
|
|
|
|||
Loading…
Reference in New Issue