Thread abort оставляет транзакции зомби и сломанный SqlConnection

Я чувствую, что такого поведения не должно происходить. Здесь сценарий:

  • Запустите долгосрочную транзакцию sql.

  • Поток, который запускал команду sql (не по нашему коду!)

  • Когда поток возвращается к управляемому кода, состояние SqlConnection "Закрыто", но транзакция все еще открыт на сервере sql.

  • SQLConnection можно повторно открыть, и вы можете попытаться вызвать откат на транзакции, но у нее нет (не то, что я ожидал бы такого поведения. Дело в том, что нет способа получить доступ к транзакции на db и отбросить ее.)

Проблема заключается в том, что транзакция не очищается должным образом, когда поток прерывается. Это была проблема с .Net 1.1, 2.0 и 2.0 SP1. Мы запускаем .Net 3.5 SP1.

Вот пример программы, которая иллюстрирует проблему.

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

Существует исправление Microsoft, нацеленное на .Net2.0 SP1, который должен был решить эту проблему, но мы, очевидно, имеем более новую DLL (.Net 3.5 SP1), которые не соответствуют номерам версий, указанным в этом исправлении.

Может ли кто-нибудь объяснить это поведение и почему ThreadAbort все еще не очищает транзакцию SQL? Does.Net 3.5 SP1 не включает это исправление или это технически корректно?

Ответ 1

Это ошибка в реализации Microsoft MARS. Если вы отключите MARS в строке соединения, проблема исчезнет.

Если вам требуется MARS и удобны, чтобы ваше приложение зависело от внутренней реализации другой компании, ознакомьтесь с http://dotnet.sys-con.com/node/39040.NET Reflector, и посмотрите классы подключения и пула. Вам необходимо сохранить копию свойства DbConnectionInternal до. Позже, используйте отражение, чтобы передать ссылку на метод удаления в классе внутреннего пула. Это остановит ваше соединение от затяжки в течение 4:00 - 7:40 минут.

Есть, конечно, другие способы принудительного подключения из пула и его утилизации. Однако, несмотря на исправление от Microsoft, рефлексия кажется необходимой. Публичные методы в API ADO.NET, похоже, не помогают.

Ответ 2

Поскольку вы используете SqlConnection с пулом, ваш код никогда не контролирует закрытие соединений. Бассейн. На стороне сервера отложенная транзакция будет отменена, когда соединение будет действительно закрыто (сокет закрыт), но при объединении на стороне сервера никогда не будет видно соединения. Без закрытия соединения (физическим отключением на уровне сокета/канала/LPC или вызовом sp_reset_connection) сервер не может прервать ожидающую транзакцию. Так что это действительно сводится к тому, что соединение не получает должным образом release/reset. Я не понимаю, почему вы пытаетесь усложнить код с явным отключением прерывания потока и попытаетесь открыть закрытую транзакцию (которая никогда не будет работать). Вы должны просто обернуть SqlConnection в блок using(...), подразумеваемый окончательно и соединение Dispose будет выполняться даже при отключении потока.

Моя рекомендация состояла бы в том, чтобы сделать вещи простыми, прекратить обработку прерывистых потоков и заменить их простым "использованием" блока (using(connection) {using(transaction) {code; commit () }}.

Конечно, я предполагаю, что вы не распространяете контекст транзакции на другую область на сервере (вы не используете sp_getbindtoken и друзей, и вы не регистрироваться в распределенных транзакциях).

Эта небольшая программа показывает, что Thread.Abort правильно закрывает соединение, и транзакция откатывается:

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

        }
    }
}