Тупики, вызывающие "Не удалось возобновить транзакцию" с помощью NHibernate и распределенных транзакций

У нас возникает проблема при использовании NHibernate с распределенными транзакциями.

Рассмотрим следующий фрагмент:

//
// There is already an ambient distributed transaction
//
using(var scope = new TransactionScope()) {
    using(var session = _sessionFactory.OpenSession())
    using(session.BeginTransaction()) {
        using(var cmd = new SqlCommand(_simpleUpdateQuery, (SqlConnection)session.Connection)) {
            cmd.ExecuteNonQuery();
        }

        session.Save(new SomeEntity());
        session.Transaction.Commit();
    }
    scope.Complete();
}

Иногда, когда сервер находится под экстремальной нагрузкой, мы увидим следующее:

  • Запрос, выполняемый с cmd.ExecuteNonQuery, выбирается как жертва взаимоблокировки (мы можем видеть его в SQL Profiler), но исключение не возникает.
  • session.Save завершается с сообщением об ошибке: "Операция недействительна для состояния транзакции".
  • Каждый раз, когда этот код выполняется после этого, session.BeginTransaction терпит неудачу. Первые несколько раз меняется внутреннее исключение (иногда это исключение тупиковой ситуации, которое должно было быть поднято на шаге 1). В конце концов он стабилизируется так: "Сервер не смог возобновить транзакцию. Desc: 3800000177". или "Новый запрос не запускается, потому что он должен иметь действующий дескриптор транзакции".

Если оставить в покое, приложение в конечном итоге (через секунды или минуты) восстановится после этого условия.

Почему исключение тупиковой ситуации не сообщается на шаге 1? И если мы не сможем решить это, то как мы можем временно приостановить наше приложение?

Проблема была воспроизведена в следующих средах

  • Windows 7 x64 и Windows Server 2003 x86
  • SQL Server 2005 и 2008
  • .NET 4.0 и 3.5
  • NHibernate 3.2, 3.1 и 2.1.2

Я создал тестовое оборудование, которое иногда воспроизводит проблему для нас. Он доступен здесь: http://wikiupload.com/EWJIGAECG9SQDMZ

Ответ 1

Мы, наконец, сузили это дело.

При открытии сеанса, если есть распределенная глобальная транзакция, NHibernate присоединяет обработчик события к Transaction.TransactionCompleted, который закрывает сеанс, когда распределенная транзакция завершена. Кажется, что это связано с условием гонки, в котором соединение может быть закрыто и возвращено в пул до того, как ошибка взаимоблокировки распространяется по всему, оставляя соединение в непригодном для использования состоянии.

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

using(var scope = new TransactionScope()) {
    //
    // Force promotion to distributed transaction
    //
    TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);

    var connection = new SqlConnection(_connectionString);
    connection.Open();

    //
    // Close the connection once the distributed transaction is
    // completed.
    //
    Transaction.Current.TransactionCompleted += 
        (sender, e) => connection.Close();

    using(connection.BeginTransaction())
        //
        // Deadlocks but sometimes does not raise exception
        //
        ForceDeadlockOnConnection(connection);

    scope.Complete();
}

//
// Subsequent attempts to open a connection with the same
// connection string will fail
//

Мы не решили решение, но следующие проблемы устранят проблему (возможно, и другие последствия):

  • Отключение пула соединений
  • Использование NHibernate AdoNetTransactionFactory вместо AdoNetWithDistributedTransactionFactory
  • Добавление обработки ошибок, вызывающей SqlConnection.ClearPool() при возникновении ошибки "сервер не удалось возобновить транзакцию"

Согласно Microsoft (https://connect.microsoft.com/VisualStudio/feedback/details/722659/), класс SqlConnection не является потокобезопасным и включает закрытие соединения в отдельном потоке. Основываясь на этом ответе, мы отправили отчет об ошибке для NHibernate (http://nhibernate.jira.com/browse/NH-3023).

Ответ 2

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

Первое, что я хотел бы предложить, - это попытаться подтвердить это, зарегистрировав хэш-код сеанса и hashcode session.GetSessionImplementation() (мое понимание использования функции nhibernate для замка состоит в том, что вы увидите тот же экземпляр сеанс, хотя на самом деле это другой сеанс, и реализация сеанса фактически покажет разницу). см., если вы видите одни и те же хэш-коды, используемые при обработке разных сообщений.

Если это вопрос управления сеансом, попробуйте использовать модуль nservicebus для управления сеансами для ваших обработчиков. вот сообщение от andreas об этом. я не думаю, что его редактирование о том, как сделать это, встроенное в багажник, было в версии 2.5, поэтому вы, вероятно, захотите продолжить это. (я мог ошибаться в этом.)

http://andreasohlund.net/2010/02/03/nhibernate-session-management-in-nservicebus/

Ответ 3

Это не решит вашу проблему, но вы можете заставить свой IPreInsertEventListener просто отправить сообщение NSB, а затем передать получателю сообщения хранимую процедуру. Я делал это с проблемными прослушивателями до и после событий при использовании NHibernate и NSB в прошлом.

Еще одна мысль заключается в том, что ваш прослушиватель pre-event создаст свой собственный объект подключения, заключенный в приложение nice using, и не будет касаться соединения NHibernate. Если он блокируется, то просто сделайте бросок, убедитесь, что вы удалили какой-либо объект в области.

Ответ 4

Это проблема NHibernate. NHibernate не открывает и не закрывает соединение в том же потоке, который не поддерживается ADO.NET. Вы можете обойти это, открыв и закрыв соединение самостоятельно. NHibernate не будет закрывать соединение, если оно не открыло его.

Обход

var connection = ((SessionFactoryImpl)_sessionFactory).ConnectionProvider.GetConnection();
using(var session = _sessionFactory.OpenSession(connection))
{
   //do database stuff
}
connection.Close();