L'abandon de thread laisse des transactions zombies et cassé SqlConnection

Je me sens comme ce comportement ne devrait pas se produire. Voici le scénario:

  1. Démarrer une transaction SQL longue durée.

  2. Le thread qui a exécuté la command sql est annulé (pas par notre code!)

  3. Lorsque le thread returnne au code managé, l'état de SqlConnection est "Fermé" – mais la transaction est toujours ouverte sur le server SQL.

  4. Le SQLConnection peut être rouvert, et vous pouvez essayer d'appeler rollback sur la transaction, mais il n'a aucun effet (pas que je m'attendrais à ce comportement.Le point est il n'y a aucun moyen d'accéder à la transaction sur la DB et rouler arrière.)

Le problème est simplement que la transaction n'est pas nettoyée correctement lorsque le thread s'interrompt. Cela a été un problème avec .Net 1.1, 2.0 et 2.0 SP1. Nous exécutons .Net 3.5 SP1.

Voici un exemple de programme qui illustre le problème.

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.SqlClient; using System.Threading; namespace ConsoleApplication1 { class Run { static Thread transactionThread; public class ConnectionHolder : IDisposable { public void Dispose() { } public void executeLongTransaction() { Console.WriteLine("Starting a long running transaction."); using (SqlConnection _con = new SqlConnection("Data Source=<YourServer>;Initial Catalog=<YourDB>;Integrated Security=True;Persist Security Info=False;Max Pool Size=200;MultipleActiveResultSets=True;Connect Timeout=30;Application Name=ConsoleApplication1.vshost")) { try { SqlTransaction trans = null; trans = _con.BeginTransaction(); SqlCommand cmd = new SqlCommand("update <YourTable> set Name = 'XXX' where ID = @0; waitfor delay '00:00:05'", _con, trans); cmd.Parameters.Add(new SqlParameter("0", 340)); cmd.ExecuteNonQuery(); cmd.Transaction.Commit(); Console.WriteLine("Finished the long running transaction."); } catch (ThreadAbortException tae) { Console.WriteLine("Thread - caught ThreadAbortException in executeLongTransaction - resetting."); Console.WriteLine("Exception message: {0}", tae.Message); } } } } static void killTransactionThread() { Thread.Sleep(2 * 1000); // We're not doing this anywhere in our real code. This is for simulation // purposes only! transactionThread.Abort(); Console.WriteLine("Killing the transaction thread..."); } /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main(ssortingng[] args) { using (var connectionHolder = new ConnectionHolder()) { transactionThread = new Thread(connectionHolder.executeLongTransaction); transactionThread.Start(); new Thread(killTransactionThread).Start(); transactionThread.Join(); Console.WriteLine("The transaction thread has died. Please run 'select * from sysprocesses where open_tran > 0' now while this window remains open. \n\n"); Console.Read(); } } } } 

Il existe un correctif Microsoft ciblant .Net2.0 SP1 qui était censé résoudre ce problème , mais nous avons évidemment des DLL plus récentes (.Net 3.5 SP1) qui ne correspondent pas aux numéros de version répertoriés dans ce correctif.

Quelqu'un peut-il expliquer ce comportement, et pourquoi ThreadAbort ne nettoie toujours pas correctement la transaction SQL? Est-ce que .Net 3.5 SP1 n'inclut pas ce correctif ou ce comportement est-il techniquement correct?

Ceci est un bug dans l'implémentation MARS de Microsoft. Désactiver MARS dans votre string de connection fera disparaître le problème.

Si vous avez besoin de MARS, et que vous êtes à l'aise pour que votre application dépende de l'implémentation interne d'une autre entreprise, familiarisez-vous avec http://dotnet.sys-con.com/node/39040 , sortez .NET Reflector et observez la connection et le pool Des classs. Vous devez stocker une copy de la propriété DbConnectionInternal avant que l'échec se produise. Plus tard, utilisez la reflection pour transmettre la reference à une méthode de désallocation dans la class de pool interne. Cela empêchera votre connection de restr entre 4h00 et 7h40 minutes.

Il y a sûrement d'autres moyens de forcer la connection hors de la piscine et d'en disposer. Short d'un correctif de Microsoft, cependant, la reflection semble être nécessaire. Les methods publiques dans l'API ADO.NET ne semblent pas aider.

Puisque vous utilisez SqlConnection avec pooling, votre code ne contrôle jamais la fermeture des connections. La piscine est. Côté server, une transaction en attente sera annulée lorsque la connection est réellement fermée (socket fermé), mais avec la mise en pool, le côté server ne verra jamais la fermeture d'une connection. Sans la fermeture de la connection (soit par déconnection physique sur la couche socket / pipe / LPC, soit par l'appel sp_reset_connection ), le server ne peut pas abandonner la transaction en attente. Cela se résume donc au fait que la connection n'est pas correctement relâchée / réinitialisée. Je ne comprends pas pourquoi vous essayez de compliquer le code avec un abandon de thread explicite et d'essayer de rouvrir une transaction fermée (cela ne marchera jamais ). Vous devriez simplement encapsuler SqlConnection dans un bloc using(...) , l'implicite finally et la connection Dispose seront exécutés même à l'arrêt du thread.

Ma recommandation serait de garder les choses simples, abandonner la gestion de l'abandon de thread fantaisie et le replace par un simple bloc 'using' (using(connection) {using(transaction) {code; commit () }} .

Bien sûr, je suppose que vous ne sp_getbindtoken pas le context de la transaction dans une autre scope du server (vous n'utilisez pas sp_getbindtoken et vos amis, et vous ne vous inscrivez pas dans les transactions dissortingbuées).

Ce petit programme montre que Thread.Abort ferme correctement une connection et la transaction est annulée:

 using System; using System.Data.SqlClient; using testThreadAbort.Properties; using System.Threading; using System.Diagnostics; namespace testThreadAbort { class Program { static AutoResetEvent evReady = new AutoResetEvent(false); static long xactId = 0; static void ThreadFunc() { using (SqlConnection conn = new SqlConnection(Settings.Default.conn)) { conn.Open(); using (SqlTransaction trn = conn.BeginTransaction()) { // Resortingeve our XACTID // SqlCommand cmd = new SqlCommand("select transaction_id from sys.dm_tran_current_transaction", conn, trn); xactId = (long) cmd.ExecuteScalar(); Console.Out.WriteLine("XactID: {0}", xactId); cmd = new SqlCommand(@" insert into test (a) values (1); waitfor delay '00:01:00'", conn, trn); // Signal readyness and wait... // evReady.Set(); cmd.ExecuteNonQuery(); trn.Commit(); } } } static void Main(ssortingng[] args) { try { using (SqlConnection conn = new SqlConnection(Settings.Default.conn)) { conn.Open(); SqlCommand cmd = new SqlCommand(@" if object_id('test') is not null begin drop table test; end create table test (a int);", conn); cmd.ExecuteNonQuery(); } Thread thread = new Thread(new ThreadStart(ThreadFunc)); thread.Start(); evReady.WaitOne(); Thread.Sleep(TimeSpan.FromSeconds(5)); Console.Out.WriteLine("Aborting..."); thread.Abort(); thread.Join(); Console.Out.WriteLine("Aborted"); Debug.Assert(0 != xactId); using (SqlConnection conn = new SqlConnection(Settings.Default.conn)) { conn.Open(); // checked if xactId is still active // SqlCommand cmd = new SqlCommand("select count(*) from sys.dm_tran_active_transactions where transaction_id = @xactId", conn); cmd.Parameters.AddWithValue("@xactId", xactId); object count = cmd.ExecuteScalar(); Console.WriteLine("Active transactions with xactId {0}: {1}", xactId, count); // Check count of rows in test (would block on row lock) // cmd = new SqlCommand("select count(*) from test", conn); count = cmd.ExecuteScalar(); Console.WriteLine("Count of rows in text: {0}", count); } } catch (Exception e) { Console.Error.Write(e); } } } }