SqlCommand ExecuteNonQuery выдает OutOfMemoryException

У меня проблема с выполнением Sql, который на самом деле является простым вызовом хранимой процедуры на SqlServer.

Рассмотрим ниже хранимую процедуру Sql:

    CREATE PROCEDURE InfiniteLoop
    AS
    BEGIN
        DECLARE @ixi NUMERIC(38) = 0
        DECLARE @i NUMERIC(38) = 0
        WHILE 1 = 1
        BEGIN
           SET @i = @i + 1
           SET @ixi = @ixi * @i
           PRINT N'some text'
        END
    END;

Теперь я вызываю эту процедуру из C# следующим образом:

public void CallProcedure()
{
   SqlCommand cmd = this._connection.CreateCommand();
   cmd.CommandType = CommandType.StoredProcedure;
   command.CommandText = 'InfiniteLoop';

   //Line below throws OutOfMemoryException
   cmd.ExecuteNonQuery();

   cmd.Dispose();
}

Память начинает расти очень быстро. Через несколько секунд выдается исключение. Обычно весь этот код использует разделы «try/catch» и «using», но я упростил этот фрагмент, чтобы показать, что проблема возникает из библиотеки SqlClient, а не из моего кода напрямую.


person pjozwiak    schedule 19.03.2013    source источник
comment
Похоже, вы нашли интересный момент о реализации ExecuteNonQuery. Но какой у тебя вопрос?   -  person lc.    schedule 19.03.2013


Ответы (1)


После дополнительных исследований я нашел решение, как остановить OutOfMemoryException и принять ожидаемое исключение TimeoutException.

В этом случае память увеличивается из-за использования PRINT в хранимой процедуре. Драйвер по умолчанию собирает выходные данные из базы данных. Поэтому, если пользователь не читает это, может возникнуть исключение OutOfMemoryException.

В зависимости от желаемого результата можно использовать два решения.

Первый хорош, когда вывод базы данных не важен для вас, и вы ожидаете тайм-аута, когда выполнение занимает слишком много времени. Фрагмент ниже решает проблему следующим образом:

public void CallProcedure()
{
   // Line below release all Errors/PRINT output from command. Command is now
   // not collecting them, so memory is not growing. 
   // CommandTimeout will be thrown after preset value on command object.
   this._connection.FireInfoMessageEventOnUserErrors = true;

   SqlCommand cmd = this._connection.CreateCommand();
   cmd.CommandTimeout = 15;
   cmd.CommandType = CommandType.StoredProcedure;
   command.CommandText = 'InfiniteLoop';

   //Line below throws OutOfMemoryException
   cmd.ExecuteNonQuery();

   cmd.Dispose();
}

Второй вариант хорош, когда вы хотите выполнить действительно трудоемкие процедуры, которые могут занять много времени. Исключение тайм-аута никогда не произойдет. Чтобы включить это поведение, вам необходимо прикрепить SqlInfoMessageEventHandler к InfoMessage в SqlConnection. См. фрагмент ниже:

public void CallProcedure()
{
   // By attaching this event no Timeout exception on ExecuteNonQuery occur
   // Your ExecuteNonQuery can hang infinitly!
   // If you need Timeout then you need to write you own handler from different thread
   this._connection.InfoMessage += new SqlInfoMessageEventHandler(OnInfoMessage);

   // Line below release all Errors/PRINT output from command. Command is now
   // not collecting them so memory is not growing.
   this._connection.FireInfoMessageEventOnUserErrors = true;

   SqlCommand cmd = this._connection.CreateCommand();

   // In this case Timeout will never occur
   cmd.CommandTimeout = 15;
   cmd.CommandType = CommandType.StoredProcedure;
   command.CommandText = 'InfiniteLoop';

   //Line below throws OutOfMemoryException
   cmd.ExecuteNonQuery();

   cmd.Dispose();

   this._connection.InfoMessage -= new SqlInfoMessageEventHandler(OnInfoMessage);
}

void OnInfoMessage(object sender, SqlInfoMessageEventArgs e)
{
   System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString()+": "+ e.Message);
}
person pjozwiak    schedule 20.03.2013