Подтвердить что ты не робот

Пул соединений, поврежденный вложенными транзакциями ADO.NET(с MSDTC)

Я не могу найти ответ нигде.

Я покажу простой фрагмент кода, который представляет, как легко испортить пул подключений.
Повреждение пула подключений означает, что каждая попытка открытия нового соединения не будет выполнена.

Чтобы испытать проблему, нам нужно:

  • находиться в распределенной транзакции
  • вложенное sqlconnection и его sqltransaction в других sqlconnection и sqltransaction.
  • выполнить откат (explict or implict - просто не совершать) вложенное sqltransaction

Когда пул подключений поврежден, каждый sqlConnection.Open() выдает один из:

  • SqlException: новый запрос не разрешен для запуска, поскольку он должен иметь действительный дескриптор транзакции.
  • SqlException: распределенная транзакция завершена. Либо запустите этот сеанс в новой транзакции, либо в транзакции NULL.

В ADO.NET существует какая-то гонка потоков. Если бы я положил Thread.Sleep(10) где-то в коде, это могло бы изменить полученное исключение на второе. Иногда он изменяется без каких-либо изменений.


Как воспроизвести

  • Включить службу диспетчера распределенных транзакций (она включена по умолчанию).
  • Создать пустое консольное приложение.
  • Создайте 2 базы данных (может быть пустым) или 1 база данных и раскол: Transaction.Current.EnlistDurable[...]
  • Скопируйте и вставьте следующий код:

var connectionStringA = String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True;pooling=true;Max Pool Size=20;Enlist=true",
            @".\YourServer", "DataBaseA");
var connectionStringB = String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True;pooling=true;Max Pool Size=20;Enlist=true",
            @".\YourServer", "DataBaseB");

try
{
    using (var transactionScope = new TransactionScope())
    {
        //we need to force promotion to distributed transaction:
        using (var sqlConnection = new SqlConnection(connectionStringA))
        {
            sqlConnection.Open();
        }
        // you can replace last 3 lines with: (the result will be the same)
        // Transaction.Current.EnlistDurable(Guid.NewGuid(), new EmptyIEnlistmentNotificationImplementation(), EnlistmentOptions.EnlistDuringPrepareRequired);

        bool errorOccured;
        using (var sqlConnection2 = new SqlConnection(connectionStringB))
        {
            sqlConnection2.Open();
            using (var sqlTransaction2 = sqlConnection2.BeginTransaction())
            {
                using (var sqlConnection3 = new SqlConnection(connectionStringB))
                {
                    sqlConnection3.Open();
                    using (var sqlTransaction3 = sqlConnection3.BeginTransaction())
                    {
                        errorOccured = true;
                        sqlTransaction3.Rollback();
                    }
                }
                if (!errorOccured)
                {
                    sqlTransaction2.Commit();
                }
                else
                {
                    //do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2
                }
            }
        }
        if (!errorOccured)
            transactionScope.Complete();
    }
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Тогда:

for (var i = 0; i < 10; i++) //all tries will fail
{
    try
    {
        using (var sqlConnection1 = new SqlConnection(connectionStringB))
        {
            // Following line will throw: 
            // 1. SqlException: New request is not allowed to start because it should come with valid transaction descriptor.
            // or
            // 2. SqlException: Distributed transaction completed. Either enlist this session in a new transaction or the NULL transaction.
            sqlConnection1.Open();
            Console.WriteLine("Connection successfully open.");
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}


Известные плохие решения и то, что интересно можно наблюдать

Плохие решения:

  • Внутри вложенного sqltransaction с использованием блока do:
    sqlTransaction3.Rollback(); SqlConnection.ClearPool(sqlConnection3);

  • Замените все SqlTransactions на TransactionScopes (TransactionScope должен обернуть SqlConnection.Open())

  • Во вложенном блоке используйте sqlconnection из внешнего блока

Интересные наблюдения:

  • Если вам нужно подождать пару минут после соединения пула соединений, тогда все будет работать нормально. Таким образом, объединение пула соединений длится всего пару минут.

  • С приложением отладчика. Когда выполнение покидает внешнее sqltransaction с использованием блока SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.. Это исключение не улавливается try ... catch .....


Как его решить?

Эта проблема заставляет мое веб-приложение почти мертвым (невозможно открыть какое-либо новое соединение sql).
Представленный фрагмент кода извлекается из всего конвейера, который также включает вызовы сторонних фреймворков. Я не могу просто изменить код.

  • Кто-нибудь знает, что именно происходит не так?
  • Это ошибка ADO.NET?
  • Возможно, я (и некоторые фреймворки...) что-то не так?


Моя среда (это не очень важно)

  • .NET Framework 4.5
  • MS SQL Server 2012
4b9b3361

Ответ 1

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

Вложенные транзакции в SQL не так, как они бы отображались в структуре кода, который их создает.

Независимо от того, сколько вложенных транзакций существует, важна только внешняя транзакция.

Для того, чтобы внешняя транзакция могла совершать транзакции, внутренние транзакции должны совершать, другими словами, внутренние транзакции не действуют, если они фиксируют - внешний должен все еще совершать транзакцию для завершения.

Однако, если внутренняя транзакция откатывается назад, внешняя транзакция откатывается назад до ее начала. Внешняя транзакция все равно должна откат или фиксация - или она еще в начальном состоянии.

Следовательно, в приведенном выше примере линия

//do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2

должен быть

sqlTransaction2.Rollback();

если нет других транзакций, которые могли бы завершить и, следовательно, завершить внешнюю транзакцию.