Czuję, że takie zachowanie nie powinno mieć miejsca. Oto scenariusz:

  1. Rozpocznij długotrwałą transakcję sql.

  2. Wątek, który uruchomił polecenie sql zostanie przerwany (nie przez nasz kod!)

  3. Kiedy wątek wróci do Managed kod, stan SqlConnection to „Zamknięte” – ale transakcja jest nadal otwarte na serwerze sql.

  4. SQLConnection można ponownie otworzyć, i możesz spróbować wywołać wycofanie transakcja, ale nie ma efekt (nie żebym oczekiwał takiego zachowania. Chodzi o to, że nie ma możliwości uzyskania dostępu do transakcji w bazie danych i wycofania jej.)

Problem polega po prostu na tym, że transakcja nie jest prawidłowo czyszczona po przerwaniu wątku. To był problem z .Net 1.1, 2.0 i 2.0 SP1. Korzystamy z platformy .Net 3.5 SP1.

Oto przykładowy program ilustrujący problem.

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

Istnieje poprawka Microsoft skierowana do .Net2.0 SP1, która miała rozwiązać ten problem, ale my oczywiście mają nowsze biblioteki DLL (.Net 3.5 SP1), które nie są zgodne z numerami wersji wymienionymi w tej poprawce.

Czy ktoś może wyjaśnić to zachowanie i dlaczego ThreadAbort nadal nie czyści prawidłowo transakcji sql? Czy .Net 3.5 SP1 nie zawiera tej poprawki, czy to zachowanie jest technicznie poprawne?

16
womp 2 czerwiec 2011, 23:20
4
Proszę o żadne komentarze na temat nie używania Thread.Abort - nie używamy go nigdzie. To po prostu czasami fakt, że IIS rzuca je, jeśli przypadkowo uzyskasz recykling domeny aplikacji lub cokolwiek innego. Nie używamy Thread.Abort nigdzie w naszym kodzie :) Właśnie zauważyliśmy to zachowanie i prześledziliśmy je z powrotem do tego scenariusza — przykładowy program jest oczywiście wymyślony.
 – 
womp
2 czerwiec 2011, 23:24
5
Jeśli nie używasz Thread.Abort w dowolnym miejscu w kodzie, możesz umieścić ten komentarz w tym kodzie, ponieważ Thread.Abort jest bardzo wyraźnie umieszczony w samym środku opublikowanego kodu tutaj. Rozumiem, że to przykładowy kod i tak dalej, ale powinieneś umieścić komentarz tam, a nie w komentarzu. W przeciwnym razie otrzymasz te komentarze.
 – 
Lasse V. Karlsen
2 czerwiec 2011, 23:26
1
Hahah... Byłem zbyt wolny, żeby to uprzedzić. Jedynym powodem, dla którego SqlConnection jest poza try/catch, jest to, że mogę spróbować go ponownie otworzyć, gdy złapię ThreadAbort. Ten przykład jest jednak całkowicie wymyślony — nie reprezentuje naszego prawdziwego kodu. Nasze transakcje nie są długotrwałe. Omawiane zapytanie zostało skrócone do około 5 sekund czasu wykonania przy ekstremalnie dużym obciążeniu, kiedy to zaczęliśmy zauważać problem. Znowu - wymyślony przykład. Czy możemy skoncentrować się na faktycznym zachowaniu, o które pytam?
 – 
womp
2 czerwiec 2011, 23:26
4
Ale potem padłeś ofiarą kolejnego wielkiego „nie”, nie wysyłaj pytań z kodem, który ma inne problemy niż Twój kod produkcyjny. Ludzie będą rozłączać się z kodem, który publikujesz, niezależnie od tego, jak bardzo jest on wymyślny. Przyjmą, że zawęziłeś problem do tego typu kodu i prosi o pomoc w naprawieniu kodu zgodnie z opublikowanym.
 – 
Lasse V. Karlsen
2 czerwiec 2011, 23:28
2
V. Karlsen To nie jest inny problem. Jest to symulowanie opisanego problemu (przypuszczalnie po to, aby inni mogli go przetestować lub zweryfikować w teście jednostkowym). Zwróć uwagę na waitfor w TSQL.
 – 
user166390
2 czerwiec 2011, 23:30

2 odpowiedzi

Najlepsza odpowiedź

Jest to błąd w implementacji MARS firmy Microsoft. Wyłączenie MARS w ciągu połączenia sprawi, że problem zniknie.

Jeśli potrzebujesz usługi MARS i nie masz nic przeciwko uzależnieniu swojej aplikacji od wewnętrznej implementacji innej firmy, zapoznaj się z http: //dotnet.sys-con.com/node/39040, uwolnij .NET Reflector i spójrz na klasy połączeń i puli. Musisz przechowywać kopię właściwości DbConnectionInternal przed awarią. Później użyj refleksji, aby przekazać odwołanie do metody cofania alokacji w klasie wewnętrznej puli. Spowoduje to zatrzymanie połączenia przez 4:00 - 7:40 minut.

Z pewnością istnieją inne sposoby na wymuszenie wyjęcia połączenia z basenu i wyrzucenie go. Jednak bez poprawki od Microsoftu, konieczna wydaje się refleksja. Wydaje się, że metody publiczne w interfejsie API ADO.NET nie pomagają.

3
Cody Jones 8 listopad 2011, 23:01
Czy MARS nie jest domyślnie wyłączony?
 – 
Sal♦
19 kwiecień 2018, 16:17

Ponieważ używasz SqlConnection z pulą, Twój kod nigdy nie kontroluje zamykania połączeń. Basen jest. Po stronie serwera oczekująca transakcja zostanie wycofana, gdy połączenie zostanie naprawdę zamknięte (zamknięte gniazdo), ale w przypadku łączenia w pulę po stronie serwera nigdy nie nastąpi zamknięcie połączenia. Bez zamknięcia połączenia (przez fizyczne rozłączenie w warstwie gniazda/potoku/LPC lub przez wywołanie sp_reset_connection), serwer nie może przerwać oczekującej transakcji. Tak naprawdę sprowadza się to do tego, że połączenie nie zostaje poprawnie zwolnione/zresetowane. Nie rozumiem, dlaczego próbujesz skomplikować kod przez wyraźne odrzucenie wątku i próbujesz ponownie otworzyć zamkniętą transakcję (to nigdy zadziała). Powinieneś po prostu zawinąć SqlConnection w blok using(...), sugerowane w końcu i połączenie Dispose zostanie uruchomione nawet po przerwaniu wątku.

Moim zaleceniem byłoby, aby wszystko było proste, porzuć fantazyjną obsługę przerywania wątku i zastąp ją zwykłym blokiem „używania” (using(connection) {using(transaction) {code; commit () }}.

Oczywiście zakładam, że nie przenosisz kontekstu transakcji do innego zakresu na serwerze (nie używasz sp_getbindtoken i znajomych, a Ty nie rejestrujesz się w transakcjach rozproszonych).

Ten mały program pokazuje, że Thread.Abort prawidłowo zamyka połączenie, a transakcja jest wycofywana:

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

        }
    }
}
8
Remus Rusanu 3 czerwiec 2011, 02:24
Jeśli porzucę wątek, przerwanie, to nie symuluje problemu. Nawet w bloku using transakcja jest nadal otwarta na serwerze. Uruchom program i zobacz... zmień połączenie, aby użyć bloku using... ten sam problem... Problem polega na tym, że połączenie jest zwracane do puli bez czyszczenia transakcji w bazie danych.
 – 
womp
3 czerwiec 2011, 01:52
1
I nie robimy nic wyszukanego w zapytaniach... to całkiem podstawowe instrukcje aktualizacji, żadnych transakcji rozproszonych itp. - przykładowy program ma po prostu maleńką instrukcję aktualizacji i "czekaj" i pokazuje ten sam problem.
 – 
womp
3 czerwiec 2011, 01:56
Program nieco uprościłem, aby uwzględnić niektóre z Twoich sugestii.
 – 
womp
3 czerwiec 2011, 02:06
3
Testowałem to wielokrotnie i rzeczywiście po kilku iteracjach mogę trafić na problem. Połączenie pozostaje otwarte przez ADO.Net i dlatego transakcja nie jest wycofywana na serwerze. Wstawiony wiersz jest nadal zablokowany. .Net 3.5 kontra R2
 – 
Remus Rusanu
3 czerwiec 2011, 22:29
1
Mamy stwardnienie rozsiane na telefonie w tej sprawie... możemy w końcu wdrożyć obejście refleksji. Zapoznaj się z tym artykułem, aby uzyskać interesujący wgląd w wewnętrzne elementy puli połączeń: dotnet.sys-con.com/node /39040
 – 
womp
4 czerwiec 2011, 01:46