Пул соединений возвращает тот же экземпляр исключения для двух потоков с использованием той же плохой строки соединения?

Хорошо, это похоже на основную ошибку в .NET:

Рассмотрим следующую простую программу, которая намеренно пытается подключиться к несуществующей базе данных:

class Program
{
    static void Main(string[] args)
    {            

        Thread threadOne = new Thread(GetConnectionOne);
        Thread threadTwo = new Thread(GetConnectionTwo);            
        threadOne.Start();
        threadTwo.Start();

    }



    static void GetConnectionOne()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection("Data Source=.\\wfea;Initial Catalog=zc;Persist Security Info=True;Trusted_Connection=yes;"))
            {
                conn.Open();
            }    
        } catch (Exception e)
        {
            File.AppendAllText("ConnectionOneError.txt", e.Message + "\n" + e.StackTrace + "\n");
        }

    }


    static void GetConnectionTwo()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection("Data Source=.\\wfea;Initial Catalog=zc;Persist Security Info=True;Trusted_Connection=yes;"))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            File.AppendAllText("ConnectionTwoError.txt", e.Message + "\n" + e.StackTrace + "\n");
        }

    }
}

Запустите эту программу и установите контрольные точки в блоках catch. Объект DBConnection будет пытаться подключиться в течение 15 секунд (в обоих потоках), после чего он выдаст ошибку. Проверьте трассировку стека исключений, и трассировка стека будет иметь двойные стеки вызовов, как показано ниже:

at System.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject)
at System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection owningConnection)
at System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory)
at System.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject)
at System.Data.ProviderBase.DbConnectionPool.GetConnection(DbConnection owningObject)
at System.Data.SqlClient.SqlConnection.Open()
at ZoCom2Test.Program.GetConnectionOne() in C:\src\trunk\ZTest\Program.cs:line 38
at System.Data.ProviderBase.DbConnectionFactory.GetConnection(DbConnection owningConnection)
at System.Data.ProviderBase.DbConnectionClosed.OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory)
at System.Data.SqlClient.SqlConnection.Open()
at ZoCom2Test.Program.GetConnectionTwo() in C:\src\trunk\ZTest\Program.cs:line 54

Возможно, вам придется несколько раз попробовать, чтобы это произошло, но я хочу, чтобы это произошло прямо сейчас на моей машине. Как это возможно? Это должно быть абсолютно невозможно на уровне VM. Похоже, функция DBConnection.Open() одновременно бросает одно и то же исключение на два потока одновременно или что-то странное.

Ответы

Ответ 1

Попробуйте это вместо этого и посмотрите, что произойдет:

class ThreadingBug
{
    private const string CONNECTION_STRING =
        "Data Source=.\\wfea;Initial Catalog=catalog;Persist Security Info=True;Trusted_Connection=yes;";

    static void Main(string[] args)
    {
        try
        {
            Thread threadOne = new Thread(GetConnectionOne);
            Thread threadTwo = new Thread(GetConnectionTwo);
            threadOne.Start();
            threadTwo.Start();

            threadOne.Join(2000);
            threadTwo.Join(2000);
        }
        catch (Exception e)
        {
            File.AppendAllText("Main.txt", e.ToString());
        }
    }

    static void GetConnectionOne()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            File.AppendAllText("GetConnectionOne.txt", e.ToString());
        }
    }

    static void GetConnectionTwo()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            File.AppendAllText("GetConnectionTwo.txt", e.ToString());
        }
    }
}

Я считаю, что здесь есть ошибка, хотя это ни главное, ни фундаментальное. После того, как вы сузили это (и делали что-то вроде удаления одного потока), похоже, что один и тот же экземпляр класса Exception генерируется реализацией пула подключений на обоих потоках (для Gregory для этого). Иногда это проявляется как коррумпированная ( "смешанная" ) трассировка стека, а иногда просто как одна и та же трассировка стека для обоих потоков, даже когда код отличается от двух потоков.

Комментирование одного из вызовов Thread.Start показывает совершенно другую трассировку стека, демонстрируя, что нечетная часть находится в реализации пула соединений - трассы нечетных стеков передаются пулом соединений, поскольку оба потока используют один и тот же строка подключения и учетные данные.

Я отправил сообщение об ошибке Connect this на https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=522506. Каждый человек должен свободно проголосовать за то, насколько важно (или неважно) вы его ощущаете, можете ли вы воспроизвести его или у вас есть обходной путь. Это поможет Microsoft определить приоритет исправления.


Обновление: Проблема с подключением была обновлена. Microsoft признает это как ошибку и планирует исправить ее в будущей версии.

Благодаря nganju, Gregory и всем остальным, которые участвовали в решении этой проблемы. Это была ошибка, и она будет исправлена, и это из-за нас.

Ответ 2

ОК, мне удалось воспроизвести это (VS2008, FX3.5SP1, двухъядерный) как внутри , так и вне (*) отладчика. И после того, как вы немного изменили свою логику catch, она даже надежно воспроизводится. И, как упоминал Грегори, это один и тот же экземпляр исключения, брошенный в оба потока.

Это должно быть совершенно невозможно в уровень VM.

Откуда у тебя такая идея?

Оба потока пытаются подключиться через пул соединений. Я ничего не знаю о том, как работает пул, но я угадаю: он сериализует 2 одновременных запроса. Похоже, что это хорошо для Сервера. И затем, когда попытка не удалась, у нее есть 1 исключение и 2 ожидающих потока.

Я тоже ожидал, что CLR или ConnectionPool дублируют исключение и добавят 2 отдельных стека, но вместо этого он объединяет 2 вызова.

Итак, я думаю, что ваша ошибка могла бы быть функцией, статус: по дизайну.
Потому что на самом деле это не "смешанная" стопка, а скорее умышленно Y-образная. Это не выглядит случайным.

Было бы неплохо, если бы кто-нибудь нашел ссылку на это поведение. Сейчас я не уверен, что это CLR или функция ConnectionPool.

(*) Изменить: Я думаю, что видел его за пределами отладчика один раз, но теперь я не могу воспроизвести это. Таким образом, это может быть отладчик или проблема с синхронизацией.

Ответ 3

Это не ошибка в виртуальной машине. Вот ваша оскорбительная линия:

private static readonly DbConnectionFactory _connectionFactory;

Внутри этого мы имеем пул соединений. В котором хранится ссылка на произошедшее исключение. Это открывает условие гонки при выполнении многопоточности.

Как это доказать?

Логически, если вы используете разные пулы соединений, у нас не будет этого состояния гонки. Поэтому я повторил тот же тест с другим источником данных, указанным в строке соединения для каждого потока. Исключения теперь отображаются правильно.

Это действительно случай, когда пул соединений не является потокобезопасным.

Ответ 4

Вы получаете одно и то же исключение. Однако я не понимаю, почему. Посмотрите на окно вывода, в частности, что exception1 == exception2.

class ThreadingBug
{
    private const string CONNECTION_STRING =
        "Data Source=.\\wfea;Initial Catalog=catalog;Persist Security Info=True;Trusted_Connection=yes;";

    static void Main(string[] args)
    {
        try
        {
            Thread threadOne = new Thread(GetConnectionOne);
            Thread threadTwo = new Thread(GetConnectionTwo);
            threadOne.Start();
            threadTwo.Start();

            threadOne.Join(20000);
            threadTwo.Join(20000);

            Debug.WriteLine("Same?" + (exception1 == exception2));
        }
        catch (Exception e)
        {
            Debug.WriteLine("error main" + e);
        }
    }

    static Exception exception1;

    static void GetConnectionOne()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine("Error Con one" + e);
            exception1 = e;
        }
    }
    static Exception exception2;

    static void GetConnectionTwo()
    {
        try
        {
            using (SqlConnection conn = new SqlConnection(CONNECTION_STRING))
            {
                conn.Open();
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine("Error Con two" + e);
            exception2 = e;
        }
    }
}

Изменить: ниже был мой оригинальный ответ.

Скорее всего, ваши "случайные" имена файлов схожи, если не совпадают, поскольку их иногда вызывают в очень близкие временные рамки. Часто, когда у вас есть проблема, которая случайно появляется, и у вас есть вызов Random.Next, это должно быть первое, что вы смотрите.