Совместная транзакция между различными соединениями OracleDB
После нескольких дней, проведенных для расследования вопроса, я решил представить этот вопрос, потому что нет никакого смысла в том, что происходит.
Дело
Мой компьютер настроен с локальной базой данных Oracle Express.
У меня есть проект JAVA с несколькими JUnit Tests, которые расширяют родительский класс (я знаю, что это не "лучшая практика" ), которая открывает соединение OJDBC (используя статический пул соединений Hikari из 10 подключений) в методе @Before и свернута Верните его в @After.
public class BaseLocalRollbackableConnectorTest {
private static Logger logger = LoggerFactory.getLogger(BaseLocalRollbackableConnectorTest.class);
protected Connection connection;
@Before
public void setup() throws SQLException{
logger.debug("Getting connection and setting autocommit to FALSE");
connection = StaticConnectionPool.getPooledConnection();
}
@After
public void teardown() throws SQLException{
logger.debug("Rollback connection");
connection.rollback();
logger.debug("Close connection");
connection.close();
}
StacicConnectionPool
public class StaticConnectionPool {
private static HikariDataSource ds;
private static final Logger log = LoggerFactory.getLogger(StaticConnectionPool.class);
public static Connection getPooledConnection() throws SQLException {
if (ds == null) {
log.debug("Initializing ConnectionPool");
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(10);
config.setDataSourceClassName("oracle.jdbc.pool.OracleDataSource");
config.addDataSourceProperty("url", "jdbc:oracle:thin:@localhost:1521:XE");
config.addDataSourceProperty("user", "MyUser");
config.addDataSourceProperty("password", "MyPsw");
config.setAutoCommit(false);
ds = new HikariDataSource(config);
}
return ds.getConnection();
}
}
Этот проект содержит сотни тестов (не параллельно), которые используют это соединение (на локальном хосте) для выполнения запросов (вставка/обновление и выбор) с использованием Sql2o, но транзакция и соединение соединения управляются только извне (по вышеприведенному тесту).
База данных полностью пуста, чтобы иметь тесты ACID.
Таким образом, ожидаемый результат - вставить что-то в БД, сделать утверждения, а затем откат. таким образом, второй тест не найдет данных, добавленных предыдущим тестом, чтобы поддерживать уровень изоляции.
Проблема
Выполнение всех тестов вместе (последовательно), в 90% случаев они работают правильно. 10% один или два теста, случайным образом, терпят неудачу, поскольку в базе данных (например, дублируются уникальные данные) есть грязные данные (например, дублированные). просмотр журналов, откаты предыдущих тестов были выполнены правильно. На самом деле, если я проверю базу данных, она пуста)
Если я буду выполнять эти тесты на сервере с более высокой производительностью, но тот же JDK, такой же Oracle DB XE, этот коэффициент отказа увеличен до 50%.
Это очень странно, и я понятия не имею, потому что соединения разные между тестами, и откатывается каждый раз. Уровень изоляции JDBC READ COMMITTED, поэтому, даже если мы использовали одно и то же соединение, это не должно создавать никаких проблем даже при использовании одного и того же соединения.
Поэтому мой вопрос:
Почему это происходит? у тебя есть идеи? Является ли откат JDBC синхронным, как я знаю, или могут быть некоторые случаи, когда он может продвигаться вперед, хотя он не полностью завершен?
Это мои основные параметры базы данных:
процессы 100
сессии 172
сделки 189
Ответы
Ответ 1
После всех подтверждений ваших ответов, что я не злюсь с откатом и транзакциями в модульных тестах, я глубоко проверил все запросы и все возможные причины и, к счастью (да, к счастью... даже если мне стыдно за это, я сделайте мой разум свободным) все работает как ожидалось (транзакции, до, после и т.д.).
Есть несколько запросов, которые получают результат некоторых сложных представлений (и глубоко глубоко настроенных на уровень DAO) для идентификации информации о одной строке.
Это представление основано на MAX of a TIMESTAMP
, чтобы идентифицировать последнее из определенного события (в реальной жизни события, следующие через несколько месяцев).
Выполняя подготовку базы данных для продолжения модульных тестов, эти события добавляются последовательно каждым тестом.
В некоторых случаях, когда эти запросы вставки по одной и той же транзакции являются особенно быстрыми, в одну и ту же Миллисекунду добавляется больше событий, связанных с одним и тем же объектом (TIMESTAMP добавляется вручную с использованием даты JODA DateTime) и MAX даты, возвращает два или больше значений.
По этой причине объясняется тот факт, что на более производительных компьютерах/серверах это происходит чаще, чем более медленные.
Это представление используется в большем количестве тестов и в зависимости от теста, ошибка отличается и случайна (значение NULL добавлено как первичный ключ, дублированный первичный ключ и т.д.).
Пример: в следующем INSERT SELECT
запросе очевидна эта ошибка:
INSERT INTO TABLE1 (ID,COL1,COL2,COL3)
SELECT :myId, T.VAL1, T.VAL2, T.VAL3
FROM MyView v
JOIN Table2 t on t.ID = v.ID
WHERE ........
параметр myId добавляется впоследствии как параметр Sql2o
MyView -
SELECT ID, MAX(MDATE) FROM TABLEV WHERE.... GROUP BY ...
Когда представление возвращает хотя бы 2 результата из-за одной и той же Макс. даты, он терпит неудачу, потому что идентификатор является фиксированным (генерируется последовательностью в начале, но сохраняется с использованием параметра во второй раз). Это порождает нарушение PK.
Это только один случай, но заставил меня (и моих коллег) сумасшедшим из-за этого случайного поведения...
Добавление спая в 1 миллисекунду между этими событиями вставляется, оно фиксировано. теперь мы работаем над тем, чтобы найти другое решение, хотя этот случай (пользователь, который взаимодействует два раза в той же миллисекунде) не может произойти в производственной системе
но важно то, что никакая магия не происходит, как обычно!
Теперь вы можете оскорбить меня:)
Ответ 2
Я столкнулся с той же проблемой 2-3 года назад (я потратил много времени, чтобы получить это прямо). Проблема в том, что @Before и @After не всегда действительно последовательны. [Вы можете попробовать это, начав процесс в отладке и поместите некоторые точки останова в аннотированных методах.
Изменить: Я не был достаточно ясен, как отметил Tonio. Порядок @Before и @After гарантируется с точки зрения работы перед тестом и после этого. Проблема была в моем случае, что иногда @Before и @After были испорчены.
Ожидаемое:
@Before → test1() → @After → @Before → @test2() → @After
Но иногда я испытывал следующий порядок:
@Before → test1() → @Before → @After → @test2() → @After
Я не уверен, что это ошибка или нет. В то время, когда я врывался в глубину, и это казалось каким-то (процессор?) Планированием связанной магии.
Решение этой проблемы было в нашем случае для запуска тестов в одном потоке и вызова вручную процессов инициализации и очистки... Что-то вроде этого:
public class BaseLocalRollbackableConnectorTest {
private static Logger logger = LoggerFactory.getLogger(BaseLocalRollbackableConnectorTest.class);
protected Connection connection;
public void setup() throws SQLException{
logger.debug("Getting connection and setting autocommit to FALSE");
connection = StaticConnectionPool.getPooledConnection();
}
public void teardown() throws SQLException{
logger.debug("Rollback connection");
connection.rollback();
logger.debug("Close connection");
connection.close();
}
@Test
public void test() throws Exception{
try{
setup();
//test
}catch(Exception e){ //making sure that the teardown will run even if the test is failing
teardown();
throw e;
}
teardown();
}
}
Я не тестировал его, но гораздо более элегантным решением могло бы быть синхронизация методов @Before и @After на одном и том же объекте. Пожалуйста, уточните меня, если у вас есть чанс, чтобы попробовать.:)
Я надеюсь, что это тоже решит вашу проблему.
Ответ 3
Если ваша проблема просто должна быть "решена" (например, не "лучшая практика" ), независимо от производительности, чтобы просто завершить тестирование, попробуйте установить:
config.setMaximumPoolSize(1);
Вам может потребоваться установить максимальный тайм-аут, поскольку тесты в тестовой очереди будут ждать его очереди и могут быть таймаутом. Обычно я не предлагаю такие решения, но ваша установка субоптимальна, это приведет к условиям гонки и потере данных. Тем не менее, удачи в тестах.
Ответ 4
Попробуйте настроить аудит для всех операторов в Oracle. Затем найдите сеансы, которые живут одновременно. Я думаю, что в тестах есть проблема. Откат JDBC является синхронным. Commit может быть настроен как commit nowait
, но я не думаю, что вы делаете это специально для своих тестов.
Также обратите внимание на параллельный dml. На одной таблице в той же транзакции вы не можете выполнять параллельный dml + любой другой dml без фиксации, потому что вы получаете Ora-12838.
Есть ли у вас аутоавтоматическая транзакция? Бизнес-логика в тестах может вручную отменить их, и во время тестов autonoumous transaction похож на другой сеанс, и он не видит никаких коммитов из родительского сеанса.
Ответ 5
Не уверен, что это исправит, но вы можете попробовать:
public class BaseLocalRollbackableConnectorTest {
private static Logger logger = LoggerFactory.getLogger(BaseLocalRollbackableConnectorTest.class);
protected Connection connection;
private Savepoint savepoint;
@Before
public void setup() throws SQLException{
logger.debug("Getting connection and setting autocommit to FALSE");
connection = StaticConnectionPool.getPooledConnection();
savepoint = connection.setSavepoint();
}
@After
public void teardown() throws SQLException{
logger.debug("Rollback connection");
connection.rollback(savepoint);
logger.debug("Close connection");
connection.close();
while (!connection.isClosed()) {
try { Thread.sleep(500); } catch (InterruptedException ie) {}
}
}
На самом деле есть два "исправления" - цикл после закрытия, чтобы убедиться, что соединение закрыто перед возвратом в пул. Во-вторых, создайте точку сохранения перед тестом и затем восстановите ее.
Ответ 6
Как и все другие ответы, трудно сказать, что не так с предоставленной информацией. Более того, даже если вам удастся найти текущую проблему с помощью аудита, это не значит, что ваши тесты свободны от ошибок данных.
Но здесь альтернатива: поскольку у вас уже есть пустая схема базы данных, вы можете экспортировать ее в файл SQL. Затем перед каждым тестом:
- Отбросить схему
- Снова заново создайте схему.
- Подайте данные образца (при необходимости)
Это позволит сэкономить много времени на отладку, убедитесь, что база данных в ее первозданном состоянии каждый раз, когда вы запускаете тесты. Все это можно сделать в script.
Примечание. Oracle Enterprise имеет функцию flashback для поддержки вашего вида операции. Кроме того, если вам удастся использовать Hibernate и другие, в других базах памяти (например HSQLDB), который вы можете использовать для увеличения скорости тестирования и поддержания согласованности в вашем наборе данных.
РЕДАКТИРОВАТЬ: Это кажется неправдоподобным, но на всякий случай: connection.rollback()
вступает в силу, если вы не вызываете commit
() перед этим.
Ответ 7
Вы можете сделать одну вещь, увеличив ее. соединений в максимальном размере пула и откат операции в том же месте, где вы совершили операцию, вместо того, чтобы использовать ее в операторе @после.
Надеюсь, что это сработает.