Нигде не могу найти ответа.
Я покажу простой фрагмент кода, который показывает, как легко повредить пул соединений.
Повреждение пула соединений означает, что каждая новая попытка открытия соединения завершится ошибкой.
Чтобы столкнуться с проблемой, нам потребуется:
- быть в распределенной транзакции
- вложенное sqlconnection и его sqltransaction в другие sqlconnection и sqltransaction
- выполнить откат (явный или неявный - просто не фиксировать) вложенный sqltransaction
Когда пул соединений поврежден, каждый sqlConnection.Open () выдает одно из:
- SqlException: новый запрос не может быть запущен, потому что он должен иметь действительный дескриптор транзакции.
- SqlException: распределенная транзакция завершена. Либо зарегистрируйте этот сеанс в новой транзакции, либо в транзакции NULL.
Внутри ADO.NET существует своего рода гонка потоков. Если я поставлю Thread.Sleep(10)
где-нибудь в коде, полученное исключение может измениться на второе. Иногда меняется без каких-либо модификаций.
Как воспроизвести
- Включите службу Windows Координатора распределенных транзакций (по умолчанию она включена).
- Создайте пустое консольное приложение.
- Создайте 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);
}
Then:
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