transactions ado.net transactionscope msdtc

transactions - Grupo de conexiones dañado por transacciones anidadas de ADO.NET(con MSDTC)



transactionscope (1)

No puedo encontrar respuesta en ningún lado.

Mostraré un fragmento de código simple que presenta cómo corromper fácilmente el conjunto de conexiones.
La corrupción del grupo de conexiones significa que cada nuevo intento de conexión abierta fallará.

Para experimentar el problema necesitamos:

  1. estar en transaccion distribuida
  2. sqlconnection anidada y su sqltransaction en otra sqlconnection y sqltransaction
  3. hacer rollback (explícito o implícito - simplemente no cometer) sqltransaction anidado

Cuando el grupo de conexiones está dañado, cada sqlConnection.Open () lanza uno de:

  • SqlException: la nueva solicitud no se puede iniciar porque debe venir con un descriptor de transacción válido.
  • SqlException: Transacción distribuida completada. Enlista esta sesión en una nueva transacción o la transacción NULA.

Hay algún tipo de carrera de hilos dentro de ADO.NET. Si pongo Thread.Sleep(10) en algún lugar del código, podría cambiar la excepción recibida a la segunda. A veces cambia sin modificaciones.


Como reproducirse

  1. Habilitar el servicio de ventanas Coordinador de transacciones distribuidas (está habilitado de forma predeterminada).
  2. Crear una aplicación de consola vacía.
  3. Cree 2 bases de datos (puede estar vacía) o 1 base de datos y elimine el comentario: Transaction.Current.EnlistDurable[...]
  4. Copie y pegue el siguiente código:

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); }

Entonces:

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); } }


Conocidas malas soluciones y lo interesante que se puede observar.

Soluciones pobres:

  1. Dentro de sqltransaction anidado usando el bloque do:
    sqlTransaction3.Rollback(); SqlConnection.ClearPool(sqlConnection3);

  2. Reemplace todas las transacciones Sql con TransactionScopes ( TransactionScope tiene que envolver SqlConnection.Open() )

  3. En el bloque anidado, use sqlconnection del bloque exterior.

Observaciones interesantes:

  1. Si la aplicación espera un par de minutos después de la conexión a la piscina, todo funcionará bien. Por lo tanto, la conexión de la piscina dura solo un par de minutos.

  2. Con el depurador adjunto. Cuando la ejecución abandona sqltransaction externo utilizando el bloque SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION. es aventado. Esa excepción no es detectable por try ... catch ....


Cómo resolverlo ?

Ese problema hace que mi aplicación web esté casi muerta (no se puede abrir ninguna nueva conexión de SQL).
El fragmento de código presentado se extrae de toda la canalización, que también consiste en llamadas a marcos de terceros. No puedo simplemente cambiar el código.

  • ¿Alguien sabe qué es exactamente lo que va mal?
  • ¿Es ADO.NET error?
  • Tal vez yo (y algunos marcos ...) hagamos algo mal?


Mi entorno (no parece ser muy importante)

  • .NET Framework 4.5
  • MS SQL Server 2012

Sé que esta pregunta se hizo hace mucho tiempo, pero creo que tengo la respuesta para cualquier persona que aún tenga este problema.

Las transacciones anidadas en SQL no son como aparecerían en la estructura del código que las crea.

No importa cuántas transacciones anidadas haya, solo importa la transacción externa.

Para que la transacción externa pueda comprometerse, las transacciones internas deben comprometerse, en otras palabras, las transacciones internas no tienen efecto si se comprometen, la externa debe comprometerse para que la transacción se complete.

Sin embargo, si una transacción interna se revierte, la transacción externa se revierte a su inicio . La transacción externa todavía debe revertirse o confirmarse, o aún se encuentra en su estado de inicio .

Por lo tanto, en el ejemplo anterior, la línea

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

debiera ser

sqlTransaction2.Rollback();

a menos que haya otras transacciones que puedan completar y, por lo tanto, completar la transacción externa.