Piscine de connexion corrompue par nested ADO.NET transactions (avec MSDTC))

je ne peux pas trouver la réponse nulle part.

je vais montrer un fragment de code simple qui montre comment corrompre facilement le pool de connexion.

la corruption du pool de connexion signifie que chaque nouvelle tentative de connexion ouverte échouera.



À l'expérience du problème, nous avons besoin de:

  1. pour être dans les transactions distribuées
  2. imbriqués sqlconnection et ses sqltransaction dans d'autres sqlconnection et sqltransaction
  3. rollback (explicit ou implict - il suffit de ne pas commettre) imbriquée sqltransaction

quand le pool de connexion est corrompu chaque connexion SQL.Open () lance un de:

  • SqlException: une nouvelle requête n'est pas autorisée à démarrer car elle doit être accompagnée d'un descripteur de transaction valide.
  • SqlException: transaction distribuée terminée. Soit inscrire cette session dans une nouvelle transaction ou la transaction nulle.

il y a une sorte de course de fils à l'intérieur ADO.NET. Si je mets Thread.Sleep(10) quelque part dans le code, il pourrait changer reçu exception à une seconde. Parfois, il change sans aucune modification.



Comment reproduire

  1. activer le service windows de coordonnateur de transactions distribuées (il est activé par défaut).
  2. Créer application console vide.
  3. Créer 2 bases de données (peut être vide) ou 1 base de données et décommenter la ligne: Transaction.Current.EnlistDurable[...]
  4. copier&coller code suivant:

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

Puis:

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



les mauvaises solutions connues et ce qui peut être observé d'intéressant

Mauvaises solutions:

  1. imbriquées à l'Intérieur sqltransaction l'aide du bloc faire:

    sqlTransaction3.Rollback(); SqlConnection.ClearPool(sqlConnection3);

  2. remplacer toutes les transactions SQL par des portées de transactions (TransactionScope doit envelopper SqlConnection.Open())

  3. dans le bloc imbriqué utilisez la connexion SQL à partir du bloc extérieur

observations Intéressantes:

  1. si application attendre quelques minutes après connexion piscine coruption puis tout fonctionne bien. Donc la connexion ne dure qu'en couple minute.

  2. avec débogueur attaché. Quand l'exécution sort de sqltransaction externe en utilisant le bloc SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION. est levée. Cette exception n'est pas rattrapable par try ... catch .....



Comment le résoudre ?

ce problème rend mon application web presque morte (impossible d'ouvrir une nouvelle connexion sql).

Fragment de code présenté est extrait de l'ensemble du pipeline qui consistent en des appels à une tierce partie les cadres de trop. Je ne peux pas simplement changer le code.

  • quelqu'un sait exactement ce qui va mal ?
  • Is it ADO.NET bug ?
  • peut-être I (et quelques cadres...) faire quelque chose de mal ?



Mon environnement (il ne semble pas être très important)

  • cadre. net 4.5
  • MS SQL Server 2012
19
demandé sur owerkop 2014-05-20 14:00:36

1 réponses

je sais que cette question a été posée il y a longtemps, mais je pense que j'ai la réponse pour tous ceux qui ont encore ce problème.

les Transactions imbriquées dans SQL ne sont pas comme elles apparaîtraient dans la structure du code qui les crée.

peu importe le nombre de transactions imbriquées, seule la transaction externe compte.

pour que la transaction externe soit capable de commettre, les transactions internes doivent commettre, en d'autres termes, les transactions internes n'ont pas l'effet s'ils commettent - de l'extérieur doit encore valider la transaction.

cependant, si une transaction interne est retournée, la transaction externe est retournée à son début. La transaction externe doit quand même se retourner ou s'engager - ou elle est encore dans son état démarré.

Donc, dans l'exemple ci-dessus, la ligne

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

doit être

sqlTransaction2.Rollback();

sauf il y a d'autres transactions qui pourraient compléter et terminer la transaction externe.

5
répondu Steve Padmore 2015-03-11 17:48:34