Любой способ различить Отмену и Тайм-аут

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

Я делаю звонки с помощью HttpClient, и я передал HttpMessageHandler, который выполняет кучу журналов. По сути:

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    HttpResponseMessage response = null;

    try
    {
        response = await base.SendAsync(request, cancellationToken);
    }
    catch (OperationCanceledException ex)
    {
        LogTimeout(...);
        throw;
    }
    catch (Exception ex)
    {
        LogFailure(...);
        throw;
    }
    finally
    {
        LogComplete(...);
    }

    return response;
}

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

Есть ли способ это сделать?

Изменить: мне нужно немного пояснить. Служба, выполняющая параллельные вызовы, передает CancellationTokens с тайм-аутом:

var ct = new CancellationTokenSource(TimeSpan.FromSeconds(2));

Поэтому, когда серверу требуется более двух секунд для ответа, я получаю OperationCanceledException, и если я вручную отменяю источник токена (например, потому что другой сервер вернул ошибку через 1 секунду), я все равно получаю OperationCanceledException. В идеале я мог бы посмотреть на CancellationToken.IsCancellationRequested, чтобы определить, было ли оно отменено из-за тайм-аута, в отличие от явного запроса на отмену, но похоже, что вы получаете одно и то же значение независимо от как оно был отменен.


person Ben Randall    schedule 24.02.2016    source источник
comment
Сохранение последней даты и времени клиента. Затем проверьте его с текущим временем. Если разница в дате и времени больше, чем время ожидания, то клиент считается тайм-аутом. В противном случае это отмена. Что-то в этом роде.   -  person Ian    schedule 25.02.2016
comment
Вы можете просто проверить токен отмены, чтобы узнать, была ли запрошена отмена или нет. Даже если возникнет состояние гонки и время запроса истекает, как только вы хотите его отменить, вы, вероятно, все равно захотите игнорировать его, поскольку вы его отменили.   -  person SimpleVar    schedule 25.02.2016


Ответы (2)


Если исключения не говорят вам о разнице между двумя случаями, вам нужно будет проверить либо Task, либо CancellationToken, чтобы увидеть, действительно ли была отмена.

Я бы склонялся к тому, чтобы спросить Task, у которого свойство IsCanceled будет возвращать true, если было выброшено необработанное OperationCanceledException (скорее всего, с использованием CancellationToken.ThrowIfCancellationRequested внутри base.SendAsync). Что-то вроде этого...

HttpResponseMessage response = null;
Task sendTask = null;

try
{
  sendTask = base.SendAsync(request, cancellationToken);
  await sendTask;
}
catch (OperationCanceledException ex)
{
  if (!sendTask.IsCancelled)
  {
    LogTimeout(...);
    throw;
  }
}

ИЗМЕНИТЬ

В ответ на обновление вопроса я хотел обновить свой ответ. Вы правы, отмена, независимо от того, запрашивается ли она специально на CancellationTokenSource или если она вызвана тайм-аутом, приведет к точно такому же результату. Если вы декомпилируете CancellationTokenSource, вы увидите, что для тайм-аута он просто устанавливает обратный вызов Timer, который будет явно вызывать CancellationTokenSource.Cancel по истечении тайм-аута, поэтому оба способа в конечном итоге вызовут один и тот же метод Cancel.

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

Это прискорбно, так как у вас будут доступны как ваш пользовательский метод отмены, так и исходный метод Cancel, и вы должны обязательно использовать пользовательский метод. Возможно, вы сможете обойтись без своей пользовательской логики, просто скрыв существующую операцию Cancel примерно так:

class CustomCancellationTokenSource : CancellationTokenSource
{
  public bool WasManuallyCancelled {get; private set;}

  public new void Cancel()
  {
    WasManuallyCancelled = true;
    base.Cancel();
  }
}

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

person Chiune Sugihara    schedule 25.02.2016
comment
Я добавил пояснение к моему сообщению о том, что я имею дело с двумя сценариями: 1. Я звоню CancellationTokenSource.Cancel(), чтобы запросить отмену задачи, или 2. Тайм-аут, предоставленный моему CancellationTokenSource, истекает и отменяет запрос для меня. Это сделано для того, чтобы я не ждал 30 секунд для возврата каждого запроса, но, к сожалению, это означает, что я получаю OperationCanceledException в обоих случаях, и кажется, что IsCancellationRequested верно в обоих случаях. - person Ben Randall; 25.02.2016
comment
Обновил ответ, дайте мне знать, что вы думаете. - person Chiune Sugihara; 25.02.2016
comment
CustomCancellationTokenSource — это, по сути, то, что у меня есть сейчас. Я представил его как новый метод: CancelWithoutError(), который устанавливает флаг. К сожалению, в моем HttpMessageHandler у меня нет доступа к CancellationTokenSource без использования отражения, чтобы вытащить его из CancellationToken :(. Вместо этого я установил статическую ссылку, указывающую на CurrentSafeCancellationTokenSource, и в моем обработчике я проверяю, есть ли является безопасным источником токена, и если да, я проверяю, был ли запрос отменен вручную Это немного скрывает детали, но по сути это то, как это работает. - person Ben Randall; 25.02.2016
comment
Я не знаю, что вы можете сделать лучше, чем это простым способом. Если вы хотите усложниться, вы можете проанализировать трассировку стека выброшенного исключения, вызов Cancel является результатом некоторых внутренних вызовов, если это тайм-аут, чтобы вы могли использовать это, чтобы различать два. - person Chiune Sugihara; 25.02.2016
comment
К сожалению, исключение генерируется, когда вызывающая сторона (базовый HttpClient в данном случае) проверяет, были ли они отменены, поэтому вы даже не видите вызов Cancel как часть стека. - person Ben Randall; 25.02.2016
comment
Проверить этот стек будет сложнее. Я думаю, что вы смотрите на вызов, который будет брошен, если будет запрошена отмена. Что вам, вероятно, нужно сделать, так это зарегистрировать обратный вызов отмены, используя одну из перегрузок CancellationToken.Register. Это будет срабатывать всякий раз, когда запрашивается отмена, и оттуда стеки для двух путей должны быть разными. Вы можете передать аргумент, чтобы вы могли установить внешнее состояние, чтобы вы знали, какой это был результат. Опять же, это становится все более сложной территорией, которую я бы не рекомендовал... - person Chiune Sugihara; 25.02.2016
comment
Да, я тоже изучал CancellationToken.Register, но у него была неприятная проблема: он не обязательно вызывался до вызова обработчика исключений. :( - person Ben Randall; 26.02.2016
comment
Для меня это должен быть принятый ответ, потому что он единственный, который действительно работает. - person bN_; 01.09.2020

Если вы хотите различать два типа отмены, вам нужно использовать два разных токена отмены. Другого пути нет. Это не так сложно, поскольку они могут быть связаны - просто немного неловко.

Самый чистый способ написать эту IMO — переместить код тайм-аута в метод SendAsync вместо вызывающего метода:

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
  using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
  {
    cts.CancelAfter(TimeSpan.FromSeconds(2));
    try
    {
      return await base.SendAsync(request, cts.Token);
    }
    catch (OperationCanceledException ex)
    {
      if (cancellationToken.IsCancellationRequested)
        return null;
      LogTimeout(...);
      throw;
    }
    catch (Exception ex)
    {
      LogFailure(...);
      throw;
    }
    finally
    {
      LogComplete(...);
    }
  }
}

Если вы не хотите перемещать код тайм-аута в SendAsync, вам также нужно будет вести журнал вне этого метода.

person Stephen Cleary    schedule 25.02.2016
comment
Спасибо за предложение. Возможно, мне удастся сделать что-то подобное, если я скажу, что все вызовы, выполняемые с заданным HttpClient, должны иметь одинаковый тайм-аут, и я могу просто сделать его параметром при построении HttpMessageHandler, иначе я думаю, что я' придется придерживаться решения CustomCancellationTokenSource, предоставленного Chiune. - person Ben Randall; 25.02.2016