L’interruzione del thread lascia transazioni di zombie e interruzione di SqlConnection

Sento che questo comportamento non dovrebbe accadere. Ecco lo scenario:

  1. Avvia una transazione sql long-running.

  2. Il thread che ha eseguito il comando sql viene interrotto (non dal nostro codice!)

  3. Quando il thread ritorna al codice gestito, lo stato di SqlConnection è “Chiuso”, ma la transazione è ancora aperta sul server sql.

  4. SQLConnection può essere riaperto, e puoi provare a richiamare il rollback sulla transazione, ma non ha alcun effetto (non che mi aspetterei da questo comportamento. Il punto è che non c’è modo di accedere alla transazione sul db e rollarlo indietro.)

Il problema è semplicemente che la transazione non viene ripulita correttamente quando il thread si interrompe. Questo era un problema con .Net 1.1, 2.0 e 2.0 SP1. Stiamo eseguendo .Net 3.5 SP1.

Ecco un esempio di programma che illustra il problema.

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=;Initial Catalog=;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  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..."); } ///  /// The main entry point for the application. ///  [STAThread] static void Main(string[] 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(); } } } } 

C’è un hotfix di Microsoft indirizzato a .Net2.0 SP1 che dovrebbe risolvere questo problema , ma ovviamente abbiamo le DLL più recenti (.Net 3.5 SP1) che non corrispondono ai numeri di versione elencati in questo hotfix.

Qualcuno può spiegare questo comportamento e perché ThreadAbort non sta ancora ripulendo correttamente la transazione sql? .Net 3.5 SP1 non include questo aggiornamento rapido, o questo comportamento è tecnicamente corretto?

Questo è un bug nell’implementazione MARS di Microsoft. Distriggersndo MARS nella stringa di connessione, il problema scompare.

Se hai bisogno di MARS, e stai facendo in modo che la tua applicazione dipenda dall’implementazione interna di un’altra azienda, familiarizza con http://dotnet.sys-con.com/node/39040 , esplora .NET Reflector e guarda la connessione e il pool classi. È necessario memorizzare una copia della proprietà DbConnectionInternal prima che si verifichi l’errore. Successivamente, utilizzare reflection per passare il riferimento a un metodo di deallocation nella class di pooling interna. Ciò interromperà la connessione per 4:00 – 7:40 minuti.

Esistono sicuramente altri modi per forzare la connessione fuori dal pool e per essere eliminata. A parte una correzione di Microsoft, tuttavia, la riflessione sembra essere necessaria. I metodi pubblici nell’API ADO.NET non sembrano aiutare.

Poiché stai utilizzando SqlConnection con il pool, il tuo codice non ha mai il controllo della chiusura delle connessioni. La piscina è. Sul lato server, una transazione in attesa verrà ripristinata quando la connessione è veramente chiusa (socket chiuso), ma con il pooling sul lato server non viene mai vista una connessione chiusa. sp_reset_connection la chiusura della connessione (tramite la disconnessione fisica sul socket / pipe / livello LPC o tramite la chiamata sp_reset_connection ), il server non può interrompere la transazione in sospeso. Quindi in realtà si riduce al fatto che la connessione non ottiene correttamente il rilascio / ripristino. Non capisco il motivo per cui stai cercando di complicare il codice con il licenziamento esplicito del thread interruzione e il tentativo di riaprire una transazione chiusa (che non funzionerà mai ). Dovresti semplicemente racchiudere SqlConnection in un blocco using(...) , l’implicita infine e la connessione Dispose verrà eseguita anche in caso di interruzione del thread.

La mia raccomandazione sarebbe quella di mantenere le cose semplici, abbandonare la gestione abortita del thread e sostituirla con un semplice blocco ‘using’ (using(connection) {using(transaction) {code; commit () }} .

Naturalmente suppongo che non si propaghi il contesto della transazione in un ambito diverso nel server (non si usa sp_getbindtoken e gli amici e non si registra nelle transazioni distribuite).

Questo piccolo programma mostra che Thread.Abort chiude correttamente una connessione e la transazione viene ripristinata:

 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()) { // Retrieve 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(string[] 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); } } } }