Прерывание потока оставляет транзакции-зомби и неработающую SqlConnection

#c# #.net #sql-server #multithreading #thread-abort

#c# #.net #sql-сервер #многопоточность #прерывание потока

Вопрос:

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

  1. Запустите длительную транзакцию sql.

  2. Поток, который запустил команду sql, прерывается (не нашим кодом!)

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

  4. SqlConnection можно повторно открыть, и вы можете попытаться вызвать откат транзакции, но это не имеет никакого эффекта (не то чтобы я ожидал такого поведения. Дело в том, что нет способа получить доступ к транзакции в базе данных и откатить ее.)

Проблема просто в том, что транзакция не очищается должным образом, когда поток прерывается. Это была проблема с .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. nn");

                Console.Read();
            }
        }
    }
}
  

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

Кто-нибудь может объяснить это поведение и почему ThreadAbort все еще не выполняет надлежащую очистку транзакции sql? В .Net 3.5 SP1 нет этого исправления, или такое поведение технически корректно?

Комментарии:

1. Пожалуйста, никаких комментариев по поводу неиспользования Thread. Прерывание — мы нигде его не используем. Просто иногда факт жизни, который выдает IIS, вызывает их, если вы случайно получаете переработку appdomain или что-то еще. Мы не используем Thread. Прерывание в любом месте нашего кода 🙂 Мы только что заметили это поведение и проследили его до этого сценария — пример программы, очевидно, надуманный.

2. Если вы не используете Thread. Прерывание в любом месте вашего кода, вы можете захотеть поместить этот комментарий в этот код, начиная с Thread. Прерывание очень заметно расположено прямо в середине кода, который вы опубликовали здесь. Я понимаю, что это пример кода и все такое, но вы должны поместить комментарий там, а не в комментарии. В противном случае вы получите эти комментарии.

3. Хахах… Я был слишком медленным, чтобы предотвратить это. Единственная причина, по которой SqlConnection находится за пределами try / catch, заключается в том, что я могу попытаться повторно открыть его, когда я перехватываю ThreadAbort. Хотя этот пример полностью надуманный — он не представляет наш реальный код. Наши транзакции не совсем длительные. Время выполнения запроса, о котором идет речь, было увеличено примерно до 5 секунд при чрезвычайно высокой нагрузке, и именно тогда мы начали замечать проблему. Снова — надуманный пример. Можем ли мы, пожалуйста, сосредоточиться на фактическом поведении, о котором я спрашиваю?

4. Но тогда вы стали жертвой другого серьезного «нет-нет», не отправляйте вопросы с кодом, который имеет проблемы, отличные от вашего производственного кода. Люди будут зацикливаться на коде, который вы публикуете, независимо от того, насколько он надуманный. Они предположат, что вы сузили проблему до этого типа кода, и просят помощи в исправлении кода в соответствии с опубликованным.

5. @Лассе В. Карлсен, Это не другая проблема. Это имитирует проблему, как описано (предположительно, чтобы другие могли ее протестировать или это можно было проверить в модульном тестировании). Обратите внимание на включенный waitfor в TSQL.

Ответ №1:

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

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

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

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

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

        }
    }
}
  

Комментарии:

1. Если я отключаю прерывание потока, то это не имитирует проблему. Даже внутри блока using транзакция все еще открыта на сервере. Запустите программу и посмотрите…. измените соединение, чтобы использовать блок using … та же проблема… Проблема в том, что соединение возвращается в пул без очистки транзакции в базе данных.

2. И мы не делаем ничего необычного в запросах… это довольно простые инструкции по обновлению, никаких распределенных транзакций и т.д. — в примере программы есть только крошечная инструкция update и «waitfor», и она показывает ту же проблему.

3. Я немного упростил программу, чтобы учесть некоторые ваши предложения.

4. Я неоднократно тестировал это, и действительно, после нескольких итераций я могу решить проблему. Соединение остается открытым из-за ADO. Net и, следовательно, транзакция не откатывается на сервере. Вставленная строка по-прежнему заблокирована. .Net 3.5 против R2

5. Мы связались с MS по телефону по этому поводу… в конечном итоге мы можем реализовать обходной путь отражения. Ознакомьтесь с этой статьей, чтобы получить интересное представление о внутренних компонентах пула соединений: dotnet.sys-con.com/node/39040

Ответ №2:

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

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

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

Комментарии:

1. Разве MARS не отключен по умолчанию?