fix(oceanbase): support manual transaction control in oracle mode
This commit is contained in:
15 files changed
+391
-52
No files matched your search
@@ -255,13 +255,19 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
|
||||
|
||||
@Override
|
||||
public QueryResult executeTransaction(List<String> statements, String schema) {
|
||||
return TransactionExecutor.executeUpdateStatements(
|
||||
requireConnected(),
|
||||
statements,
|
||||
schema,
|
||||
this::setSchemaSQL,
|
||||
this::resetSchemaSQL
|
||||
);
|
||||
Connection conn = requireConnected();
|
||||
return unchecked(() -> {
|
||||
if (!conn.getAutoCommit()) {
|
||||
throw new IllegalStateException("Cannot start a one-shot transaction while a manual transaction is open");
|
||||
}
|
||||
return TransactionExecutor.executeUpdateStatements(
|
||||
conn,
|
||||
statements,
|
||||
schema,
|
||||
this::setSchemaSQL,
|
||||
this::resetSchemaSQL
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -377,7 +383,9 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
|
||||
JdbcExecutor executor,
|
||||
boolean succeeded,
|
||||
boolean requiresSessionAffinity,
|
||||
boolean evictAfterRequest
|
||||
boolean evictAfterRequest,
|
||||
boolean endsSessionAffinity,
|
||||
boolean preservesSchemaContext
|
||||
) {
|
||||
if (poolRegistry == null) {
|
||||
return;
|
||||
@@ -389,7 +397,12 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
|
||||
}
|
||||
if (succeeded && requiresSessionAffinity) {
|
||||
sessionAffinity = true;
|
||||
JdbcSchemaSwitcher.forget(connection);
|
||||
if (!preservesSchemaContext) {
|
||||
JdbcSchemaSwitcher.forget(connection);
|
||||
}
|
||||
}
|
||||
if (succeeded && endsSessionAffinity) {
|
||||
sessionAffinity = false;
|
||||
}
|
||||
if (pooledLease == null) {
|
||||
connection = null;
|
||||
|
||||
@@ -224,13 +224,71 @@ public interface DatabaseAgent {
|
||||
if (conn == null) {
|
||||
throw new IllegalStateException("Not connected");
|
||||
}
|
||||
return TransactionExecutor.executeUpdateStatements(
|
||||
conn,
|
||||
statements,
|
||||
schema,
|
||||
this::setSchemaSQL,
|
||||
this::resetSchemaSQL
|
||||
);
|
||||
return unchecked(() -> {
|
||||
if (!conn.getAutoCommit()) {
|
||||
throw new IllegalStateException("Cannot start a one-shot transaction while a manual transaction is open");
|
||||
}
|
||||
return TransactionExecutor.executeUpdateStatements(
|
||||
conn,
|
||||
statements,
|
||||
schema,
|
||||
this::setSchemaSQL,
|
||||
this::resetSchemaSQL
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Starts an interactive transaction on the current JDBC connection. */
|
||||
default Map<String, Object> beginManualTransaction(String schema) {
|
||||
Connection conn = getConnection();
|
||||
if (conn == null) {
|
||||
throw new IllegalStateException("Not connected");
|
||||
}
|
||||
return unchecked(() -> {
|
||||
if (!conn.getAutoCommit()) {
|
||||
throw new IllegalStateException("Manual transaction already open");
|
||||
}
|
||||
if (schema != null && !schema.trim().isEmpty()) {
|
||||
JdbcSchemaSwitcher.apply(conn, schema, this::setSchemaSQL, this::resetSchemaSQL);
|
||||
}
|
||||
conn.setAutoCommit(false);
|
||||
JdbcSchemaSwitcher.preserve(conn);
|
||||
return Collections.singletonMap("ok", (Object) true);
|
||||
});
|
||||
}
|
||||
|
||||
/** Commits an interactive transaction on the current JDBC connection. */
|
||||
default Map<String, Object> commitManualTransaction() {
|
||||
Connection conn = getConnection();
|
||||
if (conn == null) {
|
||||
throw new IllegalStateException("Not connected");
|
||||
}
|
||||
return unchecked(() -> {
|
||||
if (conn.getAutoCommit()) {
|
||||
throw new IllegalStateException("No manual transaction open");
|
||||
}
|
||||
conn.commit();
|
||||
conn.setAutoCommit(true);
|
||||
JdbcSchemaSwitcher.releasePreserved(conn);
|
||||
return Collections.singletonMap("ok", (Object) true);
|
||||
});
|
||||
}
|
||||
|
||||
/** Rolls back an interactive transaction on the current JDBC connection. */
|
||||
default Map<String, Object> rollbackManualTransaction() {
|
||||
Connection conn = getConnection();
|
||||
if (conn == null) {
|
||||
throw new IllegalStateException("Not connected");
|
||||
}
|
||||
return unchecked(() -> {
|
||||
if (conn.getAutoCommit()) {
|
||||
throw new IllegalStateException("No manual transaction open");
|
||||
}
|
||||
conn.rollback();
|
||||
conn.setAutoCommit(true);
|
||||
JdbcSchemaSwitcher.releasePreserved(conn);
|
||||
return Collections.singletonMap("ok", (Object) true);
|
||||
});
|
||||
}
|
||||
|
||||
default QueryResult executeBatch(List<String> statements, String schema) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import java.util.function.Supplier;
|
||||
final class JdbcSchemaSwitcher {
|
||||
private static final Map<Connection, ResetState> RESET_STATE_BY_CONNECTION =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private static final Map<Connection, Boolean> PRESERVED_BY_CONNECTION =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private JdbcSchemaSwitcher() {
|
||||
}
|
||||
@@ -27,6 +29,9 @@ final class JdbcSchemaSwitcher {
|
||||
Supplier<String> resetSchemaSql
|
||||
) throws Exception {
|
||||
if (schema == null || schema.trim().isEmpty()) {
|
||||
if (isPreserved(conn)) {
|
||||
return;
|
||||
}
|
||||
if (!restore(conn, true)) {
|
||||
throw new SQLException("Failed to restore the original JDBC schema context");
|
||||
}
|
||||
@@ -70,7 +75,20 @@ final class JdbcSchemaSwitcher {
|
||||
RESET_STATE_BY_CONNECTION.remove(conn);
|
||||
}
|
||||
|
||||
static void preserve(Connection conn) {
|
||||
PRESERVED_BY_CONNECTION.put(conn, Boolean.TRUE);
|
||||
}
|
||||
|
||||
static void releasePreserved(Connection conn) {
|
||||
PRESERVED_BY_CONNECTION.remove(conn);
|
||||
}
|
||||
|
||||
private static boolean isPreserved(Connection conn) {
|
||||
return PRESERVED_BY_CONNECTION.containsKey(conn);
|
||||
}
|
||||
|
||||
private static boolean restore(Connection conn, boolean preserveOnFailure) {
|
||||
PRESERVED_BY_CONNECTION.remove(conn);
|
||||
ResetState resetState = RESET_STATE_BY_CONNECTION.remove(conn);
|
||||
if (resetState == null) {
|
||||
return true;
|
||||
|
||||
@@ -111,7 +111,9 @@ public final class JsonRpcServer {
|
||||
jdbcExecutor,
|
||||
succeeded,
|
||||
requiresSessionAffinity(method, params),
|
||||
evictAfterRequest(method)
|
||||
evictAfterRequest(method),
|
||||
endsSessionAffinity(method),
|
||||
preservesSchemaContext()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -315,6 +317,15 @@ public final class JsonRpcServer {
|
||||
List<String> statements = gson.fromJson(params.get("statements"), statementsType);
|
||||
return agent.executeTransaction(statements, stringOrNull(params, "schema"));
|
||||
}
|
||||
if (AgentProtocol.METHOD_BEGIN_MANUAL_TRANSACTION.equals(method)) {
|
||||
return agent.beginManualTransaction(stringOrNull(params, "schema"));
|
||||
}
|
||||
if (AgentProtocol.METHOD_COMMIT_MANUAL_TRANSACTION.equals(method)) {
|
||||
return agent.commitManualTransaction();
|
||||
}
|
||||
if (AgentProtocol.METHOD_ROLLBACK_MANUAL_TRANSACTION.equals(method)) {
|
||||
return agent.rollbackManualTransaction();
|
||||
}
|
||||
if (AgentProtocol.METHOD_EXECUTE_BATCH.equals(method)) {
|
||||
Type statementsType = new TypeToken<List<String>>() {}.getType();
|
||||
List<String> statements = gson.fromJson(params.get("statements"), statementsType);
|
||||
@@ -422,6 +433,9 @@ public final class JsonRpcServer {
|
||||
}
|
||||
|
||||
private boolean requiresSessionAffinity(String method, JsonObject params) {
|
||||
if (AgentProtocol.METHOD_BEGIN_MANUAL_TRANSACTION.equals(method)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (AgentProtocol.METHOD_EXECUTE_QUERY.equals(method)
|
||||
|| AgentProtocol.METHOD_EXECUTE_QUERY_PAGE.equals(method)
|
||||
@@ -447,6 +461,23 @@ public final class JsonRpcServer {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean endsSessionAffinity(String method) {
|
||||
return AgentProtocol.METHOD_COMMIT_MANUAL_TRANSACTION.equals(method)
|
||||
|| AgentProtocol.METHOD_ROLLBACK_MANUAL_TRANSACTION.equals(method);
|
||||
}
|
||||
|
||||
private boolean preservesSchemaContext() {
|
||||
Connection connection = agent.getConnection();
|
||||
if (connection == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return !connection.getAutoCommit();
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean evictAfterRequest(String method) {
|
||||
return AgentProtocol.METHOD_GET_EXPLAIN_INFO.equals(method);
|
||||
}
|
||||
|
||||
@@ -300,6 +300,22 @@ class AbstractJdbcAgentTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOneShotTransactionWhenManualTransactionIsOpen() {
|
||||
TrackingConnection tracking = new TrackingConnection();
|
||||
TestAgent agent = new TestAgent(tracking);
|
||||
agent.connect(new ConnectParams());
|
||||
agent.beginManualTransaction(null);
|
||||
|
||||
IllegalStateException error = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> agent.executeTransaction(Collections.singletonList("UPDATE A SET ID = 1"), null)
|
||||
);
|
||||
|
||||
assertEquals("Cannot start a one-shot transaction while a manual transaction is open", error.getMessage());
|
||||
assertEquals(Collections.singletonList("setAutoCommit:false"), tracking.calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesPlSqlBlockTerminatorDuringTransactionExecution() {
|
||||
TrackingConnection tracking = new TrackingConnection();
|
||||
|
||||
@@ -150,6 +150,42 @@ class CommonJavaCompatibilityTest {
|
||||
assertEquals("Example JDBC", result.getAsJsonObject("databaseInfo").get("driverName").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonRpcServerDispatchesInteractiveTransactionMethods() {
|
||||
List<String> calls = new ArrayList<>();
|
||||
MinimalAgent agent = new MinimalAgent() {
|
||||
@Override
|
||||
public Map<String, Object> beginManualTransaction(String schema) {
|
||||
calls.add("begin:" + schema);
|
||||
return Collections.singletonMap("ok", (Object) true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> commitManualTransaction() {
|
||||
calls.add("commit");
|
||||
return Collections.singletonMap("ok", (Object) true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> rollbackManualTransaction() {
|
||||
calls.add("rollback");
|
||||
return Collections.singletonMap("ok", (Object) true);
|
||||
}
|
||||
};
|
||||
JsonRpcServer server = new JsonRpcServer(agent);
|
||||
|
||||
assertTrue(JsonParser.parseString(server.handleRequest(
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"begin_manual_transaction\",\"params\":{\"schema\":\"APP\"}}"
|
||||
)).getAsJsonObject().has("result"));
|
||||
assertTrue(JsonParser.parseString(server.handleRequest(
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"commit_manual_transaction\",\"params\":{}}"
|
||||
)).getAsJsonObject().has("result"));
|
||||
assertTrue(JsonParser.parseString(server.handleRequest(
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"rollback_manual_transaction\",\"params\":{}}"
|
||||
)).getAsJsonObject().has("result"));
|
||||
assertEquals(List.of("begin:APP", "commit", "rollback"), calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiSessionServerCreatesAndClosesIndependentAgents() throws Exception {
|
||||
java.util.List<TrackingAgent> created = new java.util.ArrayList<>();
|
||||
|
||||
@@ -1869,6 +1869,65 @@ class JdbcConnectionPoolingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void interactiveTransactionPinsConnectionUntilCommit() {
|
||||
AtomicInteger physicalOpens = new AtomicInteger();
|
||||
AtomicInteger requestIds = new AtomicInteger();
|
||||
String url = h2Url("manual_transaction");
|
||||
try (MultiSessionJsonRpcServer server = server(url, physicalOpens, 2)) {
|
||||
openSession(server, requestIds, "transaction");
|
||||
query(server, requestIds, "transaction", "CREATE TABLE ITEMS (ID INT)", null);
|
||||
|
||||
result(request(
|
||||
server,
|
||||
requestIds,
|
||||
AgentProtocol.METHOD_BEGIN_MANUAL_TRANSACTION,
|
||||
sessionParams("transaction")
|
||||
));
|
||||
query(server, requestIds, "transaction", "INSERT INTO ITEMS VALUES (1)", null);
|
||||
|
||||
openSession(server, requestIds, "observer");
|
||||
JsonObject beforeCommit = query(server, requestIds, "observer", "SELECT COUNT(*) FROM ITEMS", null);
|
||||
assertEquals(0, beforeCommit.getAsJsonArray("rows").get(0).getAsJsonArray().get(0).getAsInt());
|
||||
|
||||
result(request(
|
||||
server,
|
||||
requestIds,
|
||||
AgentProtocol.METHOD_COMMIT_MANUAL_TRANSACTION,
|
||||
sessionParams("transaction")
|
||||
));
|
||||
JsonObject afterCommit = query(server, requestIds, "observer", "SELECT COUNT(*) FROM ITEMS", null);
|
||||
assertEquals(1, afterCommit.getAsJsonArray("rows").get(0).getAsJsonArray().get(0).getAsInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void interactiveTransactionRestoresSchemaBeforeReturningConnection() {
|
||||
AtomicInteger physicalOpens = new AtomicInteger();
|
||||
AtomicInteger requestIds = new AtomicInteger();
|
||||
String url = h2Url("manual_transaction_schema");
|
||||
try (MultiSessionJsonRpcServer server = server(url, physicalOpens, 2)) {
|
||||
openSession(server, requestIds, "transaction-schema");
|
||||
query(server, requestIds, "transaction-schema", "CREATE SCHEMA IF NOT EXISTS APP", null);
|
||||
|
||||
JsonObject beginParams = sessionParams("transaction-schema");
|
||||
beginParams.addProperty("schema", "APP");
|
||||
result(request(server, requestIds, AgentProtocol.METHOD_BEGIN_MANUAL_TRANSACTION, beginParams));
|
||||
JsonObject inTransaction = query(server, requestIds, "transaction-schema", "SELECT CURRENT_SCHEMA", null);
|
||||
assertEquals("APP", inTransaction.getAsJsonArray("rows").get(0).getAsJsonArray().get(0).getAsString());
|
||||
|
||||
result(request(
|
||||
server,
|
||||
requestIds,
|
||||
AgentProtocol.METHOD_COMMIT_MANUAL_TRANSACTION,
|
||||
sessionParams("transaction-schema")
|
||||
));
|
||||
openSession(server, requestIds, "schema-observer");
|
||||
JsonObject afterCommit = query(server, requestIds, "schema-observer", "SELECT CURRENT_SCHEMA", null);
|
||||
assertEquals("PUBLIC", afterCommit.getAsJsonArray("rows").get(0).getAsJsonArray().get(0).getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void affinityDetectionCoversCommonSessionStateStatements() {
|
||||
assertTrue(JdbcConnectionAffinity.requiresSessionAffinity("SET search_path TO app"));
|
||||
|
||||
Reference in new issue
Block a user