суббота, 24 декабря 2011 г.

Hibernate nested transactions

Сталкивались вы когда-нибудь с вложенными транзакциями?

В двух словах: эта такая транзакция которая создается внутри родительской транзакции. И может быть обработана независимо от основной.

Но проще это объяснить на примере.

Например. Например вам нужно сделать сервис, который будет регулярно опрашивать базу данных и проводить обработку транзакций. Каждый такой вызов транзакции должен сопровождаться обращением в интернет или какую-другую нестабильную среду и в результате чего может образоваться ошибка (или что более правильно исключение).

Вот как-то так.

Что бы обеспечить стабильность, вы должны обрабоать эту ошибку и в дальнейшем привизать эту ошибку к транзакции.

То есть простым языком получаете список необработанных транзакций. Вызываете функцию обработки транзакции и ошибку записываете в другое поле той же транзакции.

Как все это реализовать на Hibernate \ Spring?

Ну в документации сказано что есть такое средство. Называется nested transaction. Для его активации у метода, создающего вложенную транзакцию необходимо использовать атрибут PROPAGATION_NESTED. А еще прописать в tarnsactionmanager проперти nestedTransactionAllowed со значением true.

Вот как выглядит эта теория на практике:

dao.xml

  <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
    <property name="nestedTransactionAllowed" value="true" />
  </bean>


IncomeProcessor.java

@Service
@Transactional
public class IncomeProcessor {


    [...]


    @Scheduled(fixedRate = 60 * 1000)
    public void processNormal() {
       List<Transaction> list = some.list();
       for(Transaction t : list) {
         try {
           process(t);
         } catch(Exception e){
             t.setError(e);
             dao.update(t);
         }
    }
    
    @Transactional(propagation = Propagation.NESTED)
    public void process(Transaction t) {
       doSomeExternalJob(t);
    }
}

В примере мы получем список, пробегаемся по всем эелемнтам и все ошибки (исключения) записываем в объект transaction в базу данных. В териории все исключения возникшие в вызове process(), обернуты обработчиком откатывают все изменения в базе данных сделаные методом doSomeExternalJob(). Таким образом мы не теряем данные и не захламляем базу новыми объектами созданными до возникновения исключения.

Но на практике такой подход не работает. Я не стал разбераться ошибка ли это хибернейта или jdbc драйвера. Я стал разбераться в том как эти вложенные транзакции реализованы и нашел сразу две довольно критичные проблемы в приведенной выше схеме. Так что стандартный механизм спринга лучше в таком варианте не использовать, если не хотите потерять данные (или пропустить обработку).

Прежде всего хочу сказать как это должно быть реализовано в jdbc драйвере.

При обработке вложенных транзакций драйвер, в точке входа в функцию создает так называемые Savepoint. И при выходе, в случае возникновения ошибки вызвает функцию у соединения rollback() с передачей ей нужной точки сохранения. Таким образом при входе в фукнцию process должна была бы вызваться connection.setSavepoint() и при выходе, для отката части транзакции connection.rollback(savePoint).

Почему это не работает, непонятно. Можно покапаться в исходниках sping / hibernate благо они доступны и найти что нибудь глупое навроде не правильной версии jdbc или пропуск создания какогонибудь хитрого bean с указанной логикой. Я решил не искать проблему и не создавать баг репорта по причине что я нашел вторую проблему. Так же связанную с привиденным выше алгоритмом работы. И эта ситуация на корню блокирует любые попытки программно (с помощью атрибутов) сделать элегантное решение этих nested transactions.

В чем эта вторая проблема заключается и почему исправление вложных транзакций так сложно?

Я провел простой эксперимент. Провел симуляцию правильной логики сохранения \ откатов savepoints и обнаржил что не все объекты всстанавливают свое исходное состояние. Несмотря на то что откат проходит успешно некоторые объекты все равно остаются в своем прежнем состоянии как до отката. Отмененная транзакция до указанного savepoint ни как не затрагивает объектов уже скаченных с сервера, закешированных и измененных на памяти.

Что это означает. Если вы создаете savepoint для текущей транзакции и передаете в фукнцию обработки объект транзакция то не смотря на то что вызываете rollback нет такого маханизма в спринге что бы отслеживать объекты измененные на памяти и запрашивать автоматически их состояния с сервера.

Что привело меня к мысли что исправление вложенных транзакций все равно не повзолит реализовать указанную лоигку только на стороне спринг \ хибернейт библиотек. Так как перечитывать состояние всех объектов после отката одной транзакции не имеет практического смысла и к тому же приведет к потере данных. А откатывать отдельный параметр было бы так же не допустимо.

В результате этих рассуждений я решил реализовать все это руками, с исправлением указанных проблем. Привожу работающий код:

NestedTransaction.java
package com.payment.db.services;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.List;
import java.util.Stack;

import org.hibernate.SessionFactory;
import org.hibernate.jdbc.Work;

public abstract class NestedTransaction<T> {

    public void list(SessionFactory sessionFactory, List<T> list) {
        final Stack<Savepoint> s = new Stack<Savepoint>();
        for (T e : list) {
            try {
                sessionFactory.getCurrentSession().doWork(new Work() {
                    @Override
                    public void execute(Connection connection) throws SQLException {
                        s.push(connection.setSavepoint());
                    }
                });
                process(e);
            } catch (Exception ee) {
                sessionFactory.getCurrentSession().doWork(new Work() {
                    @Override
                    public void execute(Connection connection) throws SQLException {
                        connection.rollback(s.pop());
                    }
                });
                sessionFactory.getCurrentSession().refresh(e);
                exception(e, ee);
            }
        }
    }

    public abstract void process(T e);

    public abstract void exception(T e, Exception ee);
}

IncomeProcessor.java
package com.payment.db.services;

@Service
@Transactional
public class IncomeProcessor extends NestedTransaction<ChargePool> {

    [...]

    @Scheduled(fixedRate = 60 * 1000)
    public void processNormal() {
        List<ChargePool> list = chargepool.listAvailable();

        if (list == null || list.size() == 0) {
            return;
        }

        list(sessionFactory, list);
    }

    public void process(ChargePool cp) {
        Charge c = cp.getCharge();
        if (!c.isLinked())
            throw new RuntimeException("operation on non linked charge");
        SomeProcessIncome(cp);
    }

    public void exception(ChargePool cp, Exception e) {
        cp.setLastOperationRespond(ExceptionText.covert(e));
        cp.setLastOperationDate(new Date());
        chargepool.update(cp);
    }
}


В приведенном примере создан базовый класс для обработки вложенных транзакций с использованием фукнций сохранения \ отката состояний (Savepoint) и так же исправлена проблема с откатом объектов на памяти вызовом фукнции sessionFactory.getCurrentSession().refresh(e)


0 коммент.:

Отправить комментарий