Отмена запроса SQL Server с помощью CancellationToken

У меня есть долго работающая хранимая процедура в SQL Server, которую мои пользователи должны иметь возможность отменить. Я написал небольшое тестовое приложение, как показано ниже, которое демонстрирует, что метод SqlCommand.Cancel() работает довольно хорошо:

    private SqlCommand cmd;
    private void TestSqlServerCancelSprocExecution()
    {
        TaskFactory f = new TaskFactory();
        f.StartNew(() =>
            {
              using (SqlConnection conn = new SqlConnection("connStr"))
              {
                conn.InfoMessage += conn_InfoMessage;
                conn.FireInfoMessageEventOnUserErrors = true;
                conn.Open();

                cmd = conn.CreateCommand();
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.CommandText = "dbo.[CancelSprocTest]";
                cmd.ExecuteNonQuery();
              }
           });
    }

    private void cancelButton_Click(object sender, EventArgs e)
    {
        if (cmd != null)
        {
            cmd.Cancel();
        }
    }

После вызова cmd.Cancel() я могу убедиться, что базовая хранимая процедура прекращает выполнение практически сразу. Учитывая, что я довольно часто использую шаблон async / await в своем приложении, я надеялся, что методы async на SqlCommand, которые принимают параметры CancellationToken, будут работать одинаково хорошо. К сожалению, я обнаружил, что вызов Cancel() на CancellationToken приводит к тому, что обработчик событий InfoMessage больше не вызывается, но базовая хранимая процедура продолжает выполняться. Мой тестовый код для асинхронной версии выглядит следующим образом:

    private SqlCommand cmd;
    private CancellationTokenSource cts;
    private async void TestSqlServerCancelSprocExecution()
    {
        cts = new CancellationTokenSource();
        using (SqlConnection conn = new SqlConnection("connStr"))
        {
            conn.InfoMessage += conn_InfoMessage;
            conn.FireInfoMessageEventOnUserErrors = true;
            conn.Open();

            cmd = conn.CreateCommand();
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.CommandText = "dbo.[CancelSprocTest]";
            await cmd.ExecuteNonQueryAsync(cts.Token);
        }
    }

    private void cancelButton_Click(object sender, EventArgs e)
    {
        cts.Cancel();
    }

Я что-то упустил в том, как CancellationToken должен работать? Я использую .NET 4.5.1 и SQL Server 2012, если это имеет значение.

РЕДАКТИРОВАТЬ: я переписал тестовое приложение как консольное приложение на случай, если контекст синхронизации был фактором, и я вижу такое же поведение - вызов CancellationTokenSource.Cancel() не останавливает выполнение базовой хранимой процедуры.

РЕДАКТИРОВАТЬ: вот тело хранимой процедуры, которую я вызываю, если это имеет значение. Он вставляет записи и распечатывает результаты с интервалом в одну секунду, чтобы можно было легко увидеть, были ли попытки отмены вступили в силу незамедлительно.

WHILE (@loop <= 40)
BEGIN

  DECLARE @msg AS VARCHAR(80) = 'Iteration ' + CONVERT(VARCHAR(15), @loop);
  RAISERROR (@msg,0,1) WITH NOWAIT;
  INSERT INTO foo VALUES (@loop);
  WAITFOR DELAY '00:00:01.01';

  SET @loop = @loop+1;
END;

person Dan Hermann    schedule 14.07.2014    source источник
comment
вы не получаете исключение TaskCancellation при отмене задачи?   -  person loop    schedule 14.07.2014
comment
@loop Нет, у меня нет TaskCancellationException в асинхронной версии. В неасинхронной версии я получаю ожидаемое исключение SqlException при отмене команды.   -  person Dan Hermann    schedule 14.07.2014
comment
Можете ли вы попробовать запустить TestSqlServerCancelSprocExecution () после отмены задачи.   -  person loop    schedule 14.07.2014
comment
@loop Если я сначала отменяю задачу, я получаю TaskCanceledException в строке cmd.ExecuteNonQueryAsync.   -  person Dan Hermann    schedule 14.07.2014
comment
@Dan - Может ли это из-за текущего контекста - я предполагаю здесь. Попробуйте cmd.ExecuteNonQueryAsync (cts.Token) .ConfigureAwait (false);   -  person Venki    schedule 14.07.2014
comment
@Venki Нет никакой разницы в поведении с вызовом ConfigureAwait (false). Я подумал, что вы могли что-то понять в отношении контекста синхронизации, потому что все приложение зависает после того, как я нажимаю кнопку отмены. Чтобы исключить контекст синхронизации как фактор, я переписал тестовое приложение как консольное (см. Правку выше), но хранимая процедура продолжала выполняться после того, как я вызвал CancellationToken.Cancel () в тестовом приложении консоли.   -  person Dan Hermann    schedule 14.07.2014
comment
Я не понимаю, почему вы могли подумать, что отмена CancellationToken будет похожа на вызов cmd.Cancel. Можете указать мне, где вы это читаете? Вы видели msdn.microsoft.com/en-us/library/hh211418.aspx?   -  person John Saunders    schedule 19.07.2014
comment
@JohnSaunders Я нигде этого не читал. Я предположил (очевидно неправильно), что, поскольку CancellationTokens в TPL явно взаимодействуют, SqlCommand будет использовать тот же механизм для отмены в асинхронном методе, когда он получает запрос на отмену через CancellationToken, как это происходит, когда он получает запрос на отмену через Отменить метод в синхронном методе.   -  person Dan Hermann    schedule 19.07.2014
comment
Для всех, у кого есть эта проблема, есть известная ошибка в async cancel, см. github.com/dotnet/ SqlClient / issues / 44.   -  person Rhys Jones    schedule 12.04.2020


Ответы (1)


Посмотрев на то, что делает ваша хранимая процедура, кажется, что она каким-то образом блокирует отмену.

Если вы измените

RAISERROR (@msg,0,1) WITH NOWAIT;

чтобы удалить предложение WITH NOWAIT, отмена будет работать должным образом. Однако это предотвращает запуск InfoMessage событий в реальном времени.

Вы можете отслеживать ход длительной хранимой процедуры другим способом или зарегистрироваться для отмены токена и вызвать cmd.Cancel(), поскольку знаете, что это работает.

Еще одно замечание: в .NET 4.5 вы можете просто использовать Task.Run вместо создания экземпляра TaskFactory.

Итак, вот рабочее решение:

private CancellationTokenSource cts;
private async void TestSqlServerCancelSprocExecution()
{
    cts = new CancellationTokenSource();
    try
    {
        await Task.Run(() =>
        {
            using (SqlConnection conn = new SqlConnection("connStr"))
            {
                conn.InfoMessage += conn_InfoMessage;
                conn.FireInfoMessageEventOnUserErrors = true;
                conn.Open();

                var cmd = conn.CreateCommand();
                cts.Token.Register(() => cmd.Cancel());
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.CommandText = "dbo.[CancelSprocTest]";
                cmd.ExecuteNonQuery();
            }
       });
    }
    catch (SqlException)
    {
        // sproc was cancelled
    }
}

private void cancelButton_Click(object sender, EventArgs e)
{
    cts.Cancel();
}

В моем тестировании этого я должен был обернуть ExecuteNonQuery в Task, чтобы cmd.Cancel() работал. Если бы я использовал ExecuteNonQueryAsync, даже не передав ему токена, система заблокировала бы cmd.Cancel(). Я не уверен, почему это так, но включение синхронного метода в задачу обеспечивает аналогичное использование.

person CoderDennis    schedule 18.07.2014
comment
Это прекрасно работает. Досадно, что виной всему было наличие предложения WITH NOWAIT, но, по крайней мере, есть довольно простой обходной путь. - person Dan Hermann; 19.07.2014
comment
@CoderDennis, я использую это решение, но при отмене выдается исключение: SqlException, ... операция отменена пользователем. Ты знаешь почему ? - person Hamza_L; 08.04.2016
comment
@Hamza_L я давно не смотрел этот код, но думаю, поэтому я заключил выполнение задачи в блок try...catch. Если вы отмените, это вызовет исключение. - person CoderDennis; 08.04.2016
comment
@Hamza_L Операция, отмененная пользовательским исключением, - это именно то, что должно произойти, поскольку операция отмены была инициирована пользователем, нажав кнопку отмены. Если вы не хотите, чтобы это вызвало исключение, просто проглотите его. - person Kidquick; 25.05.2017
comment
Лучший проглатывающий код, который я мог придумать, был: catch (SqlException sqlex) {switch (sqlex.ErrorCode) {case -2146232060: / * -2146232060 довольно общий, проверьте .Message тоже * / if (! Sqlex.Message.Contains (отменено)) {бросить; } ломать; по умолчанию: бросить; }} - person granadaCoder; 30.11.2017
comment
Вам нужно поместить try catch в задачу ожидания, иначе она выдаст ошибку и остановит программу. Я поместил пробную ловушку вокруг cmd.ExecuteNonQuery (); - person user890332; 08.01.2020